+39
-35
.beads/issues.jsonl
+39
-35
.beads/issues.jsonl
···
1
1
{"id":"atproto-1","title":"AT Protocol OCaml Library Suite","description":"Implement a comprehensive suite of OCaml libraries for the AT Protocol (Authenticated Transfer Protocol), enabling developers to build decentralized social networking applications. The implementation should be I/O engine agnostic using OCaml 5.4 effects, pass all public conformance tests, and leverage the OCaml ecosystem effectively.","design":"## Architecture Overview\n\nThe library suite follows the AT Protocol's layered architecture:\n\n1. **Foundation Layer** - Core primitives (syntax, crypto, encoding)\n2. **Data Layer** - IPLD, repositories, MST (Merkle Search Tree)\n3. **Identity Layer** - DIDs, handles, resolution\n4. **Network Layer** - XRPC transport, event streams, sync\n5. **Application Layer** - Lexicon schemas, high-level API\n\n## Design Principles\n\n- **Effects-based I/O**: Use OCaml 5.4 algebraic effects for I/O abstraction\n- **Functional-first**: Immutable data structures, pure functions where possible\n- **Separate packages**: Each component as independent opam package\n- **Test-driven**: Pass all AT Protocol interop tests\n- **Spec-compliant**: Follow atproto.com/specs exactly\n- **No regex**: All syntax validation uses hand-written parsers/codecs\n- **jsont for JSON**: Use jsont library for all JSON serialization\n\n## Package Structure\n\n```\natproto-syntax - Identifier parsing/validation (parser-based)\natproto-crypto - P-256/K-256 cryptography\natproto-multibase - Base encoding (base32, base58btc)\natproto-ipld - DAG-CBOR, CIDs, CAR files\natproto-mst - Merkle Search Tree\natproto-repo - Repository operations\natproto-identity - DID/Handle resolution\natproto-xrpc - HTTP API client/server\natproto-sync - Repository synchronization\natproto-lexicon - Schema language\natproto-api - High-level client API\n```\n\n## Core Dependencies\n\n| Purpose | Library |\n|---------|---------|\n| JSON | jsont |\n| Crypto (P-256) | mirage-crypto-ec |\n| Crypto (K-256) | hacl-star |\n| Hashing | digestif |\n| Time | ptime |\n| I/O (testing) | eio |","acceptance_criteria":"- All packages build with OCaml 5.4\n- All interop tests from bluesky-social/atproto-interop-tests pass\n- Effects-based I/O allows pluggable runtime (eio, lwt, etc.)\n- Documentation for each package\n- Example applications demonstrating usage","notes":"## Research Summary (Dec 2025)\n\n### Library Decisions\n\n| Component | Library | Rationale |\n|-----------|---------|-----------|\n| JSON | `jsont` | Declarative codecs, no intermediate repr |\n| CBOR | `cbor` + wrapper | Use existing, add DAG-CBOR sorting |\n| P-256 | `mirage-crypto-ec` | Mature, RFC 6979 support |\n| K-256 | `secp256k1-ml` | Auto low-S, RFC 6979 built-in |\n| Hashing | `digestif` | SHA-256 |\n| Time | `ptime` + `mtime` | High-res timestamps for TID |\n| Big integers | `zarith` | For low-S normalization |\n\n### Key Implementation Notes from Pegasus\n\n1. **DAG-CBOR**: Sort keys by length first, then lexicographically\n2. **CID**: Cache raw bytes, support empty CIDs\n3. **TID**: Use 2-bit chunks for layer calculation\n4. **MST**: Lazy async node hydration, functor over blockstore\n5. **Low-S**: Use Zarith, always left-pad to 32 bytes\n\n### Interop Test Categories\n- syntax/ - 7 identifier types (handle, did, nsid, tid, aturi, datetime, recordkey)\n- crypto/ - signature verification, did:key encoding\n- data-model/ - CBOR encoding, CID computation\n- mst/ - key heights, common prefix\n- lexicon/ - schema and record validation","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:27.257433425+01:00","updated_at":"2025-12-28T13:36:59.7795445+01:00","closed_at":"2025-12-28T13:36:59.7795445+01:00","labels":["atproto","epic","ocaml"]}
2
-
{"id":"atproto-10","title":"Foundation Layer - Core Primitives","description":"Implement the foundation layer libraries that provide core primitives for the AT Protocol. This includes identifier parsing/validation, cryptographic operations, and base encoding utilities.","design":"## Packages\n\n### atproto-syntax\n- Handle validation (domain format) - **parser-based, no regex**\n- DID validation (did:plc, did:web) - **parser-based, no regex**\n- NSID validation (namespaced identifiers) - **parser-based, no regex**\n- TID generation and validation - **codec-based**\n- Record key validation - **parser-based**\n- AT-URI parsing - **recursive descent parser**\n- Datetime parsing (RFC-3339) - **hand-written parser**\n\n### atproto-crypto\n- P-256 (secp256r1) keypair generation/signing\n- K-256 (secp256k1) keypair generation/signing\n- Low-S signature normalization (required by ATP)\n- RFC 6979 deterministic signatures\n- did:key encoding/decoding\n- JWT creation and verification (using jsont)\n\n### atproto-multibase\n- Base32 encoding/decoding (ATP blessed format)\n- Base58btc encoding/decoding (for did:key)\n- Multibase prefix handling\n\n## Design Principles\n\n- **No regex**: All syntax validation uses hand-written parsers\n- **Codec-based**: Use jsont for JSON serialization\n- **Parser combinators optional**: Can use angstrom if needed, but prefer hand-written for simplicity\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- hacl-star or secp256k1 (K-256)\n- digestif (SHA-256)\n- jsont (JSON handling)\n- ptime (datetime)\n- **NO re or pcre**","acceptance_criteria":"- atproto-syntax package validates all identifier types\n- atproto-crypto package supports P-256 and K-256 with low-S normalization\n- atproto-multibase package supports base32 and base58btc\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:38.666246387+01:00","updated_at":"2025-12-28T11:57:30.662537723+01:00","closed_at":"2025-12-28T11:57:30.662537723+01:00","labels":["epic","foundation"],"dependencies":[{"issue_id":"atproto-10","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:13.213777505+01:00","created_by":"daemon"}]}
3
-
{"id":"atproto-11","title":"Implement atproto-syntax package","description":"Implement the atproto-syntax package providing parsers and validators for all AT Protocol identifier types.","design":"## Module Structure\n\n```ocaml\n(* atproto-syntax/lib/handle.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval normalize : t -\u003e t (* lowercase *)\n\n(* atproto-syntax/lib/did.ml *)\ntype method_ = Plc | Web | Key | Other of string\ntype t = { method_: method_; identifier: string }\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\n\n(* atproto-syntax/lib/nsid.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval authority : t -\u003e string\nval name : t -\u003e string\n\n(* atproto-syntax/lib/tid.ml *)\ntype t\nval generate : unit -\u003e t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval timestamp_us : t -\u003e int64\n\n(* atproto-syntax/lib/at_uri.ml *)\ntype t = { authority: [ `Did of Did.t | `Handle of Handle.t ]; \n collection: Nsid.t option;\n rkey: Record_key.t option }\n\n(* atproto-syntax/lib/datetime.ml *)\nval parse : string -\u003e (Ptime.t, error) result\nval format : Ptime.t -\u003e string\n```\n\n## Parser-based Validation (NO REGEX)\n\n### Handle Parser\n```ocaml\n(* Requirements from interop tests:\n - Max 253 chars total, max 63 chars per segment\n - At least 2 segments\n - Segments: alphanumeric + hyphens (not at start/end)\n - Case-insensitive, normalize to lowercase\n*)\nlet parse_handle s =\n if String.length s \u003e 253 then Error `Too_long\n else\n let labels = String.split_on_char '.' s in\n if List.length labels \u003c 2 then Error `Too_few_segments\n else if not (List.for_all valid_label labels) then Error `Invalid_label\n else if not (valid_tld (List.hd (List.rev labels))) then Error `Invalid_tld\n else Ok (normalize s)\n```\n\n### TID Parser (from Pegasus)\n```ocaml\nlet charset = \"234567abcdefghijklmnopqrstuvwxyz\"\nlet first_char_valid = \"234567abcdefghij\" (* High bit = 0 *)\n\nlet parse_tid s =\n if String.length s \u003c\u003e 13 then Error `Invalid_length\n else if not (String.contains first_char_valid s.[0]) then Error `High_bit_set\n else if not (String.for_all (fun c -\u003e String.contains charset c) s) then\n Error `Invalid_char\n else Ok s\n```\n\n### DateTime Parser (strict ISO 8601)\n```ocaml\n(* From interop tests - strict requirements:\n - Uppercase T and Z required\n - Timezone required (Z or +/-HH:MM)\n - 4-digit year, 2-digit month/day/hour/min/sec\n*)\nlet parse_datetime s =\n (* Hand-written parser, not regex *)\n let year = parse_4_digits s 0 in\n let month = parse_2_digits s 5 in\n let day = parse_2_digits s 8 in\n (* ... validate T separator at pos 10 ... *)\n let hour = parse_2_digits s 11 in\n (* ... continue ... *)\n```\n\n### Record Key Parser\n```ocaml\n(* From interop tests:\n - Max 512 chars\n - Allowed: alphanumeric + . - _ : ~\n - Cannot be \".\" or \"..\"\n*)\nlet valid_rkey_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = ':' || c = '~'\n\nlet parse_record_key s =\n if String.length s = 0 || String.length s \u003e 512 then Error `Invalid_length\n else if s = \".\" || s = \"..\" then Error `Reserved\n else if not (String.for_all valid_rkey_char s) then Error `Invalid_char\n else Ok s\n```\n\n## Dependencies\n- ptime (datetime handling)\n- mtime (high-res timestamps for TID generation)\n- NO regex libraries","acceptance_criteria":"- Handle regex validation per spec\n- DID validation for did:plc and did:web\n- NSID validation with 317 char limit\n- TID generation with microsecond precision\n- Record key validation for all types\n- AT-URI parsing and construction\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:36.014427755+01:00","updated_at":"2025-12-28T01:03:43.574485354+01:00","closed_at":"2025-12-28T01:03:43.574485354+01:00","labels":["foundation","syntax"],"dependencies":[{"issue_id":"atproto-11","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:06.385208896+01:00","created_by":"daemon"}]}
4
-
{"id":"atproto-12","title":"Implement atproto-multibase package","description":"Implement the atproto-multibase package providing base encoding utilities required by AT Protocol.","design":"## Module Structure\n\n```ocaml\n(* atproto-multibase/lib/base32.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base32_sortable.ml *)\n(* ATP uses sortable base32 for TIDs: 234567abcdefghijklmnopqrstuvwxyz *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base58btc.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/multibase.ml *)\ntype encoding = Base32 | Base58btc | ...\nval encode : encoding -\u003e bytes -\u003e string\nval decode : string -\u003e (bytes * encoding, error) result\n```\n\n## Multibase Prefixes\n- `b` = base32lower\n- `z` = base58btc\n\n## No external dependencies needed","acceptance_criteria":"- Base32 encoding per ATP spec (charset 234567abcdefghijklmnopqrstuvwxyz)\n- Base58btc encoding for did:key\n- Multibase prefix handling\n- Round-trip encoding/decoding works correctly","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:43.386843683+01:00","updated_at":"2025-12-28T00:45:51.37610055+01:00","closed_at":"2025-12-28T00:45:51.37610055+01:00","labels":["encoding","foundation"],"dependencies":[{"issue_id":"atproto-12","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:07.330194621+01:00","created_by":"daemon"}]}
5
-
{"id":"atproto-13","title":"Implement atproto-crypto package","description":"Implement the atproto-crypto package providing cryptographic operations for AT Protocol including P-256 and K-256 elliptic curve support.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/keypair.ml *)\nmodule type S = sig\n type public\n type private_\n type signature\n \n val generate : unit -\u003e private_\n val public : private_ -\u003e public\n val sign : private_ -\u003e bytes -\u003e signature\n val verify : public -\u003e bytes -\u003e signature -\u003e bool\n val public_to_bytes : public -\u003e bytes (* compressed *)\n val public_of_bytes : bytes -\u003e (public, error) result\n val signature_to_bytes : signature -\u003e bytes (* 64 bytes, r||s *)\n val signature_of_bytes : bytes -\u003e (signature, error) result\nend\n\n(* atproto-crypto/lib/p256.ml - uses mirage-crypto-ec *)\ninclude Keypair.S\n\n(* atproto-crypto/lib/k256.ml - uses secp256k1-ml *)\ninclude Keypair.S\n(* Note: secp256k1-ml automatically produces low-S signatures *)\n\n(* atproto-crypto/lib/did_key.ml *)\ntype t = P256 of P256.public | K256 of K256.public\nval encode : t -\u003e string (* \"did:key:z...\" *)\nval decode : string -\u003e (t, error) result\n```\n\n## Library Choices\n\n**P-256 (secp256r1)**: Use `mirage-crypto-ec`\n- `P256.Dsa.generate()` for keypairs\n- `P256.Dsa.sign` with RFC 6979\n- `P256.Dsa.pub_to_octets ~compress:true` for serialization\n\n**K-256 (secp256k1)**: Use `secp256k1-ml` (NOT hacl-star)\n- Automatic low-S normalization (libsecp256k1 always produces low-S)\n- RFC 6979 is default behavior\n- `Secp256k1.Key.to_bytes ~compress:true` for compressed keys\n\n## Multicodec Prefixes (for did:key)\n- P-256 public: `0x80 0x24` (multicodec 0x1200)\n- K-256 public: `0xE7 0x01` (multicodec 0xE7)\n\n## Critical: Low-S Normalization\n\nK-256: Handled automatically by secp256k1-ml\n\nP-256: May need manual check using zarith:\n```ocaml\nlet p256_n = Z.of_string\n \"0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551\"\n\nlet is_low_s s =\n let s_z = Z.of_bits (Bytes.to_string s) in\n Z.leq s_z Z.(p256_n / ~$2)\n```\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- secp256k1 (K-256 via secp256k1-ml)\n- digestif (SHA-256)\n- zarith (big integers for low-S check)\n- multibase (for did:key encoding)","acceptance_criteria":"- P-256 key generation and ECDSA signing\n- K-256 key generation and ECDSA signing\n- Low-S signature normalization (required!)\n- RFC 6979 deterministic signatures\n- did:key encoding and decoding\n- All crypto interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:54.960668746+01:00","updated_at":"2025-12-28T01:42:29.522627602+01:00","closed_at":"2025-12-28T01:42:29.522627602+01:00","labels":["crypto","foundation"],"dependencies":[{"issue_id":"atproto-13","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:08.277647286+01:00","created_by":"daemon"},{"issue_id":"atproto-13","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:08:10.535715566+01:00","created_by":"daemon"}]}
6
-
{"id":"atproto-14","title":"Implement JWT support in atproto-crypto","description":"Implement JWT support for AT Protocol authentication including inter-service and access tokens.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/jwt.ml *)\ntype header = { alg: [ `ES256 | `ES256K ]; typ: string }\ntype claims = { \n iss: string; (* DID *)\n aud: string; (* Service DID *)\n exp: int64; (* Expiration timestamp *)\n iat: int64; (* Issued at *)\n lxm: string option; (* Lexicon method *)\n (* ... other claims *)\n}\n\nval create : \n key:[ `P256 of P256.private_ | `K256 of K256.private_ ] -\u003e\n claims:claims -\u003e\n string\n\nval verify :\n key:[ `P256 of P256.public | `K256 of K256.public ] -\u003e\n string -\u003e\n (claims, error) result\n\nval decode_unverified : string -\u003e (header * claims, error) result\n```\n\n## Jsont Codecs for JWT\n\n```ocaml\nlet header_jsont : header Jsont.t =\n Jsont.obj \"jwt_header\" @@ fun o -\u003e\n let alg = Jsont.obj_mem o \"alg\" Jsont.string \n ~dec:(function \"ES256\" -\u003e `ES256 | \"ES256K\" -\u003e `ES256K | _ -\u003e failwith \"invalid alg\")\n ~enc:(function `ES256 -\u003e \"ES256\" | `ES256K -\u003e \"ES256K\") in\n let typ = Jsont.obj_mem o \"typ\" Jsont.string in\n Jsont.obj_finish o { alg; typ }\n\nlet claims_jsont : claims Jsont.t =\n Jsont.obj \"jwt_claims\" @@ fun o -\u003e\n let iss = Jsont.obj_mem o \"iss\" Jsont.string in\n let aud = Jsont.obj_mem o \"aud\" Jsont.string in\n let exp = Jsont.obj_mem o \"exp\" Jsont.int64 in\n let iat = Jsont.obj_mem o \"iat\" Jsont.int64 in\n let lxm = Jsont.obj_mem o \"lxm\" ~opt:true Jsont.string in\n Jsont.obj_finish o { iss; aud; exp; iat; lxm }\n```\n\n## JWT Types for ATP\n- Access token: `typ: \"at+jwt\"`\n- Refresh token: `typ: \"refresh+jwt\"`\n\n## Dependencies\n- atproto-multibase (base64url)\n- jsont","acceptance_criteria":"- JWT creation with ES256 and ES256K algorithms\n- JWT verification with signature validation\n- Token expiration checking\n- Required claims validation (iss, aud, exp, lxm)","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:08:03.209909326+01:00","updated_at":"2025-12-28T11:00:17.646363681+01:00","closed_at":"2025-12-28T11:00:17.646363681+01:00","labels":["auth","crypto"],"dependencies":[{"issue_id":"atproto-14","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:09.279825662+01:00","created_by":"daemon"},{"issue_id":"atproto-14","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:08:11.099737771+01:00","created_by":"daemon"}]}
2
+
{"id":"atproto-10","title":"Foundation Layer - Core Primitives","description":"Implement the foundation layer libraries that provide core primitives for the AT Protocol. This includes identifier parsing/validation, cryptographic operations, and base encoding utilities.","design":"## Packages\n\n### atproto-syntax\n- Handle validation (domain format) - **parser-based, no regex**\n- DID validation (did:plc, did:web) - **parser-based, no regex**\n- NSID validation (namespaced identifiers) - **parser-based, no regex**\n- TID generation and validation - **codec-based**\n- Record key validation - **parser-based**\n- AT-URI parsing - **recursive descent parser**\n- Datetime parsing (RFC-3339) - **hand-written parser**\n\n### atproto-crypto\n- P-256 (secp256r1) keypair generation/signing\n- K-256 (secp256k1) keypair generation/signing\n- Low-S signature normalization (required by ATP)\n- RFC 6979 deterministic signatures\n- did:key encoding/decoding\n- JWT creation and verification (using jsont)\n\n### atproto-multibase\n- Base32 encoding/decoding (ATP blessed format)\n- Base58btc encoding/decoding (for did:key)\n- Multibase prefix handling\n\n## Design Principles\n\n- **No regex**: All syntax validation uses hand-written parsers\n- **Codec-based**: Use jsont for JSON serialization\n- **Parser combinators optional**: Can use angstrom if needed, but prefer hand-written for simplicity\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- hacl-star or secp256k1 (K-256)\n- digestif (SHA-256)\n- jsont (JSON handling)\n- ptime (datetime)\n- **NO re or pcre**","acceptance_criteria":"- atproto-syntax package validates all identifier types\n- atproto-crypto package supports P-256 and K-256 with low-S normalization\n- atproto-multibase package supports base32 and base58btc\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:38.666246387+01:00","updated_at":"2025-12-28T11:57:30.662537723+01:00","closed_at":"2025-12-28T11:57:30.662537723+01:00","labels":["epic","foundation"],"dependencies":[{"issue_id":"atproto-10","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:13.213777505+01:00","created_by":"daemon","metadata":"{}"}]}
3
+
{"id":"atproto-11","title":"Implement atproto-syntax package","description":"Implement the atproto-syntax package providing parsers and validators for all AT Protocol identifier types.","design":"## Module Structure\n\n```ocaml\n(* atproto-syntax/lib/handle.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval normalize : t -\u003e t (* lowercase *)\n\n(* atproto-syntax/lib/did.ml *)\ntype method_ = Plc | Web | Key | Other of string\ntype t = { method_: method_; identifier: string }\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\n\n(* atproto-syntax/lib/nsid.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval authority : t -\u003e string\nval name : t -\u003e string\n\n(* atproto-syntax/lib/tid.ml *)\ntype t\nval generate : unit -\u003e t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval timestamp_us : t -\u003e int64\n\n(* atproto-syntax/lib/at_uri.ml *)\ntype t = { authority: [ `Did of Did.t | `Handle of Handle.t ]; \n collection: Nsid.t option;\n rkey: Record_key.t option }\n\n(* atproto-syntax/lib/datetime.ml *)\nval parse : string -\u003e (Ptime.t, error) result\nval format : Ptime.t -\u003e string\n```\n\n## Parser-based Validation (NO REGEX)\n\n### Handle Parser\n```ocaml\n(* Requirements from interop tests:\n - Max 253 chars total, max 63 chars per segment\n - At least 2 segments\n - Segments: alphanumeric + hyphens (not at start/end)\n - Case-insensitive, normalize to lowercase\n*)\nlet parse_handle s =\n if String.length s \u003e 253 then Error `Too_long\n else\n let labels = String.split_on_char '.' s in\n if List.length labels \u003c 2 then Error `Too_few_segments\n else if not (List.for_all valid_label labels) then Error `Invalid_label\n else if not (valid_tld (List.hd (List.rev labels))) then Error `Invalid_tld\n else Ok (normalize s)\n```\n\n### TID Parser (from Pegasus)\n```ocaml\nlet charset = \"234567abcdefghijklmnopqrstuvwxyz\"\nlet first_char_valid = \"234567abcdefghij\" (* High bit = 0 *)\n\nlet parse_tid s =\n if String.length s \u003c\u003e 13 then Error `Invalid_length\n else if not (String.contains first_char_valid s.[0]) then Error `High_bit_set\n else if not (String.for_all (fun c -\u003e String.contains charset c) s) then\n Error `Invalid_char\n else Ok s\n```\n\n### DateTime Parser (strict ISO 8601)\n```ocaml\n(* From interop tests - strict requirements:\n - Uppercase T and Z required\n - Timezone required (Z or +/-HH:MM)\n - 4-digit year, 2-digit month/day/hour/min/sec\n*)\nlet parse_datetime s =\n (* Hand-written parser, not regex *)\n let year = parse_4_digits s 0 in\n let month = parse_2_digits s 5 in\n let day = parse_2_digits s 8 in\n (* ... validate T separator at pos 10 ... *)\n let hour = parse_2_digits s 11 in\n (* ... continue ... *)\n```\n\n### Record Key Parser\n```ocaml\n(* From interop tests:\n - Max 512 chars\n - Allowed: alphanumeric + . - _ : ~\n - Cannot be \".\" or \"..\"\n*)\nlet valid_rkey_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = ':' || c = '~'\n\nlet parse_record_key s =\n if String.length s = 0 || String.length s \u003e 512 then Error `Invalid_length\n else if s = \".\" || s = \"..\" then Error `Reserved\n else if not (String.for_all valid_rkey_char s) then Error `Invalid_char\n else Ok s\n```\n\n## Dependencies\n- ptime (datetime handling)\n- mtime (high-res timestamps for TID generation)\n- NO regex libraries","acceptance_criteria":"- Handle regex validation per spec\n- DID validation for did:plc and did:web\n- NSID validation with 317 char limit\n- TID generation with microsecond precision\n- Record key validation for all types\n- AT-URI parsing and construction\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:36.014427755+01:00","updated_at":"2025-12-28T01:03:43.574485354+01:00","closed_at":"2025-12-28T01:03:43.574485354+01:00","labels":["foundation","syntax"],"dependencies":[{"issue_id":"atproto-11","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:06.385208896+01:00","created_by":"daemon","metadata":"{}"}]}
4
+
{"id":"atproto-12","title":"Implement atproto-multibase package","description":"Implement the atproto-multibase package providing base encoding utilities required by AT Protocol.","design":"## Module Structure\n\n```ocaml\n(* atproto-multibase/lib/base32.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base32_sortable.ml *)\n(* ATP uses sortable base32 for TIDs: 234567abcdefghijklmnopqrstuvwxyz *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base58btc.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/multibase.ml *)\ntype encoding = Base32 | Base58btc | ...\nval encode : encoding -\u003e bytes -\u003e string\nval decode : string -\u003e (bytes * encoding, error) result\n```\n\n## Multibase Prefixes\n- `b` = base32lower\n- `z` = base58btc\n\n## No external dependencies needed","acceptance_criteria":"- Base32 encoding per ATP spec (charset 234567abcdefghijklmnopqrstuvwxyz)\n- Base58btc encoding for did:key\n- Multibase prefix handling\n- Round-trip encoding/decoding works correctly","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:43.386843683+01:00","updated_at":"2025-12-28T00:45:51.37610055+01:00","closed_at":"2025-12-28T00:45:51.37610055+01:00","labels":["encoding","foundation"],"dependencies":[{"issue_id":"atproto-12","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:07.330194621+01:00","created_by":"daemon","metadata":"{}"}]}
5
+
{"id":"atproto-13","title":"Implement atproto-crypto package","description":"Implement the atproto-crypto package providing cryptographic operations for AT Protocol including P-256 and K-256 elliptic curve support.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/keypair.ml *)\nmodule type S = sig\n type public\n type private_\n type signature\n \n val generate : unit -\u003e private_\n val public : private_ -\u003e public\n val sign : private_ -\u003e bytes -\u003e signature\n val verify : public -\u003e bytes -\u003e signature -\u003e bool\n val public_to_bytes : public -\u003e bytes (* compressed *)\n val public_of_bytes : bytes -\u003e (public, error) result\n val signature_to_bytes : signature -\u003e bytes (* 64 bytes, r||s *)\n val signature_of_bytes : bytes -\u003e (signature, error) result\nend\n\n(* atproto-crypto/lib/p256.ml - uses mirage-crypto-ec *)\ninclude Keypair.S\n\n(* atproto-crypto/lib/k256.ml - uses secp256k1-ml *)\ninclude Keypair.S\n(* Note: secp256k1-ml automatically produces low-S signatures *)\n\n(* atproto-crypto/lib/did_key.ml *)\ntype t = P256 of P256.public | K256 of K256.public\nval encode : t -\u003e string (* \"did:key:z...\" *)\nval decode : string -\u003e (t, error) result\n```\n\n## Library Choices\n\n**P-256 (secp256r1)**: Use `mirage-crypto-ec`\n- `P256.Dsa.generate()` for keypairs\n- `P256.Dsa.sign` with RFC 6979\n- `P256.Dsa.pub_to_octets ~compress:true` for serialization\n\n**K-256 (secp256k1)**: Use `secp256k1-ml` (NOT hacl-star)\n- Automatic low-S normalization (libsecp256k1 always produces low-S)\n- RFC 6979 is default behavior\n- `Secp256k1.Key.to_bytes ~compress:true` for compressed keys\n\n## Multicodec Prefixes (for did:key)\n- P-256 public: `0x80 0x24` (multicodec 0x1200)\n- K-256 public: `0xE7 0x01` (multicodec 0xE7)\n\n## Critical: Low-S Normalization\n\nK-256: Handled automatically by secp256k1-ml\n\nP-256: May need manual check using zarith:\n```ocaml\nlet p256_n = Z.of_string\n \"0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551\"\n\nlet is_low_s s =\n let s_z = Z.of_bits (Bytes.to_string s) in\n Z.leq s_z Z.(p256_n / ~$2)\n```\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- secp256k1 (K-256 via secp256k1-ml)\n- digestif (SHA-256)\n- zarith (big integers for low-S check)\n- multibase (for did:key encoding)","acceptance_criteria":"- P-256 key generation and ECDSA signing\n- K-256 key generation and ECDSA signing\n- Low-S signature normalization (required!)\n- RFC 6979 deterministic signatures\n- did:key encoding and decoding\n- All crypto interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:54.960668746+01:00","updated_at":"2025-12-28T01:42:29.522627602+01:00","closed_at":"2025-12-28T01:42:29.522627602+01:00","labels":["crypto","foundation"],"dependencies":[{"issue_id":"atproto-13","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:08.277647286+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-13","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:08:10.535715566+01:00","created_by":"daemon","metadata":"{}"}]}
6
+
{"id":"atproto-14","title":"Implement JWT support in atproto-crypto","description":"Implement JWT support for AT Protocol authentication including inter-service and access tokens.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/jwt.ml *)\ntype header = { alg: [ `ES256 | `ES256K ]; typ: string }\ntype claims = { \n iss: string; (* DID *)\n aud: string; (* Service DID *)\n exp: int64; (* Expiration timestamp *)\n iat: int64; (* Issued at *)\n lxm: string option; (* Lexicon method *)\n (* ... other claims *)\n}\n\nval create : \n key:[ `P256 of P256.private_ | `K256 of K256.private_ ] -\u003e\n claims:claims -\u003e\n string\n\nval verify :\n key:[ `P256 of P256.public | `K256 of K256.public ] -\u003e\n string -\u003e\n (claims, error) result\n\nval decode_unverified : string -\u003e (header * claims, error) result\n```\n\n## Jsont Codecs for JWT\n\n```ocaml\nlet header_jsont : header Jsont.t =\n Jsont.obj \"jwt_header\" @@ fun o -\u003e\n let alg = Jsont.obj_mem o \"alg\" Jsont.string \n ~dec:(function \"ES256\" -\u003e `ES256 | \"ES256K\" -\u003e `ES256K | _ -\u003e failwith \"invalid alg\")\n ~enc:(function `ES256 -\u003e \"ES256\" | `ES256K -\u003e \"ES256K\") in\n let typ = Jsont.obj_mem o \"typ\" Jsont.string in\n Jsont.obj_finish o { alg; typ }\n\nlet claims_jsont : claims Jsont.t =\n Jsont.obj \"jwt_claims\" @@ fun o -\u003e\n let iss = Jsont.obj_mem o \"iss\" Jsont.string in\n let aud = Jsont.obj_mem o \"aud\" Jsont.string in\n let exp = Jsont.obj_mem o \"exp\" Jsont.int64 in\n let iat = Jsont.obj_mem o \"iat\" Jsont.int64 in\n let lxm = Jsont.obj_mem o \"lxm\" ~opt:true Jsont.string in\n Jsont.obj_finish o { iss; aud; exp; iat; lxm }\n```\n\n## JWT Types for ATP\n- Access token: `typ: \"at+jwt\"`\n- Refresh token: `typ: \"refresh+jwt\"`\n\n## Dependencies\n- atproto-multibase (base64url)\n- jsont","acceptance_criteria":"- JWT creation with ES256 and ES256K algorithms\n- JWT verification with signature validation\n- Token expiration checking\n- Required claims validation (iss, aud, exp, lxm)","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:08:03.209909326+01:00","updated_at":"2025-12-28T11:00:17.646363681+01:00","closed_at":"2025-12-28T11:00:17.646363681+01:00","labels":["auth","crypto"],"dependencies":[{"issue_id":"atproto-14","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:09.279825662+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-14","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:08:11.099737771+01:00","created_by":"daemon","metadata":"{}"}]}
7
7
{"id":"atproto-1ne","title":"Add missing lexicon record validation tests","description":"15 entries in record-data-invalid.json are currently skipped. These need to be implemented:\n\n**String format validation (12 tests):**\n- invalid string format handle\n- invalid string format did\n- invalid string format atidentifier\n- invalid string format nsid\n- invalid string format aturi\n- invalid string format cid\n- invalid string format datetime\n- invalid string format language\n- invalid string format uri\n- invalid string format tid\n- invalid string format recordkey\n- union inner invalid\n\n**Unknown field type validation (3 tests):**\n- unknown wrong type (bool)\n- unknown wrong type (bytes)\n- unknown wrong type (blob)\n\nThis requires implementing format validation in the Validator module.","acceptance_criteria":"- All 15 currently-skipped tests are enabled and passing\n- Format validation is implemented for all string formats\n- Unknown field type restrictions are enforced\n- 51/51 record-data-invalid.json entries are tested","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:32.793841929+01:00","updated_at":"2025-12-28T12:47:58.051715126+01:00","closed_at":"2025-12-28T12:47:58.051715126+01:00","labels":["conformance","lexicon","testing"]}
8
-
{"id":"atproto-20","title":"Data Layer - IPLD, MST, Repository","description":"Implement the data layer libraries that handle content-addressed data structures, repositories, and the Merkle Search Tree used by AT Protocol.","design":"## Packages\n\n### atproto-ipld\n- DAG-CBOR encoder/decoder (deterministic)\n- CID creation and parsing (CIDv1, SHA-256)\n- CAR file reading and writing\n- Blob type handling\n\n### atproto-mst\n- Merkle Search Tree implementation\n- Key depth calculation (SHA-256 leading zeros)\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n\n### atproto-repo\n- Repository structure (v3 format)\n- Commit object creation and signing\n- Record operations (create, update, delete)\n- Repository sync operations\n\n## Dependencies\n- atproto-crypto\n- atproto-ipld\n- digestif","acceptance_criteria":"- IPLD package handles DAG-CBOR and CIDs correctly\n- MST implementation matches spec exactly\n- Repository package supports commits and signing\n- All data-model and MST interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:46.199875469+01:00","updated_at":"2025-12-28T11:57:32.152844222+01:00","closed_at":"2025-12-28T11:57:32.152844222+01:00","labels":["data","epic"],"dependencies":[{"issue_id":"atproto-20","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:14.142103555+01:00","created_by":"daemon"}]}
9
-
{"id":"atproto-21","title":"Implement DAG-CBOR codec","description":"Implement DAG-CBOR encoder and decoder for AT Protocol's data model. DAG-CBOR is a deterministic subset of CBOR used for content-addressed data.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/dag_cbor.ml *)\ntype value =\n | Null\n | Bool of bool\n | Int of int64 (* Use int64 for JavaScript safe integer range *)\n | String of string\n | Bytes of bytes\n | Array of value list\n | Map of (string * value) list (* sorted by key *)\n | Link of Cid.t\n\nval encode : value -\u003e bytes\nval decode : bytes -\u003e (value, error) result\n\n(* JSON representation using jsont *)\nval jsont : value Jsont.t\n```\n\n## Implementation Strategy\n\nUse `cbor` opam library as base, add DAG-CBOR wrapper:\n\n1. **cbor library** handles: CBOR encoding/decoding, tag support\n2. **Our wrapper** adds:\n - Map key sorting (length first, then lexicographic)\n - Float rejection\n - Integer range validation (-2^53 to 2^53)\n - CID tag 42 handling\n\n## CRITICAL: Key Sorting Algorithm (from Pegasus)\n\n```ocaml\nlet compare_keys k1 k2 =\n let len1 = String.length k1 in\n let len2 = String.length k2 in\n if len1 = len2 then String.compare k1 k2\n else Int.compare len1 len2 (* Length first! *)\n\nlet sort_map_keys pairs =\n List.sort (fun (k1, _) (k2, _) -\u003e compare_keys k1 k2) pairs\n```\n\n## CID Tag 42 Encoding\n\n```ocaml\nlet encode_cid cid =\n let cid_bytes = Cid.to_bytes cid in (* Includes \\x00 multibase prefix *)\n `Tag (42, `Bytes cid_bytes)\n```\n\n## Integer Range Check (JavaScript Safety)\n\n```ocaml\nlet js_safe_min = -9007199254740991L (* -(2^53 - 1) *)\nlet js_safe_max = 9007199254740991L (* 2^53 - 1 *)\n\nlet validate_integer i =\n if i \u003c js_safe_min || i \u003e js_safe_max then\n Error `Integer_out_of_range\n else Ok i\n```\n\n## Special JSON Representations\n\n```ocaml\n(* $link for CID *)\nlet cid_link_jsont =\n Jsont.Object.map ~kind:\"cid-link\" (fun link -\u003e Link (Cid.of_string link))\n |\u003e Jsont.Object.mem \"$link\" Jsont.string ~enc:Cid.to_string\n |\u003e Jsont.Object.finish\n\n(* $bytes for raw bytes *)\nlet bytes_jsont =\n Jsont.Object.map ~kind:\"bytes\" (fun b64 -\u003e Bytes (Base64.decode b64))\n |\u003e Jsont.Object.mem \"$bytes\" Jsont.string ~enc:Base64.encode\n |\u003e Jsont.Object.finish\n```\n\n## Dependencies\n- cbor \u003e= 0.5 (base CBOR codec)\n- jsont (JSON handling)\n- digestif (for CID hashing)","acceptance_criteria":"- DAG-CBOR encoding is deterministic (sorted keys, specific types)\n- No floats allowed in data model\n- JSONโCBOR conversion works correctly\n- All data-model interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:24.992900973+01:00","updated_at":"2025-12-28T02:05:09.703411875+01:00","closed_at":"2025-12-28T02:05:09.703411875+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-21","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:18.587980423+01:00","created_by":"daemon"},{"issue_id":"atproto-21","depends_on_id":"atproto-22","type":"blocks","created_at":"2025-12-28T00:09:25.230617121+01:00","created_by":"daemon"}]}
10
-
{"id":"atproto-22","title":"Implement CID (Content Identifier)","description":"Implement Content Identifier (CID) support for AT Protocol. CIDs are self-describing content-addressed identifiers.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/cid.ml *)\ntype codec = DagCbor | Raw\ntype t\n\n(* Creation *)\nval create : codec:codec -\u003e bytes -\u003e t\nval of_dag_cbor : bytes -\u003e t (* convenience *)\nval of_raw : bytes -\u003e t (* for blobs *)\n\n(* Parsing *)\nval of_string : string -\u003e (t, error) result\nval of_bytes : bytes -\u003e (t, error) result\n\n(* Serialization *)\nval to_string : t -\u003e string (* base32 encoded *)\nval to_bytes : t -\u003e bytes (* binary form for tag 42 *)\n\n(* Accessors *)\nval codec : t -\u003e codec\nval hash : t -\u003e bytes (* raw SHA-256 hash *)\nval equal : t -\u003e t -\u003e bool\nval compare : t -\u003e t -\u003e int\n```\n\n## ATP Blessed CID Format\n\n- Version: CIDv1 only\n- Hash: SHA-256 (multicodec 0x12), 256 bits\n- Codec: dag-cbor (0x71) for data, raw (0x55) for blobs\n- String encoding: base32 (multibase prefix 'b')\n\n## CID Binary Structure\n```\n\u003cversion=1\u003e \u003ccodec-varint\u003e \u003chash-multicodec\u003e \u003chash-length\u003e \u003chash-bytes\u003e\n```\n\n## Dependencies\n- digestif (SHA-256)\n- atproto-multibase","acceptance_criteria":"- CIDv1 creation with SHA-256 and dag-cbor multicodec\n- CID string parsing and validation\n- Binary CID encoding for CBOR tag 42\n- All CID interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:35.195261117+01:00","updated_at":"2025-12-28T01:55:58.641459339+01:00","closed_at":"2025-12-28T01:55:58.641459339+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-22","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:19.549103067+01:00","created_by":"daemon"},{"issue_id":"atproto-22","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:09:24.279353993+01:00","created_by":"daemon"}]}
11
-
{"id":"atproto-23","title":"Implement CAR file format","description":"Implement CAR (Content Addressable aRchive) file format support for AT Protocol. CAR files are used for repository export and sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/car.ml *)\ntype header = { version: int; roots: Cid.t list }\ntype block = { cid: Cid.t; data: bytes }\n\n(* Reading *)\nval read_header : bytes -\u003e (header * int, error) result\nval read_blocks : bytes -\u003e offset:int -\u003e block Seq.t\n\n(* Writing *)\nval write : roots:Cid.t list -\u003e blocks:block list -\u003e bytes\n\n(* Streaming API using effects *)\ntype _ Effect.t +=\n | Read_bytes : int -\u003e bytes Effect.t\n \nval stream_blocks : unit -\u003e block option (* uses Read_bytes effect *)\n```\n\n## CAR v1 Format\n\n```\n\u003cheader-length-varint\u003e \u003cdag-cbor-header\u003e\n\u003cblock-1-length-varint\u003e \u003ccid-1\u003e \u003cdata-1\u003e\n\u003cblock-2-length-varint\u003e \u003ccid-2\u003e \u003cdata-2\u003e\n...\n```\n\n## Header Structure\n```cbor\n{ \"version\": 1, \"roots\": [\u003ccid\u003e, ...] }\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)","acceptance_criteria":"- CAR v1 reading and writing\n- Streaming block iteration\n- Proper varint encoding\n- Root CID validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:43.573326253+01:00","updated_at":"2025-12-28T02:08:35.37815686+01:00","closed_at":"2025-12-28T02:08:35.37815686+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-23","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:20.490759113+01:00","created_by":"daemon"},{"issue_id":"atproto-23","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:26.481546763+01:00","created_by":"daemon"}]}
12
-
{"id":"atproto-24","title":"Implement Merkle Search Tree (MST)","description":"Implement Merkle Search Tree (MST) for AT Protocol repositories. The MST provides a content-addressed, verifiable key-value store.","design":"## Module Structure\n\n```ocaml\n(* atproto-mst/lib/mst.ml *)\nmodule type Blockstore = sig\n type t\n val get : t -\u003e Cid.t -\u003e bytes option\n val put : t -\u003e Cid.t -\u003e bytes -\u003e unit\nend\n\nmodule Make (Store : Blockstore) : sig\n type t\n \n val empty : Store.t -\u003e t\n val of_root : Store.t -\u003e Cid.t -\u003e t\n \n val get : t -\u003e string -\u003e Cid.t option\n val add : t -\u003e string -\u003e Cid.t -\u003e t\n val delete : t -\u003e string -\u003e t\n \n val root : t -\u003e Cid.t\n val entries : t -\u003e (string * Cid.t) Seq.t\n \n val diff : old:t -\u003e new_:t -\u003e diff list\nend\n```\n\n## CRITICAL: Key Height Calculation (from Pegasus)\n\nATProto uses **2-bit chunks** (fanout = 4), NOT single bits:\n\n```ocaml\nlet leading_zeros_on_hash key =\n let digest = Digestif.SHA256.(digest_string key |\u003e to_raw_string) in\n let rec loop idx zeros =\n if idx \u003e= String.length digest then zeros\n else\n let byte = Char.code digest.[idx] in\n let zeros' = zeros +\n if byte = 0 then 4 (* Full byte = 4 two-bit zeros *)\n else if byte \u003c 4 then 3 (* 0b000000xx *)\n else if byte \u003c 16 then 2 (* 0b0000xxxx *)\n else if byte \u003c 64 then 1 (* 0b00xxxxxx *)\n else 0 (* 0bxxxxxxxx *)\n in\n if byte = 0 then loop (idx + 1) zeros' else zeros'\n in\n loop 0 0\n```\n\n## Raw Node Structure (for CBOR)\n\n```ocaml\ntype node_raw = {\n l: Cid.t option; (* Left subtree *)\n e: entry_raw list (* Entries at this level *)\n}\n\ntype entry_raw = {\n p: int; (* Prefix length shared with previous key *)\n k: bytes; (* Key suffix (after shared prefix) *)\n v: Cid.t; (* Value CID *)\n t: Cid.t option (* Right subtree *)\n}\n```\n\n## Hydrated Node (for traversal)\n\n```ocaml\ntype node = {\n layer: int;\n mutable left: node option Lazy.t;\n mutable entries: entry list\n}\n\ntype entry = {\n layer: int;\n key: string; (* Full key, decompressed *)\n value: Cid.t;\n right: node option Lazy.t\n}\n```\n\n## Key Validation\n\n```ocaml\nlet is_valid_mst_key key =\n match String.split_on_char '/' key with\n | [collection; rkey] -\u003e\n String.length key \u003c= 1024 \u0026\u0026\n collection \u003c\u003e \"\" \u0026\u0026 rkey \u003c\u003e \"\" \u0026\u0026\n String.for_all is_valid_char collection \u0026\u0026\n String.for_all is_valid_char rkey\n | _ -\u003e false\n\nlet is_valid_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = '~'\n```\n\n## Building from Sorted Leaves\n\n```ocaml\nlet of_assoc store assoc =\n let sorted = List.sort (fun (k1, _) (k2, _) -\u003e String.compare k1 k2) assoc in\n let with_layers = List.map (fun (k, v) -\u003e\n (k, v, leading_zeros_on_hash k)) sorted in\n (* Group by layer, build tree bottom-up *)\n ...\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)\n- digestif (SHA-256 for key hashing)","acceptance_criteria":"- Correct key depth calculation (SHA-256 leading zeros / 2)\n- Deterministic tree structure from key/value pairs\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n- All MST interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:56.250995314+01:00","updated_at":"2025-12-28T02:13:22.864318902+01:00","closed_at":"2025-12-28T02:13:22.864318902+01:00","labels":["data","mst"],"dependencies":[{"issue_id":"atproto-24","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:21.767912975+01:00","created_by":"daemon"},{"issue_id":"atproto-24","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:27.06774891+01:00","created_by":"daemon"}]}
8
+
{"id":"atproto-20","title":"Data Layer - IPLD, MST, Repository","description":"Implement the data layer libraries that handle content-addressed data structures, repositories, and the Merkle Search Tree used by AT Protocol.","design":"## Packages\n\n### atproto-ipld\n- DAG-CBOR encoder/decoder (deterministic)\n- CID creation and parsing (CIDv1, SHA-256)\n- CAR file reading and writing\n- Blob type handling\n\n### atproto-mst\n- Merkle Search Tree implementation\n- Key depth calculation (SHA-256 leading zeros)\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n\n### atproto-repo\n- Repository structure (v3 format)\n- Commit object creation and signing\n- Record operations (create, update, delete)\n- Repository sync operations\n\n## Dependencies\n- atproto-crypto\n- atproto-ipld\n- digestif","acceptance_criteria":"- IPLD package handles DAG-CBOR and CIDs correctly\n- MST implementation matches spec exactly\n- Repository package supports commits and signing\n- All data-model and MST interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:46.199875469+01:00","updated_at":"2025-12-28T11:57:32.152844222+01:00","closed_at":"2025-12-28T11:57:32.152844222+01:00","labels":["data","epic"],"dependencies":[{"issue_id":"atproto-20","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:14.142103555+01:00","created_by":"daemon","metadata":"{}"}]}
9
+
{"id":"atproto-21","title":"Implement DAG-CBOR codec","description":"Implement DAG-CBOR encoder and decoder for AT Protocol's data model. DAG-CBOR is a deterministic subset of CBOR used for content-addressed data.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/dag_cbor.ml *)\ntype value =\n | Null\n | Bool of bool\n | Int of int64 (* Use int64 for JavaScript safe integer range *)\n | String of string\n | Bytes of bytes\n | Array of value list\n | Map of (string * value) list (* sorted by key *)\n | Link of Cid.t\n\nval encode : value -\u003e bytes\nval decode : bytes -\u003e (value, error) result\n\n(* JSON representation using jsont *)\nval jsont : value Jsont.t\n```\n\n## Implementation Strategy\n\nUse `cbor` opam library as base, add DAG-CBOR wrapper:\n\n1. **cbor library** handles: CBOR encoding/decoding, tag support\n2. **Our wrapper** adds:\n - Map key sorting (length first, then lexicographic)\n - Float rejection\n - Integer range validation (-2^53 to 2^53)\n - CID tag 42 handling\n\n## CRITICAL: Key Sorting Algorithm (from Pegasus)\n\n```ocaml\nlet compare_keys k1 k2 =\n let len1 = String.length k1 in\n let len2 = String.length k2 in\n if len1 = len2 then String.compare k1 k2\n else Int.compare len1 len2 (* Length first! *)\n\nlet sort_map_keys pairs =\n List.sort (fun (k1, _) (k2, _) -\u003e compare_keys k1 k2) pairs\n```\n\n## CID Tag 42 Encoding\n\n```ocaml\nlet encode_cid cid =\n let cid_bytes = Cid.to_bytes cid in (* Includes \\x00 multibase prefix *)\n `Tag (42, `Bytes cid_bytes)\n```\n\n## Integer Range Check (JavaScript Safety)\n\n```ocaml\nlet js_safe_min = -9007199254740991L (* -(2^53 - 1) *)\nlet js_safe_max = 9007199254740991L (* 2^53 - 1 *)\n\nlet validate_integer i =\n if i \u003c js_safe_min || i \u003e js_safe_max then\n Error `Integer_out_of_range\n else Ok i\n```\n\n## Special JSON Representations\n\n```ocaml\n(* $link for CID *)\nlet cid_link_jsont =\n Jsont.Object.map ~kind:\"cid-link\" (fun link -\u003e Link (Cid.of_string link))\n |\u003e Jsont.Object.mem \"$link\" Jsont.string ~enc:Cid.to_string\n |\u003e Jsont.Object.finish\n\n(* $bytes for raw bytes *)\nlet bytes_jsont =\n Jsont.Object.map ~kind:\"bytes\" (fun b64 -\u003e Bytes (Base64.decode b64))\n |\u003e Jsont.Object.mem \"$bytes\" Jsont.string ~enc:Base64.encode\n |\u003e Jsont.Object.finish\n```\n\n## Dependencies\n- cbor \u003e= 0.5 (base CBOR codec)\n- jsont (JSON handling)\n- digestif (for CID hashing)","acceptance_criteria":"- DAG-CBOR encoding is deterministic (sorted keys, specific types)\n- No floats allowed in data model\n- JSONโCBOR conversion works correctly\n- All data-model interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:24.992900973+01:00","updated_at":"2025-12-28T02:05:09.703411875+01:00","closed_at":"2025-12-28T02:05:09.703411875+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-21","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:18.587980423+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-21","depends_on_id":"atproto-22","type":"blocks","created_at":"2025-12-28T00:09:25.230617121+01:00","created_by":"daemon","metadata":"{}"}]}
10
+
{"id":"atproto-22","title":"Implement CID (Content Identifier)","description":"Implement Content Identifier (CID) support for AT Protocol. CIDs are self-describing content-addressed identifiers.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/cid.ml *)\ntype codec = DagCbor | Raw\ntype t\n\n(* Creation *)\nval create : codec:codec -\u003e bytes -\u003e t\nval of_dag_cbor : bytes -\u003e t (* convenience *)\nval of_raw : bytes -\u003e t (* for blobs *)\n\n(* Parsing *)\nval of_string : string -\u003e (t, error) result\nval of_bytes : bytes -\u003e (t, error) result\n\n(* Serialization *)\nval to_string : t -\u003e string (* base32 encoded *)\nval to_bytes : t -\u003e bytes (* binary form for tag 42 *)\n\n(* Accessors *)\nval codec : t -\u003e codec\nval hash : t -\u003e bytes (* raw SHA-256 hash *)\nval equal : t -\u003e t -\u003e bool\nval compare : t -\u003e t -\u003e int\n```\n\n## ATP Blessed CID Format\n\n- Version: CIDv1 only\n- Hash: SHA-256 (multicodec 0x12), 256 bits\n- Codec: dag-cbor (0x71) for data, raw (0x55) for blobs\n- String encoding: base32 (multibase prefix 'b')\n\n## CID Binary Structure\n```\n\u003cversion=1\u003e \u003ccodec-varint\u003e \u003chash-multicodec\u003e \u003chash-length\u003e \u003chash-bytes\u003e\n```\n\n## Dependencies\n- digestif (SHA-256)\n- atproto-multibase","acceptance_criteria":"- CIDv1 creation with SHA-256 and dag-cbor multicodec\n- CID string parsing and validation\n- Binary CID encoding for CBOR tag 42\n- All CID interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:35.195261117+01:00","updated_at":"2025-12-28T01:55:58.641459339+01:00","closed_at":"2025-12-28T01:55:58.641459339+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-22","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:19.549103067+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-22","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:09:24.279353993+01:00","created_by":"daemon","metadata":"{}"}]}
11
+
{"id":"atproto-23","title":"Implement CAR file format","description":"Implement CAR (Content Addressable aRchive) file format support for AT Protocol. CAR files are used for repository export and sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/car.ml *)\ntype header = { version: int; roots: Cid.t list }\ntype block = { cid: Cid.t; data: bytes }\n\n(* Reading *)\nval read_header : bytes -\u003e (header * int, error) result\nval read_blocks : bytes -\u003e offset:int -\u003e block Seq.t\n\n(* Writing *)\nval write : roots:Cid.t list -\u003e blocks:block list -\u003e bytes\n\n(* Streaming API using effects *)\ntype _ Effect.t +=\n | Read_bytes : int -\u003e bytes Effect.t\n \nval stream_blocks : unit -\u003e block option (* uses Read_bytes effect *)\n```\n\n## CAR v1 Format\n\n```\n\u003cheader-length-varint\u003e \u003cdag-cbor-header\u003e\n\u003cblock-1-length-varint\u003e \u003ccid-1\u003e \u003cdata-1\u003e\n\u003cblock-2-length-varint\u003e \u003ccid-2\u003e \u003cdata-2\u003e\n...\n```\n\n## Header Structure\n```cbor\n{ \"version\": 1, \"roots\": [\u003ccid\u003e, ...] }\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)","acceptance_criteria":"- CAR v1 reading and writing\n- Streaming block iteration\n- Proper varint encoding\n- Root CID validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:43.573326253+01:00","updated_at":"2025-12-28T02:08:35.37815686+01:00","closed_at":"2025-12-28T02:08:35.37815686+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-23","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:20.490759113+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-23","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:26.481546763+01:00","created_by":"daemon","metadata":"{}"}]}
12
+
{"id":"atproto-24","title":"Implement Merkle Search Tree (MST)","description":"Implement Merkle Search Tree (MST) for AT Protocol repositories. The MST provides a content-addressed, verifiable key-value store.","design":"## Module Structure\n\n```ocaml\n(* atproto-mst/lib/mst.ml *)\nmodule type Blockstore = sig\n type t\n val get : t -\u003e Cid.t -\u003e bytes option\n val put : t -\u003e Cid.t -\u003e bytes -\u003e unit\nend\n\nmodule Make (Store : Blockstore) : sig\n type t\n \n val empty : Store.t -\u003e t\n val of_root : Store.t -\u003e Cid.t -\u003e t\n \n val get : t -\u003e string -\u003e Cid.t option\n val add : t -\u003e string -\u003e Cid.t -\u003e t\n val delete : t -\u003e string -\u003e t\n \n val root : t -\u003e Cid.t\n val entries : t -\u003e (string * Cid.t) Seq.t\n \n val diff : old:t -\u003e new_:t -\u003e diff list\nend\n```\n\n## CRITICAL: Key Height Calculation (from Pegasus)\n\nATProto uses **2-bit chunks** (fanout = 4), NOT single bits:\n\n```ocaml\nlet leading_zeros_on_hash key =\n let digest = Digestif.SHA256.(digest_string key |\u003e to_raw_string) in\n let rec loop idx zeros =\n if idx \u003e= String.length digest then zeros\n else\n let byte = Char.code digest.[idx] in\n let zeros' = zeros +\n if byte = 0 then 4 (* Full byte = 4 two-bit zeros *)\n else if byte \u003c 4 then 3 (* 0b000000xx *)\n else if byte \u003c 16 then 2 (* 0b0000xxxx *)\n else if byte \u003c 64 then 1 (* 0b00xxxxxx *)\n else 0 (* 0bxxxxxxxx *)\n in\n if byte = 0 then loop (idx + 1) zeros' else zeros'\n in\n loop 0 0\n```\n\n## Raw Node Structure (for CBOR)\n\n```ocaml\ntype node_raw = {\n l: Cid.t option; (* Left subtree *)\n e: entry_raw list (* Entries at this level *)\n}\n\ntype entry_raw = {\n p: int; (* Prefix length shared with previous key *)\n k: bytes; (* Key suffix (after shared prefix) *)\n v: Cid.t; (* Value CID *)\n t: Cid.t option (* Right subtree *)\n}\n```\n\n## Hydrated Node (for traversal)\n\n```ocaml\ntype node = {\n layer: int;\n mutable left: node option Lazy.t;\n mutable entries: entry list\n}\n\ntype entry = {\n layer: int;\n key: string; (* Full key, decompressed *)\n value: Cid.t;\n right: node option Lazy.t\n}\n```\n\n## Key Validation\n\n```ocaml\nlet is_valid_mst_key key =\n match String.split_on_char '/' key with\n | [collection; rkey] -\u003e\n String.length key \u003c= 1024 \u0026\u0026\n collection \u003c\u003e \"\" \u0026\u0026 rkey \u003c\u003e \"\" \u0026\u0026\n String.for_all is_valid_char collection \u0026\u0026\n String.for_all is_valid_char rkey\n | _ -\u003e false\n\nlet is_valid_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = '~'\n```\n\n## Building from Sorted Leaves\n\n```ocaml\nlet of_assoc store assoc =\n let sorted = List.sort (fun (k1, _) (k2, _) -\u003e String.compare k1 k2) assoc in\n let with_layers = List.map (fun (k, v) -\u003e\n (k, v, leading_zeros_on_hash k)) sorted in\n (* Group by layer, build tree bottom-up *)\n ...\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)\n- digestif (SHA-256 for key hashing)","acceptance_criteria":"- Correct key depth calculation (SHA-256 leading zeros / 2)\n- Deterministic tree structure from key/value pairs\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n- All MST interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:56.250995314+01:00","updated_at":"2025-12-28T02:13:22.864318902+01:00","closed_at":"2025-12-28T02:13:22.864318902+01:00","labels":["data","mst"],"dependencies":[{"issue_id":"atproto-24","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:21.767912975+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-24","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:27.06774891+01:00","created_by":"daemon","metadata":"{}"}]}
13
13
{"id":"atproto-24w","title":"Add missing syntax conformance tests","description":"Add tests for syntax fixtures that are not currently covered:\n\n1. **AT Identifier** - atidentifier_syntax_valid.txt, atidentifier_syntax_invalid.txt\n - Requires implementing an At_identifier module or testing DID/Handle as union\n\n2. **CID syntax** - cid_syntax_valid.txt, cid_syntax_invalid.txt\n - CID module exists in atproto-ipld, need to add syntax tests\n\n3. **URI syntax** - uri_syntax_valid.txt, uri_syntax_invalid.txt\n - Generic URI validation (distinct from AT-URI)\n\n4. **Language tags** - language_syntax_valid.txt, language_syntax_invalid.txt\n - BCP-47 language tag validation","design":"## Implementation Plan\n\n### 1. AT Identifier (DID or Handle union)\n- AT Identifier is either a valid DID or a valid Handle\n- Add `At_identifier` module to atproto-syntax or test inline\n- Test: try DID first, then Handle - if both fail, invalid\n\n### 2. CID Syntax \n- CID module already exists in atproto-ipld\n- Add CID syntax tests to test_syntax.ml using Cid.of_string\n- Need to add atproto_ipld dependency to test\n\n### 3. URI Syntax\n- Generic RFC-3986 URI validation\n- Can use Uri library's parsing or add simple validator\n- Test: Uri.of_string should succeed for valid, parsing should catch invalid\n\n### 4. Language Tags (BCP-47)\n- Need to implement Language module in atproto-syntax\n- BCP-47 format: language[-script][-region][-variant][-extension][-privateuse]\n- Examples: \"en\", \"en-US\", \"zh-Hant\", \"i-navajo\"\n\n## Files to Modify\n- `lib/syntax/atproto_syntax.ml` - expose new modules\n- `lib/syntax/dune` - if new files needed\n- `test/syntax/test_syntax.ml` - add 8 new test functions\n- `test/syntax/dune` - add atproto_ipld dependency for CID tests\n\n## Order of Implementation\n1. AT Identifier tests (uses existing DID/Handle)\n2. CID tests (uses existing Cid module from ipld)\n3. Language module + tests (new implementation)\n4. URI tests (use Uri library)","acceptance_criteria":"- All 4 fixture pairs have corresponding tests\n- Tests load ALL entries from each fixture file\n- Valid entries pass parsing\n- Invalid entries fail parsing with appropriate errors","notes":"Completed all missing syntax conformance tests:\n- AT Identifier tests (valid/invalid from fixtures)\n- CID tests (valid/invalid from fixtures)\n- Language tag tests (BCP-47 validation in lib/syntax/language.ml)\n- URI tests (RFC-3986 validation with strict checks for scheme, whitespace, invalid chars, max length)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T12:12:11.492860987+01:00","updated_at":"2025-12-28T12:40:47.252759691+01:00","closed_at":"2025-12-28T12:40:47.252759691+01:00","labels":["conformance","syntax","testing"]}
14
-
{"id":"atproto-25","title":"Implement Repository and Commit","description":"Implement repository support for AT Protocol. A repository is a signed, content-addressed collection of records organized by the MST.","design":"## Module Structure\n\n```ocaml\n(* atproto-repo/lib/commit.ml *)\ntype t = {\n did: Did.t;\n version: int; (* always 3 *)\n data: Cid.t; (* MST root *)\n rev: Tid.t;\n prev: Cid.t option;\n sig_: bytes;\n}\n\nval create : \n did:Did.t -\u003e \n data:Cid.t -\u003e \n rev:Tid.t -\u003e \n ?prev:Cid.t -\u003e \n key:K256.private_ -\u003e \n t\n\nval verify : t -\u003e public_key:K256.public -\u003e bool\nval to_dag_cbor : t -\u003e bytes\nval of_dag_cbor : bytes -\u003e (t, error) result\n\n(* atproto-repo/lib/repo.ml *)\ntype t\n\nval create : blockstore:Blockstore.t -\u003e did:Did.t -\u003e t\nval load : blockstore:Blockstore.t -\u003e root:Cid.t -\u003e t\n\nval get_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value option\nval create_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval update_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval delete_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e t\n\nval commit : t -\u003e key:K256.private_ -\u003e Commit.t\n```\n\n## Commit Signing Process\n\n1. Create unsigned commit (all fields except sig)\n2. Encode as DAG-CBOR\n3. SHA-256 hash the bytes\n4. Sign hash with account key (low-S!)\n5. Add signature as raw bytes\n\n## Dependencies\n- atproto-mst\n- atproto-crypto\n- atproto-syntax","acceptance_criteria":"- Commit object creation with proper v3 format\n- Commit signing with account key\n- Commit verification\n- Repository operations (create, update, delete records)","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:07.716307822+01:00","updated_at":"2025-12-28T02:25:00.961982054+01:00","closed_at":"2025-12-28T02:25:00.961982054+01:00","labels":["data","repo"],"dependencies":[{"issue_id":"atproto-25","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:22.387797246+01:00","created_by":"daemon"},{"issue_id":"atproto-25","depends_on_id":"atproto-24","type":"blocks","created_at":"2025-12-28T00:09:27.958219661+01:00","created_by":"daemon"},{"issue_id":"atproto-25","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:09:28.920614309+01:00","created_by":"daemon"}]}
15
-
{"id":"atproto-26","title":"Implement Blob handling","description":"Implement blob handling for AT Protocol. Blobs are binary data (images, videos) referenced by CID in records.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/blob.ml *)\ntype ref_ = {\n cid: Cid.t;\n mime_type: string;\n size: int;\n}\n\nval create : data:bytes -\u003e mime_type:string -\u003e ref_\nval to_dag_cbor : ref_ -\u003e Dag_cbor.value\nval of_dag_cbor : Dag_cbor.value -\u003e (ref_, error) result\n\n(* JSON representation *)\n(* { \"$type\": \"blob\", \"ref\": {\"$link\": \"...\"}, \"mimeType\": \"...\", \"size\": ... } *)\n```\n\n## Blob CID Requirements\n\n- Multicodec: `raw` (0x55), NOT dag-cbor\n- Hash: SHA-256 of raw bytes\n\n## Typed vs Untyped Blobs\n\nLegacy (untyped): just a CID link\nModern (typed): full blob object with $type\n\n## Dependencies\n- atproto-ipld","acceptance_criteria":"- Blob type encoding/decoding\n- Blob reference creation and validation\n- MIME type handling\n- Size constraints enforcement","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:14.976884267+01:00","updated_at":"2025-12-28T11:03:27.015943079+01:00","closed_at":"2025-12-28T11:03:27.015943079+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-26","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:23.336547933+01:00","created_by":"daemon"}]}
16
-
{"id":"atproto-30","title":"Identity Layer - DID and Handle Resolution","description":"Implement the identity layer libraries that handle DID resolution, handle resolution, and identity verification for the AT Protocol.","design":"## Packages\n\n### atproto-identity\n- DID resolution (did:plc, did:web)\n- Handle resolution (DNS TXT, HTTPS)\n- DID document parsing\n- Identity caching\n- Bidirectional verification (DIDโHandle)\n\n## Resolution Flow\n\n1. Handle โ DID: DNS TXT `_atproto.\u003chandle\u003e` or HTTPS `/.well-known/atproto-did`\n2. DID โ DID Document: Fetch from PLC directory or .well-known\n3. Extract: Signing key, PDS endpoint, handle\n\n## Effects-based Design\n\n```ocaml\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e string Effect.t\n | Dns_txt : string -\u003e string list Effect.t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto\n- jsont or yojson","acceptance_criteria":"- DID resolution works for did:plc and did:web\n- Handle resolution via DNS TXT and HTTPS works\n- DID document parsing is complete\n- Identity verification works end-to-end","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:54.380506112+01:00","updated_at":"2025-12-28T11:57:33.145244873+01:00","closed_at":"2025-12-28T11:57:33.145244873+01:00","labels":["epic","identity"],"dependencies":[{"issue_id":"atproto-30","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:15.083956697+01:00","created_by":"daemon"}]}
17
-
{"id":"atproto-31","title":"Implement DID resolution","description":"Implement DID resolution for AT Protocol supporting did:plc and did:web methods.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/did_resolver.ml *)\ntype did_document = {\n id: Did.t;\n also_known_as: string list; (* handles *)\n verification_method: verification_method list;\n service: service list;\n}\n\nand verification_method = {\n id: string;\n type_: string;\n controller: Did.t;\n public_key_multibase: string;\n}\n\nand service = {\n id: string;\n type_: string;\n service_endpoint: Uri.t;\n}\n\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Did.t -\u003e (did_document, error) result\nval get_signing_key : did_document -\u003e (Did_key.t, error) result\nval get_pds_endpoint : did_document -\u003e (Uri.t, error) result\nval get_handle : did_document -\u003e Handle.t option\n```\n\n## Jsont Codecs for DID Documents\n\n```ocaml\nlet verification_method_jsont : verification_method Jsont.t =\n Jsont.obj \"verification_method\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" Jsont.string in\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n let controller = Jsont.obj_mem o \"controller\" did_jsont in\n let public_key_multibase = Jsont.obj_mem o \"publicKeyMultibase\" Jsont.string in\n Jsont.obj_finish o { id; type_; controller; public_key_multibase }\n\nlet did_document_jsont : did_document Jsont.t =\n Jsont.obj \"did_document\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" did_jsont in\n let also_known_as = Jsont.obj_mem o \"alsoKnownAs\" ~opt:true \n (Jsont.list Jsont.string) ~default:[] in\n let verification_method = Jsont.obj_mem o \"verificationMethod\" \n (Jsont.list verification_method_jsont) in\n let service = Jsont.obj_mem o \"service\" ~opt:true \n (Jsont.list service_jsont) ~default:[] in\n Jsont.obj_finish o { id; also_known_as; verification_method; service }\n```\n\n## Resolution Endpoints\n\n- did:plc โ `https://plc.directory/\u003cdid\u003e`\n- did:web โ `https://\u003cdomain\u003e/.well-known/did.json`\n\n## Effects-based Design\n\nResolution uses effects for HTTP, allowing different runtimes:\n- eio handler for testing\n- cohttp handler for production\n- mock handler for unit tests\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto (for did:key parsing)\n- jsont","acceptance_criteria":"- did:plc resolution from PLC directory\n- did:web resolution from .well-known\n- DID document parsing\n- Caching with configurable TTL","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:42.738403632+01:00","updated_at":"2025-12-28T10:36:57.60764779+01:00","closed_at":"2025-12-28T10:36:57.60764779+01:00","labels":["did","identity"],"dependencies":[{"issue_id":"atproto-31","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.183867539+01:00","created_by":"daemon"},{"issue_id":"atproto-31","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:04.901673996+01:00","created_by":"daemon"},{"issue_id":"atproto-31","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:10:05.785020408+01:00","created_by":"daemon"}]}
18
-
{"id":"atproto-32","title":"Implement Handle resolution","description":"Implement handle resolution for AT Protocol. Handles are domain-based identifiers that resolve to DIDs.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/handle_resolver.ml *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Handle.t -\u003e (Did.t, error) result\n```\n\n## Resolution Algorithm\n\n1. Query DNS TXT record at `_atproto.\u003chandle\u003e`\n2. Look for record with `did=\u003cdid\u003e` value\n3. If no DNS record, try HTTPS: `https://\u003chandle\u003e/.well-known/atproto-did`\n4. Response should be plain text DID\n\n## Example\n\nHandle: `alice.bsky.social`\n1. DNS: `_atproto.alice.bsky.social` TXT โ `did=did:plc:abc123`\n2. Or HTTPS: `https://alice.bsky.social/.well-known/atproto-did` โ `did:plc:abc123`\n\n## Dependencies\n- atproto-syntax","acceptance_criteria":"- DNS TXT record resolution (_atproto.\u003chandle\u003e)\n- HTTPS fallback (/.well-known/atproto-did)\n- Handle normalization (lowercase)\n- Proper error handling for resolution failures","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:50.77787892+01:00","updated_at":"2025-12-28T10:45:02.168086436+01:00","closed_at":"2025-12-28T10:45:02.168086436+01:00","labels":["handle","identity"],"dependencies":[{"issue_id":"atproto-32","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.809033959+01:00","created_by":"daemon"},{"issue_id":"atproto-32","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:06.598127952+01:00","created_by":"daemon"}]}
19
-
{"id":"atproto-33","title":"Implement identity verification","description":"Implement bidirectional identity verification ensuring DIDs and handles are properly linked.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/identity.ml *)\ntype verified_identity = {\n did: Did.t;\n handle: Handle.t;\n signing_key: Did_key.t;\n pds_endpoint: Uri.t;\n}\n\ntype verification_error =\n | Did_resolution_failed of error\n | Handle_resolution_failed of error\n | Handle_mismatch of { expected: Handle.t; found: Handle.t option }\n | Did_mismatch of { expected: Did.t; found: Did.t }\n\nval verify_did : Did.t -\u003e (verified_identity, verification_error) result\nval verify_handle : Handle.t -\u003e (verified_identity, verification_error) result\nval verify_bidirectional : Did.t -\u003e Handle.t -\u003e (verified_identity, verification_error) result\n```\n\n## Verification Flow\n\n1. **verify_did**:\n - Resolve DID โ DID document\n - Extract handle from alsoKnownAs\n - Resolve handle โ DID\n - Verify DIDs match\n\n2. **verify_handle**:\n - Resolve handle โ DID\n - Resolve DID โ DID document\n - Verify handle in alsoKnownAs\n\n## Dependencies\n- atproto-identity (did_resolver, handle_resolver)","acceptance_criteria":"- DIDโHandle verification (handle in alsoKnownAs)\n- HandleโDID verification (DID resolves correctly)\n- Bidirectional verification\n- Proper error messages for mismatches","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:58.806441234+01:00","updated_at":"2025-12-28T11:10:15.62066401+01:00","closed_at":"2025-12-28T11:10:15.62066401+01:00","labels":["identity","verification"],"dependencies":[{"issue_id":"atproto-33","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:03.802465302+01:00","created_by":"daemon"},{"issue_id":"atproto-33","depends_on_id":"atproto-31","type":"blocks","created_at":"2025-12-28T00:10:07.905145269+01:00","created_by":"daemon"},{"issue_id":"atproto-33","depends_on_id":"atproto-32","type":"blocks","created_at":"2025-12-28T00:10:08.46247471+01:00","created_by":"daemon"}]}
20
-
{"id":"atproto-40","title":"Network Layer - XRPC and Sync","description":"Implement the network layer libraries that handle HTTP transport (XRPC), WebSocket event streams, and repository synchronization for the AT Protocol.","design":"## Packages\n\n### atproto-xrpc\n- XRPC client (query/procedure calls)\n- XRPC server (Express-like routing)\n- Lexicon-based validation\n- Authentication (OAuth, JWT)\n- Error handling\n\n### atproto-sync\n- Event stream (WebSocket) client\n- Firehose events (#commit, #identity, #account)\n- Repository diff handling\n- Commit proof verification\n\n## XRPC Protocol\n\n- GET /xrpc/\u003cNSID\u003e for queries\n- POST /xrpc/\u003cNSID\u003e for procedures\n- JSON request/response bodies\n- Bearer token authentication\n\n## Event Stream Wire Protocol\n\n- WebSocket with binary frames\n- DAG-CBOR encoded messages\n- Header + payload structure\n\n## Dependencies\n- atproto-syntax\n- atproto-ipld\n- atproto-lexicon","acceptance_criteria":"- XRPC client can make authenticated requests\n- XRPC server can handle requests with Lexicon validation\n- Event stream (firehose) subscription works\n- Repository sync protocol works","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:01.661143114+01:00","updated_at":"2025-12-28T11:57:34.384344188+01:00","closed_at":"2025-12-28T11:57:34.384344188+01:00","labels":["epic","network"],"dependencies":[{"issue_id":"atproto-40","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.029904827+01:00","created_by":"daemon"}]}
21
-
{"id":"atproto-41","title":"Implement XRPC client","description":"Implement XRPC client for AT Protocol. XRPC is the HTTP-based API protocol used for client-server communication.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/client.ml *)\ntype t\n\ntype _ Effect.t +=\n | Http_request : request -\u003e response Effect.t\n\nand request = {\n method_: [ `GET | `POST ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\nand response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\nval create : base_url:Uri.t -\u003e t\nval with_auth : t -\u003e token:string -\u003e t\n\nval query : \n t -\u003e \n nsid:Nsid.t -\u003e \n params:(string * string) list -\u003e \n (Jsont.json, xrpc_error) result\n\nval procedure :\n t -\u003e\n nsid:Nsid.t -\u003e\n ?params:(string * string) list -\u003e\n input:Jsont.json -\u003e\n (Jsont.json, xrpc_error) result\n\ntype xrpc_error = {\n error: string;\n message: string option;\n}\n```\n\n## Jsont Codec for XRPC Error\n\n```ocaml\nlet xrpc_error_jsont : xrpc_error Jsont.t =\n Jsont.obj \"xrpc_error\" @@ fun o -\u003e\n let error = Jsont.obj_mem o \"error\" Jsont.string in\n let message = Jsont.obj_mem o \"message\" ~opt:true Jsont.string in\n Jsont.obj_finish o { error; message }\n```\n\n## XRPC URL Structure\n\n- Query: `GET /xrpc/\u003cnsid\u003e?param1=val1\u0026param2=val2`\n- Procedure: `POST /xrpc/\u003cnsid\u003e` with JSON body\n\n## Authentication\n\nBearer token in Authorization header:\n`Authorization: Bearer \u003caccess-token\u003e`\n\n## Dependencies\n- atproto-syntax (nsid)\n- jsont","acceptance_criteria":"- Query endpoints (GET) with parameter handling\n- Procedure endpoints (POST) with JSON body\n- Authentication (Bearer token)\n- Proper error response handling\n- Lexicon-based validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:23.998190895+01:00","updated_at":"2025-12-28T10:32:40.042969531+01:00","closed_at":"2025-12-28T10:32:40.042969531+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-41","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:03.65623332+01:00","created_by":"daemon"},{"issue_id":"atproto-41","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:11:08.071739524+01:00","created_by":"daemon"}]}
22
-
{"id":"atproto-42","title":"Implement XRPC server","description":"Implement XRPC server for AT Protocol. This enables building PDS and other AT Protocol services.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/server.ml *)\ntype t\ntype handler = context -\u003e (response, xrpc_error) result\n\nand context = {\n params: (string * string) list;\n input: Jsont.json option;\n auth: auth_info option;\n}\n\nand auth_info = {\n did: Did.t;\n scope: string list;\n}\n\nand response =\n | Json of Jsont.json\n | Bytes of { data: bytes; content_type: string }\n\nval create : unit -\u003e t\n\nval query : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\nval procedure : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\n\n(* Effects-based request handling *)\ntype _ Effect.t +=\n | Handle_request : request -\u003e response Effect.t\n\nval handle : t -\u003e request -\u003e response\n```\n\n## Middleware Pattern\n\n```ocaml\nval with_auth : t -\u003e (context -\u003e auth_info option) -\u003e t\nval with_validation : t -\u003e lexicons:Lexicon.registry -\u003e t\nval with_rate_limit : t -\u003e limits:rate_limit_config -\u003e t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-lexicon (for validation)\n- jsont","acceptance_criteria":"- Route registration by NSID\n- Request parameter validation\n- Response serialization\n- Error handling middleware\n- Lexicon schema validation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:30.734032128+01:00","updated_at":"2025-12-28T11:18:17.597713348+01:00","closed_at":"2025-12-28T11:18:17.597713348+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-42","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:04.381357087+01:00","created_by":"daemon"},{"issue_id":"atproto-42","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:08.952225305+01:00","created_by":"daemon"},{"issue_id":"atproto-42","depends_on_id":"atproto-52","type":"blocks","created_at":"2025-12-28T00:12:10.416004945+01:00","created_by":"daemon"}]}
23
-
{"id":"atproto-43","title":"Implement Firehose (event stream) client","description":"Implement event stream (firehose) client for AT Protocol. The firehose provides real-time updates from the network.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/firehose.ml *)\ntype event =\n | Commit of commit_event\n | Identity of identity_event\n | Account of account_event\n\nand commit_event = {\n seq: int64;\n repo: Did.t;\n rev: Tid.t;\n since: Tid.t option;\n commit: Cid.t;\n blocks: bytes; (* CAR slice *)\n ops: operation list;\n too_big: bool;\n}\n\nand operation = {\n action: [ `Create | `Update | `Delete ];\n path: string; (* collection/rkey *)\n cid: Cid.t option;\n}\n\nand identity_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n handle: Handle.t option;\n}\n\nand account_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n active: bool;\n status: string option;\n}\n\ntype _ Effect.t +=\n | Websocket_connect : Uri.t -\u003e websocket Effect.t\n | Websocket_recv : websocket -\u003e bytes Effect.t\n | Websocket_close : websocket -\u003e unit Effect.t\n\nval subscribe : \n uri:Uri.t -\u003e \n ?cursor:int64 -\u003e \n (event -\u003e unit) -\u003e \n unit\n```\n\n## Wire Protocol\n\n- Binary WebSocket frames\n- Each frame: header (DAG-CBOR) + payload (DAG-CBOR)\n- Header: `{ \"op\": 1, \"t\": \"#commit\" }`\n\n## Dependencies\n- atproto-ipld (dag-cbor)\n- atproto-syntax","acceptance_criteria":"- WebSocket connection management\n- DAG-CBOR frame decoding\n- Event type dispatching (#commit, #identity, #account)\n- Cursor-based resumption\n- All firehose interop tests pass","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:42.406702551+01:00","updated_at":"2025-12-28T10:54:13.835589935+01:00","closed_at":"2025-12-28T10:54:13.835589935+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-43","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:05.216684474+01:00","created_by":"daemon"},{"issue_id":"atproto-43","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:11:10.008522642+01:00","created_by":"daemon"}]}
24
-
{"id":"atproto-44","title":"Implement Repository sync","description":"Implement repository synchronization for AT Protocol. This enables PDS-to-PDS and relay sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/repo_sync.ml *)\ntype sync_result = {\n commit: Commit.t;\n blocks: (Cid.t * bytes) list;\n}\n\nval get_repo : \n client:Xrpc.Client.t -\u003e \n did:Did.t -\u003e \n (sync_result, error) result\n\nval get_checkout :\n client:Xrpc.Client.t -\u003e\n did:Did.t -\u003e\n commit:Cid.t -\u003e\n (sync_result, error) result\n\n(* Diff handling *)\ntype diff_entry = {\n action: [ `Create | `Update | `Delete ];\n collection: Nsid.t;\n rkey: string;\n cid: Cid.t option;\n value: Dag_cbor.value option;\n}\n\nval compute_diff : \n old_commit:Cid.t -\u003e \n new_commit:Cid.t -\u003e \n blocks:(Cid.t -\u003e bytes option) -\u003e\n diff_entry list\n\nval apply_diff :\n repo:Repo.t -\u003e\n diff:diff_entry list -\u003e\n Repo.t\n```\n\n## Sync Protocol Endpoints\n\n- `com.atproto.sync.getRepo` - Full repo export\n- `com.atproto.sync.getCheckout` - Specific commit\n- `com.atproto.sync.subscribeRepos` - Real-time updates\n\n## Dependencies\n- atproto-repo\n- atproto-xrpc","acceptance_criteria":"- Repository export (getRepo)\n- Incremental sync (subscribeRepos)\n- Diff computation between commits\n- Proof verification","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:51.918242921+01:00","updated_at":"2025-12-28T11:15:00.121154336+01:00","closed_at":"2025-12-28T11:15:00.121154336+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-44","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:06.164238338+01:00","created_by":"daemon"},{"issue_id":"atproto-44","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:11:10.849151222+01:00","created_by":"daemon"},{"issue_id":"atproto-44","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:11.847570996+01:00","created_by":"daemon"}]}
25
-
{"id":"atproto-45","title":"Implement OAuth client","description":"Implement OAuth client for AT Protocol authentication. OAuth is the preferred authentication method.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/oauth.ml *)\ntype client_config = {\n client_id: string;\n redirect_uri: Uri.t;\n scope: string list;\n}\n\ntype authorization_request = {\n state: string;\n code_verifier: string; (* PKCE *)\n authorization_url: Uri.t;\n}\n\ntype tokens = {\n access_token: string;\n refresh_token: string option;\n expires_at: Ptime.t;\n scope: string list;\n}\n\nval start_authorization : \n config:client_config -\u003e \n pds:Uri.t -\u003e \n authorization_request\n\nval complete_authorization :\n config:client_config -\u003e\n code:string -\u003e\n code_verifier:string -\u003e\n (tokens, error) result\n\nval refresh_tokens :\n config:client_config -\u003e\n refresh_token:string -\u003e\n (tokens, error) result\n```\n\n## OAuth Flow\n\n1. Discover authorization server from PDS\n2. Generate PKCE code_verifier + code_challenge\n3. Redirect to authorization URL\n4. Exchange code for tokens\n5. Use access_token in Bearer header\n6. Refresh when expired\n\n## Dependencies\n- atproto-crypto (for PKCE)\n- atproto-xrpc","acceptance_criteria":"- OAuth 2.0 authorization code flow\n- PKCE support\n- Token refresh\n- DPoP (proof of possession) support","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:59.811580681+01:00","updated_at":"2025-12-28T11:24:41.399056388+01:00","closed_at":"2025-12-28T11:24:41.399056388+01:00","labels":["auth","network"],"dependencies":[{"issue_id":"atproto-45","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:07.109758394+01:00","created_by":"daemon"},{"issue_id":"atproto-45","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:12.874999712+01:00","created_by":"daemon"},{"issue_id":"atproto-45","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:11:13.692776478+01:00","created_by":"daemon"}]}
26
-
{"id":"atproto-50","title":"Application Layer - Lexicon and API","description":"Implement the application layer libraries that handle Lexicon schemas, record validation, and provide a high-level API for building AT Protocol applications.","design":"## Packages\n\n### atproto-lexicon\n- Lexicon schema parser\n- Record validation\n- XRPC param/input/output validation\n- Schema registry\n\n### atproto-lexicon-gen\n- Code generation from Lexicon schemas\n- Type-safe OCaml types\n- Encoder/decoder generation\n\n### atproto-api\n- High-level client API\n- Session management\n- RichText handling\n- Common operations (post, like, follow, etc.)\n\n## Lexicon Types\n\n- record: Repository record schemas\n- query: HTTP GET endpoints\n- procedure: HTTP POST endpoints\n- subscription: WebSocket streams\n\n## Field Types\n\n- Primitives: boolean, integer, string, bytes, cid-link\n- Containers: array, object\n- References: ref, union\n- Special: blob, unknown, token\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- jsont","acceptance_criteria":"- Lexicon parser handles all schema types\n- Record validation works against schemas\n- Code generation produces type-safe OCaml\n- All lexicon interop tests pass","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:09.195003323+01:00","updated_at":"2025-12-28T11:57:35.469581739+01:00","closed_at":"2025-12-28T11:57:35.469581739+01:00","labels":["application","epic"],"dependencies":[{"issue_id":"atproto-50","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.879118155+01:00","created_by":"daemon"}]}
27
-
{"id":"atproto-51","title":"Implement Lexicon schema parser","description":"Implement Lexicon schema parser for AT Protocol. Lexicon is the schema language used to define records and APIs.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/schema.ml *)\ntype lexicon = {\n lexicon: int; (* version, always 1 *)\n id: Nsid.t;\n revision: int option;\n description: string option;\n defs: (string * definition) list;\n}\n\nand definition =\n | Record of record_def\n | Query of query_def\n | Procedure of procedure_def\n | Subscription of subscription_def\n | Object of object_def\n | Array of array_def\n | Token of token_def\n | String of string_def\n (* ... *)\n\nand record_def = {\n description: string option;\n key: record_key;\n record: object_def;\n}\n\nand query_def = {\n description: string option;\n parameters: params_def option;\n output: output_def option;\n errors: error_def list;\n}\n\n(* ... full schema types ... *)\n\nval parse : Jsont.json -\u003e (lexicon, error) result\n\n(* atproto-lexicon/lib/registry.ml *)\ntype t\n\nval create : unit -\u003e t\nval add : t -\u003e lexicon -\u003e t\nval get : t -\u003e Nsid.t -\u003e lexicon option\nval get_def : t -\u003e Nsid.t -\u003e string -\u003e definition option\n```\n\n## Jsont Codecs for Lexicon Schemas\n\n```ocaml\nlet string_def_jsont : string_def Jsont.t =\n Jsont.obj \"string_def\" @@ fun o -\u003e\n let format = Jsont.obj_mem o \"format\" ~opt:true Jsont.string in\n let min_length = Jsont.obj_mem o \"minLength\" ~opt:true Jsont.int in\n let max_length = Jsont.obj_mem o \"maxLength\" ~opt:true Jsont.int in\n let min_graphemes = Jsont.obj_mem o \"minGraphemes\" ~opt:true Jsont.int in\n let max_graphemes = Jsont.obj_mem o \"maxGraphemes\" ~opt:true Jsont.int in\n let enum = Jsont.obj_mem o \"enum\" ~opt:true (Jsont.list Jsont.string) in\n let const = Jsont.obj_mem o \"const\" ~opt:true Jsont.string in\n Jsont.obj_finish o { format; min_length; max_length; min_graphemes; max_graphemes; enum; const }\n\nlet definition_jsont : definition Jsont.t =\n (* Discriminated union based on \"type\" field *)\n Jsont.obj \"definition\" @@ fun o -\u003e\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n match type_ with\n | \"record\" -\u003e Record (decode_record_def o)\n | \"query\" -\u003e Query (decode_query_def o)\n | \"procedure\" -\u003e Procedure (decode_procedure_def o)\n | \"object\" -\u003e Object (decode_object_def o)\n | \"string\" -\u003e String (decode_string_def o)\n | _ -\u003e failwith (\"unknown definition type: \" ^ type_)\n\nlet lexicon_jsont : lexicon Jsont.t =\n Jsont.obj \"lexicon\" @@ fun o -\u003e\n let lexicon = Jsont.obj_mem o \"lexicon\" Jsont.int in\n let id = Jsont.obj_mem o \"id\" nsid_jsont in\n let revision = Jsont.obj_mem o \"revision\" ~opt:true Jsont.int in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let defs = Jsont.obj_mem o \"defs\" (Jsont.obj_map definition_jsont) in\n Jsont.obj_finish o { lexicon; id; revision; description; defs }\n```\n\n## Lexicon Schema Structure\n\n```json\n{\n \"lexicon\": 1,\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": { \"type\": \"record\", ... },\n \"entity\": { \"type\": \"object\", ... }\n }\n}\n```\n\n## Dependencies\n- atproto-syntax\n- jsont","acceptance_criteria":"- Parse all Lexicon schema types (record, query, procedure, subscription)\n- Parse all field types (primitives, containers, refs)\n- Parse all format constraints\n- Schema registry with NSID lookup\n- All lexicon interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:28.701630723+01:00","updated_at":"2025-12-28T10:12:30.084906585+01:00","closed_at":"2025-12-28T10:12:30.084906585+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-51","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:04.743859406+01:00","created_by":"daemon"},{"issue_id":"atproto-51","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:12:08.34127929+01:00","created_by":"daemon"}]}
28
-
{"id":"atproto-52","title":"Implement Lexicon validation","description":"Implement Lexicon-based validation for AT Protocol data. This validates records and API payloads against schemas.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/validator.ml *)\ntype validation_error = {\n path: string list;\n message: string;\n}\n\nval validate_record :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n value:Dag_cbor.value -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_params :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n params:(string * string) list -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_input :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n input:Jsont.json -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_output :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n output:Jsont.json -\u003e\n (unit, validation_error list) result\n```\n\n## Constraint Types\n\n- **String**: minLength, maxLength, minGraphemes, maxGraphemes, format, enum, const\n- **Integer**: minimum, maximum, enum, const\n- **Bytes**: minLength, maxLength\n- **Array**: minLength, maxLength, items type\n- **Blob**: maxSize, accept (MIME types)\n- **Union**: open/closed, refs\n\n## Format Validators (Parser-based, NO REGEX)\n\nEach format has a dedicated parser module:\n\n```ocaml\n(* atproto-lexicon/lib/formats.ml *)\n\nlet validate_did s = Did.of_string s |\u003e Result.is_ok\nlet validate_handle s = Handle.of_string s |\u003e Result.is_ok\nlet validate_nsid s = Nsid.of_string s |\u003e Result.is_ok\nlet validate_tid s = Tid.of_string s |\u003e Result.is_ok\nlet validate_cid s = Cid.of_string s |\u003e Result.is_ok\nlet validate_at_uri s = At_uri.of_string s |\u003e Result.is_ok\nlet validate_at_identifier s = \n Did.of_string s |\u003e Result.is_ok || Handle.of_string s |\u003e Result.is_ok\nlet validate_record_key s = Record_key.of_string s |\u003e Result.is_ok\n\nlet validate_datetime s =\n (* Hand-written RFC-3339 parser *)\n parse_datetime s |\u003e Result.is_ok\n\nlet validate_language s =\n (* BCP-47 language tag parser *)\n parse_language_tag s |\u003e Result.is_ok\n\nlet validate_uri s =\n (* RFC-3986 URI parser *)\n Uri.of_string s |\u003e Option.is_some\n```\n\n## Dependencies\n- atproto-lexicon (schema)\n- atproto-syntax (format validators)\n- jsont","acceptance_criteria":"- Validate records against schemas\n- Validate XRPC params, input, output\n- Proper error messages with paths\n- All constraint types supported\n- All record-data interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:39.125440686+01:00","updated_at":"2025-12-28T10:25:46.671434007+01:00","closed_at":"2025-12-28T10:25:46.671434007+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-52","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:05.375287273+01:00","created_by":"daemon"},{"issue_id":"atproto-52","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:09.479940241+01:00","created_by":"daemon"}]}
29
-
{"id":"atproto-53","title":"Implement Lexicon code generation","description":"Implement code generation from Lexicon schemas to OCaml types and API bindings.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon-gen/lib/codegen.ml *)\ntype config = {\n output_dir: string;\n module_prefix: string;\n}\n\nval generate_types : config:config -\u003e lexicon:Lexicon.t -\u003e unit\nval generate_client : config:config -\u003e lexicons:Lexicon.t list -\u003e unit\n```\n\n## Generated Code Example\n\nInput Lexicon:\n```json\n{\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": {\n \"type\": \"record\",\n \"record\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": { \"type\": \"string\", \"maxGraphemes\": 300 },\n \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n }\n }\n }\n }\n}\n```\n\nGenerated OCaml:\n```ocaml\nmodule App_bsky_feed_post = struct\n type t = {\n text: string;\n created_at: Ptime.t;\n }\n \n let jsont : t Jsont.t =\n Jsont.obj \"app.bsky.feed.post\" @@ fun o -\u003e\n let text = Jsont.obj_mem o \"text\" Jsont.string in\n let created_at = Jsont.obj_mem o \"createdAt\" Datetime.jsont in\n Jsont.obj_finish o { text; created_at }\n \n val to_dag_cbor : t -\u003e Dag_cbor.value\n val of_dag_cbor : Dag_cbor.value -\u003e (t, error) result\nend\n```\n\n## CLI Tool\n\n```bash\natproto-lexicon-gen --input lexicons/ --output lib/generated/\n```\n\n## Dependencies\n- atproto-lexicon\n- jsont","acceptance_criteria":"- Generate OCaml types from Lexicon schemas\n- Generate encoders/decoders\n- Type-safe API bindings\n- CLI tool for code generation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:47.861552784+01:00","updated_at":"2025-12-28T11:28:03.226633204+01:00","closed_at":"2025-12-28T11:28:03.226633204+01:00","labels":["application","codegen"],"dependencies":[{"issue_id":"atproto-53","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:06.539440409+01:00","created_by":"daemon"},{"issue_id":"atproto-53","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:11.189125052+01:00","created_by":"daemon"}]}
30
-
{"id":"atproto-54","title":"Implement high-level API client","description":"Implement high-level API client for AT Protocol / Bluesky. This provides a user-friendly interface for common operations.","design":"## Module Structure\n\n```ocaml\n(* atproto-api/lib/agent.ml *)\ntype t\n\nval create : pds:Uri.t -\u003e t\n\n(* Authentication *)\nval login : t -\u003e identifier:string -\u003e password:string -\u003e (t, error) result\nval login_oauth : t -\u003e tokens:Oauth.tokens -\u003e t\nval refresh_session : t -\u003e (t, error) result\n\n(* Profile *)\nval get_profile : t -\u003e actor:string -\u003e (profile, error) result\nval update_profile : t -\u003e display_name:string option -\u003e ... -\u003e (unit, error) result\n\n(* Posts *)\nval create_post : t -\u003e text:string -\u003e ?reply:reply_ref -\u003e ... -\u003e (post_ref, error) result\nval delete_post : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Social *)\nval like : t -\u003e uri:At_uri.t -\u003e cid:Cid.t -\u003e (like_ref, error) result\nval follow : t -\u003e did:Did.t -\u003e (follow_ref, error) result\nval unfollow : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Feed *)\nval get_timeline : t -\u003e ?cursor:string -\u003e ?limit:int -\u003e (timeline, error) result\nval get_author_feed : t -\u003e actor:string -\u003e ... -\u003e (feed, error) result\n\n(* atproto-api/lib/richtext.ml *)\ntype t\n\nval create : string -\u003e t\nval detect_facets : t -\u003e t (* auto-detect mentions, links *)\nval add_mention : t -\u003e start:int -\u003e end_:int -\u003e did:Did.t -\u003e t\nval add_link : t -\u003e start:int -\u003e end_:int -\u003e uri:Uri.t -\u003e t\nval to_post_record : t -\u003e Dag_cbor.value\n```\n\n## Jsont Codecs for API Types\n\n```ocaml\nlet profile_jsont : profile Jsont.t =\n Jsont.obj \"profile\" @@ fun o -\u003e\n let did = Jsont.obj_mem o \"did\" did_jsont in\n let handle = Jsont.obj_mem o \"handle\" handle_jsont in\n let display_name = Jsont.obj_mem o \"displayName\" ~opt:true Jsont.string in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let avatar = Jsont.obj_mem o \"avatar\" ~opt:true Jsont.string in\n let followers_count = Jsont.obj_mem o \"followersCount\" ~opt:true Jsont.int in\n let follows_count = Jsont.obj_mem o \"followsCount\" ~opt:true Jsont.int in\n let posts_count = Jsont.obj_mem o \"postsCount\" ~opt:true Jsont.int in\n Jsont.obj_finish o { did; handle; display_name; description; avatar; \n followers_count; follows_count; posts_count }\n\nlet facet_jsont : facet Jsont.t =\n Jsont.obj \"facet\" @@ fun o -\u003e\n let index = Jsont.obj_mem o \"index\" byte_slice_jsont in\n let features = Jsont.obj_mem o \"features\" (Jsont.list facet_feature_jsont) in\n Jsont.obj_finish o { index; features }\n```\n\n## RichText Facets\n\n```json\n{\n \"text\": \"Hello @alice.bsky.social!\",\n \"facets\": [\n {\n \"index\": { \"byteStart\": 6, \"byteEnd\": 25 },\n \"features\": [\n { \"$type\": \"app.bsky.richtext.facet#mention\", \"did\": \"did:plc:...\" }\n ]\n }\n ]\n}\n```\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- atproto-repo\n- jsont","acceptance_criteria":"- Session management (login, logout, refresh)\n- Common operations (post, like, follow, etc.)\n- RichText handling (mentions, links, facets)\n- Timeline and feed fetching\n- Profile operations","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:00.736309435+01:00","updated_at":"2025-12-28T11:47:47.071271001+01:00","closed_at":"2025-12-28T11:47:47.071271001+01:00","labels":["api","application"],"dependencies":[{"issue_id":"atproto-54","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:07.636789403+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:12:12.376875324+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-33","type":"blocks","created_at":"2025-12-28T00:12:13.060557136+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:12:13.934360048+01:00","created_by":"daemon"}]}
31
-
{"id":"atproto-5l1","title":"Refactor JSON to simdjsont (replace yojson/jsont)","description":"","notes":"Migrated atproto-lexicon off Yojson onto simdjsont.Json.t + Simdjsont.decode Codec.value. Updated lib/lexicon/{parser,validator,atproto_lexicon,codegen} and lib/lexicon/dune. Updated test/lexicon/test_lexicon.ml fixtures loader + patterns. Verified: dune runtest test/lexicon OK; dune build @install OK.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2026-01-01T18:06:10.17746938+01:00","updated_at":"2026-01-01T20:40:51.684953681+01:00","dependencies":[{"issue_id":"atproto-5l1","depends_on_id":"atproto-dqs","type":"blocks","created_at":"2026-01-01T18:06:39.648046004+01:00","created_by":"daemon"}]}
32
-
{"id":"atproto-60","title":"Implement effects-based I/O abstraction","description":"Implement the effects-based I/O abstraction layer that makes all libraries runtime-agnostic.","design":"## Module Structure\n\n```ocaml\n(* atproto-effects/lib/effects.ml *)\n\n(* HTTP effects *)\ntype http_request = {\n method_: [ `GET | `POST | `PUT | `DELETE ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\ntype http_response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\ntype _ Effect.t +=\n | Http_request : http_request -\u003e http_response Effect.t\n\n(* DNS effects *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Dns_a : string -\u003e string list Effect.t\n\n(* Time effects *)\ntype _ Effect.t +=\n | Now : Ptime.t Effect.t\n | Sleep : float -\u003e unit Effect.t\n\n(* Random effects *)\ntype _ Effect.t +=\n | Random_bytes : int -\u003e bytes Effect.t\n\n(* atproto-effects-eio/lib/handler.ml *)\nval run : (unit -\u003e 'a) -\u003e 'a\n```\n\n## Handler Example (eio)\n\n```ocaml\nlet run f =\n Effect.Deep.match_ f ()\n {\n retc = Fun.id;\n exnc = raise;\n effc = fun (type a) (e : a Effect.t) -\u003e\n match e with\n | Http_request req -\u003e\n Some (fun (k : (a, _) continuation) -\u003e\n let resp = Eio_client.request req in\n continue k resp)\n | Dns_txt domain -\u003e\n Some (fun k -\u003e\n let records = Eio_dns.txt domain in\n continue k records)\n | _ -\u003e None\n }\n```\n\n## Dependencies\n- eio (for testing handler)","acceptance_criteria":"- Effect types for HTTP, DNS, time, random\n- eio-based handler for testing\n- Handler composition utilities\n- Performance benchmarks","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:29.021401617+01:00","updated_at":"2025-12-28T11:57:08.264086142+01:00","closed_at":"2025-12-28T11:57:08.264086142+01:00","labels":["effects","infrastructure"],"dependencies":[{"issue_id":"atproto-60","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:55.467983208+01:00","created_by":"daemon"}]}
33
-
{"id":"atproto-61","title":"Set up interoperability test suite","description":"Set up and run the AT Protocol interoperability tests from bluesky-social/atproto-interop-tests.","design":"## Test Structure\n\n```\ntest/\nโโโ interop/\nโ โโโ syntax_test.ml # Handle, DID, NSID, TID, etc.\nโ โโโ crypto_test.ml # Signatures, did:key\nโ โโโ data_model_test.ml # DAG-CBOR, CID\nโ โโโ mst_test.ml # Key heights, tree structure\nโ โโโ lexicon_test.ml # Schema and record validation\nโ โโโ firehose_test.ml # Commit proofs\nโโโ fixtures/ # Cloned from atproto-interop-tests\nโโโ dune\n```\n\n## Test Approach\n\n1. Clone test vectors from GitHub\n2. Parse JSON fixtures using jsont\n3. Parse text fixtures line by line\n4. Run each test case\n5. Compare output to expected values\n\n## Example Test\n\n```ocaml\nlet load_json_fixtures path =\n let json = Jsont.of_file path in\n Jsont.decode (Jsont.list fixture_jsont) json\n\nlet%test \"handle_syntax_valid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_valid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e true\n | Error _ -\u003e false\n ) fixtures\n\nlet%test \"handle_syntax_invalid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_invalid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e false\n | Error _ -\u003e true\n ) fixtures\n\nlet%test \"crypto_signature_fixtures\" =\n let fixtures = load_json_fixtures \"fixtures/crypto/signature-fixtures.json\" in\n List.for_all (fun fixture -\u003e\n let message = Base64.decode fixture.message_base64 in\n let signature = Base64.decode fixture.signature_base64 in\n let key = Did_key.of_string fixture.public_key_did in\n let result = Crypto.verify key message signature in\n result = fixture.valid_signature\n ) fixtures\n```\n\n## Dependencies\n- alcotest or ounit2\n- jsont","acceptance_criteria":"- All syntax interop tests pass\n- All crypto interop tests pass\n- All data-model interop tests pass\n- All MST interop tests pass\n- All lexicon interop tests pass\n- All firehose interop tests pass","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:40.553908313+01:00","updated_at":"2025-12-28T13:25:34.614867702+01:00","closed_at":"2025-12-28T13:25:34.614867702+01:00","labels":["conformance","testing"],"dependencies":[{"issue_id":"atproto-61","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:56.180809368+01:00","created_by":"daemon"}]}
34
-
{"id":"atproto-62","title":"Set up monorepo package structure","description":"Set up the monorepo structure for multiple opam packages within a single repository.","design":"## Repository Structure\n\n```\natproto/\nโโโ dune-project # Root with all packages\nโโโ packages/\nโ โโโ atproto-syntax/\nโ โ โโโ lib/\nโ โ โ โโโ dune\nโ โ โ โโโ *.ml\nโ โ โโโ test/\nโ โ โ โโโ dune\nโ โ โ โโโ *_test.ml\nโ โ โโโ atproto-syntax.opam\nโ โโโ atproto-crypto/\nโ โโโ atproto-multibase/\nโ โโโ atproto-ipld/\nโ โโโ atproto-mst/\nโ โโโ atproto-repo/\nโ โโโ atproto-identity/\nโ โโโ atproto-xrpc/\nโ โโโ atproto-sync/\nโ โโโ atproto-lexicon/\nโ โโโ atproto-lexicon-gen/\nโ โโโ atproto-api/\nโ โโโ atproto-effects/\nโโโ examples/\nโ โโโ simple_client/\nโ โโโ firehose_consumer/\nโโโ interop-tests/\n```\n\n## dune-project\n\n```lisp\n(lang dune 3.20)\n(name atproto)\n(generate_opam_files true)\n\n(package\n (name atproto-syntax)\n (synopsis \"AT Protocol identifier syntax parsing\")\n (depends\n (ocaml (\u003e= 5.4))\n re\n ptime))\n\n(package\n (name atproto-crypto)\n ...)\n```\n\n## CI (.github/workflows/ci.yml)\n\n- OCaml 5.4 matrix\n- Build all packages\n- Run all tests\n- Run interop tests","acceptance_criteria":"- Multi-package dune-project structure\n- Separate opam files per package\n- CI pipeline for building and testing\n- Documentation generation setup","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:50.547102438+01:00","updated_at":"2025-12-28T11:57:18.856810633+01:00","closed_at":"2025-12-28T11:57:18.856810633+01:00","labels":["infrastructure","setup"],"dependencies":[{"issue_id":"atproto-62","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:57.015938611+01:00","created_by":"daemon"}]}
14
+
{"id":"atproto-25","title":"Implement Repository and Commit","description":"Implement repository support for AT Protocol. A repository is a signed, content-addressed collection of records organized by the MST.","design":"## Module Structure\n\n```ocaml\n(* atproto-repo/lib/commit.ml *)\ntype t = {\n did: Did.t;\n version: int; (* always 3 *)\n data: Cid.t; (* MST root *)\n rev: Tid.t;\n prev: Cid.t option;\n sig_: bytes;\n}\n\nval create : \n did:Did.t -\u003e \n data:Cid.t -\u003e \n rev:Tid.t -\u003e \n ?prev:Cid.t -\u003e \n key:K256.private_ -\u003e \n t\n\nval verify : t -\u003e public_key:K256.public -\u003e bool\nval to_dag_cbor : t -\u003e bytes\nval of_dag_cbor : bytes -\u003e (t, error) result\n\n(* atproto-repo/lib/repo.ml *)\ntype t\n\nval create : blockstore:Blockstore.t -\u003e did:Did.t -\u003e t\nval load : blockstore:Blockstore.t -\u003e root:Cid.t -\u003e t\n\nval get_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value option\nval create_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval update_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval delete_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e t\n\nval commit : t -\u003e key:K256.private_ -\u003e Commit.t\n```\n\n## Commit Signing Process\n\n1. Create unsigned commit (all fields except sig)\n2. Encode as DAG-CBOR\n3. SHA-256 hash the bytes\n4. Sign hash with account key (low-S!)\n5. Add signature as raw bytes\n\n## Dependencies\n- atproto-mst\n- atproto-crypto\n- atproto-syntax","acceptance_criteria":"- Commit object creation with proper v3 format\n- Commit signing with account key\n- Commit verification\n- Repository operations (create, update, delete records)","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:07.716307822+01:00","updated_at":"2025-12-28T02:25:00.961982054+01:00","closed_at":"2025-12-28T02:25:00.961982054+01:00","labels":["data","repo"],"dependencies":[{"issue_id":"atproto-25","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:22.387797246+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-25","depends_on_id":"atproto-24","type":"blocks","created_at":"2025-12-28T00:09:27.958219661+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-25","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:09:28.920614309+01:00","created_by":"daemon","metadata":"{}"}]}
15
+
{"id":"atproto-26","title":"Implement Blob handling","description":"Implement blob handling for AT Protocol. Blobs are binary data (images, videos) referenced by CID in records.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/blob.ml *)\ntype ref_ = {\n cid: Cid.t;\n mime_type: string;\n size: int;\n}\n\nval create : data:bytes -\u003e mime_type:string -\u003e ref_\nval to_dag_cbor : ref_ -\u003e Dag_cbor.value\nval of_dag_cbor : Dag_cbor.value -\u003e (ref_, error) result\n\n(* JSON representation *)\n(* { \"$type\": \"blob\", \"ref\": {\"$link\": \"...\"}, \"mimeType\": \"...\", \"size\": ... } *)\n```\n\n## Blob CID Requirements\n\n- Multicodec: `raw` (0x55), NOT dag-cbor\n- Hash: SHA-256 of raw bytes\n\n## Typed vs Untyped Blobs\n\nLegacy (untyped): just a CID link\nModern (typed): full blob object with $type\n\n## Dependencies\n- atproto-ipld","acceptance_criteria":"- Blob type encoding/decoding\n- Blob reference creation and validation\n- MIME type handling\n- Size constraints enforcement","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:14.976884267+01:00","updated_at":"2025-12-28T11:03:27.015943079+01:00","closed_at":"2025-12-28T11:03:27.015943079+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-26","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:23.336547933+01:00","created_by":"daemon","metadata":"{}"}]}
16
+
{"id":"atproto-30","title":"Identity Layer - DID and Handle Resolution","description":"Implement the identity layer libraries that handle DID resolution, handle resolution, and identity verification for the AT Protocol.","design":"## Packages\n\n### atproto-identity\n- DID resolution (did:plc, did:web)\n- Handle resolution (DNS TXT, HTTPS)\n- DID document parsing\n- Identity caching\n- Bidirectional verification (DIDโHandle)\n\n## Resolution Flow\n\n1. Handle โ DID: DNS TXT `_atproto.\u003chandle\u003e` or HTTPS `/.well-known/atproto-did`\n2. DID โ DID Document: Fetch from PLC directory or .well-known\n3. Extract: Signing key, PDS endpoint, handle\n\n## Effects-based Design\n\n```ocaml\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e string Effect.t\n | Dns_txt : string -\u003e string list Effect.t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto\n- jsont or yojson","acceptance_criteria":"- DID resolution works for did:plc and did:web\n- Handle resolution via DNS TXT and HTTPS works\n- DID document parsing is complete\n- Identity verification works end-to-end","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:54.380506112+01:00","updated_at":"2025-12-28T11:57:33.145244873+01:00","closed_at":"2025-12-28T11:57:33.145244873+01:00","labels":["epic","identity"],"dependencies":[{"issue_id":"atproto-30","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:15.083956697+01:00","created_by":"daemon","metadata":"{}"}]}
17
+
{"id":"atproto-31","title":"Implement DID resolution","description":"Implement DID resolution for AT Protocol supporting did:plc and did:web methods.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/did_resolver.ml *)\ntype did_document = {\n id: Did.t;\n also_known_as: string list; (* handles *)\n verification_method: verification_method list;\n service: service list;\n}\n\nand verification_method = {\n id: string;\n type_: string;\n controller: Did.t;\n public_key_multibase: string;\n}\n\nand service = {\n id: string;\n type_: string;\n service_endpoint: Uri.t;\n}\n\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Did.t -\u003e (did_document, error) result\nval get_signing_key : did_document -\u003e (Did_key.t, error) result\nval get_pds_endpoint : did_document -\u003e (Uri.t, error) result\nval get_handle : did_document -\u003e Handle.t option\n```\n\n## Jsont Codecs for DID Documents\n\n```ocaml\nlet verification_method_jsont : verification_method Jsont.t =\n Jsont.obj \"verification_method\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" Jsont.string in\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n let controller = Jsont.obj_mem o \"controller\" did_jsont in\n let public_key_multibase = Jsont.obj_mem o \"publicKeyMultibase\" Jsont.string in\n Jsont.obj_finish o { id; type_; controller; public_key_multibase }\n\nlet did_document_jsont : did_document Jsont.t =\n Jsont.obj \"did_document\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" did_jsont in\n let also_known_as = Jsont.obj_mem o \"alsoKnownAs\" ~opt:true \n (Jsont.list Jsont.string) ~default:[] in\n let verification_method = Jsont.obj_mem o \"verificationMethod\" \n (Jsont.list verification_method_jsont) in\n let service = Jsont.obj_mem o \"service\" ~opt:true \n (Jsont.list service_jsont) ~default:[] in\n Jsont.obj_finish o { id; also_known_as; verification_method; service }\n```\n\n## Resolution Endpoints\n\n- did:plc โ `https://plc.directory/\u003cdid\u003e`\n- did:web โ `https://\u003cdomain\u003e/.well-known/did.json`\n\n## Effects-based Design\n\nResolution uses effects for HTTP, allowing different runtimes:\n- eio handler for testing\n- cohttp handler for production\n- mock handler for unit tests\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto (for did:key parsing)\n- jsont","acceptance_criteria":"- did:plc resolution from PLC directory\n- did:web resolution from .well-known\n- DID document parsing\n- Caching with configurable TTL","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:42.738403632+01:00","updated_at":"2025-12-28T10:36:57.60764779+01:00","closed_at":"2025-12-28T10:36:57.60764779+01:00","labels":["did","identity"],"dependencies":[{"issue_id":"atproto-31","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.183867539+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-31","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:04.901673996+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-31","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:10:05.785020408+01:00","created_by":"daemon","metadata":"{}"}]}
18
+
{"id":"atproto-32","title":"Implement Handle resolution","description":"Implement handle resolution for AT Protocol. Handles are domain-based identifiers that resolve to DIDs.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/handle_resolver.ml *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Handle.t -\u003e (Did.t, error) result\n```\n\n## Resolution Algorithm\n\n1. Query DNS TXT record at `_atproto.\u003chandle\u003e`\n2. Look for record with `did=\u003cdid\u003e` value\n3. If no DNS record, try HTTPS: `https://\u003chandle\u003e/.well-known/atproto-did`\n4. Response should be plain text DID\n\n## Example\n\nHandle: `alice.bsky.social`\n1. DNS: `_atproto.alice.bsky.social` TXT โ `did=did:plc:abc123`\n2. Or HTTPS: `https://alice.bsky.social/.well-known/atproto-did` โ `did:plc:abc123`\n\n## Dependencies\n- atproto-syntax","acceptance_criteria":"- DNS TXT record resolution (_atproto.\u003chandle\u003e)\n- HTTPS fallback (/.well-known/atproto-did)\n- Handle normalization (lowercase)\n- Proper error handling for resolution failures","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:50.77787892+01:00","updated_at":"2025-12-28T10:45:02.168086436+01:00","closed_at":"2025-12-28T10:45:02.168086436+01:00","labels":["handle","identity"],"dependencies":[{"issue_id":"atproto-32","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.809033959+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-32","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:06.598127952+01:00","created_by":"daemon","metadata":"{}"}]}
19
+
{"id":"atproto-33","title":"Implement identity verification","description":"Implement bidirectional identity verification ensuring DIDs and handles are properly linked.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/identity.ml *)\ntype verified_identity = {\n did: Did.t;\n handle: Handle.t;\n signing_key: Did_key.t;\n pds_endpoint: Uri.t;\n}\n\ntype verification_error =\n | Did_resolution_failed of error\n | Handle_resolution_failed of error\n | Handle_mismatch of { expected: Handle.t; found: Handle.t option }\n | Did_mismatch of { expected: Did.t; found: Did.t }\n\nval verify_did : Did.t -\u003e (verified_identity, verification_error) result\nval verify_handle : Handle.t -\u003e (verified_identity, verification_error) result\nval verify_bidirectional : Did.t -\u003e Handle.t -\u003e (verified_identity, verification_error) result\n```\n\n## Verification Flow\n\n1. **verify_did**:\n - Resolve DID โ DID document\n - Extract handle from alsoKnownAs\n - Resolve handle โ DID\n - Verify DIDs match\n\n2. **verify_handle**:\n - Resolve handle โ DID\n - Resolve DID โ DID document\n - Verify handle in alsoKnownAs\n\n## Dependencies\n- atproto-identity (did_resolver, handle_resolver)","acceptance_criteria":"- DIDโHandle verification (handle in alsoKnownAs)\n- HandleโDID verification (DID resolves correctly)\n- Bidirectional verification\n- Proper error messages for mismatches","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:58.806441234+01:00","updated_at":"2025-12-28T11:10:15.62066401+01:00","closed_at":"2025-12-28T11:10:15.62066401+01:00","labels":["identity","verification"],"dependencies":[{"issue_id":"atproto-33","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:03.802465302+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-33","depends_on_id":"atproto-31","type":"blocks","created_at":"2025-12-28T00:10:07.905145269+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-33","depends_on_id":"atproto-32","type":"blocks","created_at":"2025-12-28T00:10:08.46247471+01:00","created_by":"daemon","metadata":"{}"}]}
20
+
{"id":"atproto-40","title":"Network Layer - XRPC and Sync","description":"Implement the network layer libraries that handle HTTP transport (XRPC), WebSocket event streams, and repository synchronization for the AT Protocol.","design":"## Packages\n\n### atproto-xrpc\n- XRPC client (query/procedure calls)\n- XRPC server (Express-like routing)\n- Lexicon-based validation\n- Authentication (OAuth, JWT)\n- Error handling\n\n### atproto-sync\n- Event stream (WebSocket) client\n- Firehose events (#commit, #identity, #account)\n- Repository diff handling\n- Commit proof verification\n\n## XRPC Protocol\n\n- GET /xrpc/\u003cNSID\u003e for queries\n- POST /xrpc/\u003cNSID\u003e for procedures\n- JSON request/response bodies\n- Bearer token authentication\n\n## Event Stream Wire Protocol\n\n- WebSocket with binary frames\n- DAG-CBOR encoded messages\n- Header + payload structure\n\n## Dependencies\n- atproto-syntax\n- atproto-ipld\n- atproto-lexicon","acceptance_criteria":"- XRPC client can make authenticated requests\n- XRPC server can handle requests with Lexicon validation\n- Event stream (firehose) subscription works\n- Repository sync protocol works","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:01.661143114+01:00","updated_at":"2025-12-28T11:57:34.384344188+01:00","closed_at":"2025-12-28T11:57:34.384344188+01:00","labels":["epic","network"],"dependencies":[{"issue_id":"atproto-40","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.029904827+01:00","created_by":"daemon","metadata":"{}"}]}
21
+
{"id":"atproto-41","title":"Implement XRPC client","description":"Implement XRPC client for AT Protocol. XRPC is the HTTP-based API protocol used for client-server communication.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/client.ml *)\ntype t\n\ntype _ Effect.t +=\n | Http_request : request -\u003e response Effect.t\n\nand request = {\n method_: [ `GET | `POST ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\nand response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\nval create : base_url:Uri.t -\u003e t\nval with_auth : t -\u003e token:string -\u003e t\n\nval query : \n t -\u003e \n nsid:Nsid.t -\u003e \n params:(string * string) list -\u003e \n (Jsont.json, xrpc_error) result\n\nval procedure :\n t -\u003e\n nsid:Nsid.t -\u003e\n ?params:(string * string) list -\u003e\n input:Jsont.json -\u003e\n (Jsont.json, xrpc_error) result\n\ntype xrpc_error = {\n error: string;\n message: string option;\n}\n```\n\n## Jsont Codec for XRPC Error\n\n```ocaml\nlet xrpc_error_jsont : xrpc_error Jsont.t =\n Jsont.obj \"xrpc_error\" @@ fun o -\u003e\n let error = Jsont.obj_mem o \"error\" Jsont.string in\n let message = Jsont.obj_mem o \"message\" ~opt:true Jsont.string in\n Jsont.obj_finish o { error; message }\n```\n\n## XRPC URL Structure\n\n- Query: `GET /xrpc/\u003cnsid\u003e?param1=val1\u0026param2=val2`\n- Procedure: `POST /xrpc/\u003cnsid\u003e` with JSON body\n\n## Authentication\n\nBearer token in Authorization header:\n`Authorization: Bearer \u003caccess-token\u003e`\n\n## Dependencies\n- atproto-syntax (nsid)\n- jsont","acceptance_criteria":"- Query endpoints (GET) with parameter handling\n- Procedure endpoints (POST) with JSON body\n- Authentication (Bearer token)\n- Proper error response handling\n- Lexicon-based validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:23.998190895+01:00","updated_at":"2025-12-28T10:32:40.042969531+01:00","closed_at":"2025-12-28T10:32:40.042969531+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-41","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:03.65623332+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-41","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:11:08.071739524+01:00","created_by":"daemon","metadata":"{}"}]}
22
+
{"id":"atproto-42","title":"Implement XRPC server","description":"Implement XRPC server for AT Protocol. This enables building PDS and other AT Protocol services.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/server.ml *)\ntype t\ntype handler = context -\u003e (response, xrpc_error) result\n\nand context = {\n params: (string * string) list;\n input: Jsont.json option;\n auth: auth_info option;\n}\n\nand auth_info = {\n did: Did.t;\n scope: string list;\n}\n\nand response =\n | Json of Jsont.json\n | Bytes of { data: bytes; content_type: string }\n\nval create : unit -\u003e t\n\nval query : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\nval procedure : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\n\n(* Effects-based request handling *)\ntype _ Effect.t +=\n | Handle_request : request -\u003e response Effect.t\n\nval handle : t -\u003e request -\u003e response\n```\n\n## Middleware Pattern\n\n```ocaml\nval with_auth : t -\u003e (context -\u003e auth_info option) -\u003e t\nval with_validation : t -\u003e lexicons:Lexicon.registry -\u003e t\nval with_rate_limit : t -\u003e limits:rate_limit_config -\u003e t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-lexicon (for validation)\n- jsont","acceptance_criteria":"- Route registration by NSID\n- Request parameter validation\n- Response serialization\n- Error handling middleware\n- Lexicon schema validation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:30.734032128+01:00","updated_at":"2025-12-28T11:18:17.597713348+01:00","closed_at":"2025-12-28T11:18:17.597713348+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-42","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:04.381357087+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-42","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:08.952225305+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-42","depends_on_id":"atproto-52","type":"blocks","created_at":"2025-12-28T00:12:10.416004945+01:00","created_by":"daemon","metadata":"{}"}]}
23
+
{"id":"atproto-43","title":"Implement Firehose (event stream) client","description":"Implement event stream (firehose) client for AT Protocol. The firehose provides real-time updates from the network.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/firehose.ml *)\ntype event =\n | Commit of commit_event\n | Identity of identity_event\n | Account of account_event\n\nand commit_event = {\n seq: int64;\n repo: Did.t;\n rev: Tid.t;\n since: Tid.t option;\n commit: Cid.t;\n blocks: bytes; (* CAR slice *)\n ops: operation list;\n too_big: bool;\n}\n\nand operation = {\n action: [ `Create | `Update | `Delete ];\n path: string; (* collection/rkey *)\n cid: Cid.t option;\n}\n\nand identity_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n handle: Handle.t option;\n}\n\nand account_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n active: bool;\n status: string option;\n}\n\ntype _ Effect.t +=\n | Websocket_connect : Uri.t -\u003e websocket Effect.t\n | Websocket_recv : websocket -\u003e bytes Effect.t\n | Websocket_close : websocket -\u003e unit Effect.t\n\nval subscribe : \n uri:Uri.t -\u003e \n ?cursor:int64 -\u003e \n (event -\u003e unit) -\u003e \n unit\n```\n\n## Wire Protocol\n\n- Binary WebSocket frames\n- Each frame: header (DAG-CBOR) + payload (DAG-CBOR)\n- Header: `{ \"op\": 1, \"t\": \"#commit\" }`\n\n## Dependencies\n- atproto-ipld (dag-cbor)\n- atproto-syntax","acceptance_criteria":"- WebSocket connection management\n- DAG-CBOR frame decoding\n- Event type dispatching (#commit, #identity, #account)\n- Cursor-based resumption\n- All firehose interop tests pass","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:42.406702551+01:00","updated_at":"2025-12-28T10:54:13.835589935+01:00","closed_at":"2025-12-28T10:54:13.835589935+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-43","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:05.216684474+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-43","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:11:10.008522642+01:00","created_by":"daemon","metadata":"{}"}]}
24
+
{"id":"atproto-44","title":"Implement Repository sync","description":"Implement repository synchronization for AT Protocol. This enables PDS-to-PDS and relay sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/repo_sync.ml *)\ntype sync_result = {\n commit: Commit.t;\n blocks: (Cid.t * bytes) list;\n}\n\nval get_repo : \n client:Xrpc.Client.t -\u003e \n did:Did.t -\u003e \n (sync_result, error) result\n\nval get_checkout :\n client:Xrpc.Client.t -\u003e\n did:Did.t -\u003e\n commit:Cid.t -\u003e\n (sync_result, error) result\n\n(* Diff handling *)\ntype diff_entry = {\n action: [ `Create | `Update | `Delete ];\n collection: Nsid.t;\n rkey: string;\n cid: Cid.t option;\n value: Dag_cbor.value option;\n}\n\nval compute_diff : \n old_commit:Cid.t -\u003e \n new_commit:Cid.t -\u003e \n blocks:(Cid.t -\u003e bytes option) -\u003e\n diff_entry list\n\nval apply_diff :\n repo:Repo.t -\u003e\n diff:diff_entry list -\u003e\n Repo.t\n```\n\n## Sync Protocol Endpoints\n\n- `com.atproto.sync.getRepo` - Full repo export\n- `com.atproto.sync.getCheckout` - Specific commit\n- `com.atproto.sync.subscribeRepos` - Real-time updates\n\n## Dependencies\n- atproto-repo\n- atproto-xrpc","acceptance_criteria":"- Repository export (getRepo)\n- Incremental sync (subscribeRepos)\n- Diff computation between commits\n- Proof verification","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:51.918242921+01:00","updated_at":"2025-12-28T11:15:00.121154336+01:00","closed_at":"2025-12-28T11:15:00.121154336+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-44","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:06.164238338+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-44","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:11:10.849151222+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-44","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:11.847570996+01:00","created_by":"daemon","metadata":"{}"}]}
25
+
{"id":"atproto-45","title":"Implement OAuth client","description":"Implement OAuth client for AT Protocol authentication. OAuth is the preferred authentication method.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/oauth.ml *)\ntype client_config = {\n client_id: string;\n redirect_uri: Uri.t;\n scope: string list;\n}\n\ntype authorization_request = {\n state: string;\n code_verifier: string; (* PKCE *)\n authorization_url: Uri.t;\n}\n\ntype tokens = {\n access_token: string;\n refresh_token: string option;\n expires_at: Ptime.t;\n scope: string list;\n}\n\nval start_authorization : \n config:client_config -\u003e \n pds:Uri.t -\u003e \n authorization_request\n\nval complete_authorization :\n config:client_config -\u003e\n code:string -\u003e\n code_verifier:string -\u003e\n (tokens, error) result\n\nval refresh_tokens :\n config:client_config -\u003e\n refresh_token:string -\u003e\n (tokens, error) result\n```\n\n## OAuth Flow\n\n1. Discover authorization server from PDS\n2. Generate PKCE code_verifier + code_challenge\n3. Redirect to authorization URL\n4. Exchange code for tokens\n5. Use access_token in Bearer header\n6. Refresh when expired\n\n## Dependencies\n- atproto-crypto (for PKCE)\n- atproto-xrpc","acceptance_criteria":"- OAuth 2.0 authorization code flow\n- PKCE support\n- Token refresh\n- DPoP (proof of possession) support","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:59.811580681+01:00","updated_at":"2025-12-28T11:24:41.399056388+01:00","closed_at":"2025-12-28T11:24:41.399056388+01:00","labels":["auth","network"],"dependencies":[{"issue_id":"atproto-45","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:07.109758394+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-45","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:12.874999712+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-45","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:11:13.692776478+01:00","created_by":"daemon","metadata":"{}"}]}
26
+
{"id":"atproto-50","title":"Application Layer - Lexicon and API","description":"Implement the application layer libraries that handle Lexicon schemas, record validation, and provide a high-level API for building AT Protocol applications.","design":"## Packages\n\n### atproto-lexicon\n- Lexicon schema parser\n- Record validation\n- XRPC param/input/output validation\n- Schema registry\n\n### atproto-lexicon-gen\n- Code generation from Lexicon schemas\n- Type-safe OCaml types\n- Encoder/decoder generation\n\n### atproto-api\n- High-level client API\n- Session management\n- RichText handling\n- Common operations (post, like, follow, etc.)\n\n## Lexicon Types\n\n- record: Repository record schemas\n- query: HTTP GET endpoints\n- procedure: HTTP POST endpoints\n- subscription: WebSocket streams\n\n## Field Types\n\n- Primitives: boolean, integer, string, bytes, cid-link\n- Containers: array, object\n- References: ref, union\n- Special: blob, unknown, token\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- jsont","acceptance_criteria":"- Lexicon parser handles all schema types\n- Record validation works against schemas\n- Code generation produces type-safe OCaml\n- All lexicon interop tests pass","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:09.195003323+01:00","updated_at":"2025-12-28T11:57:35.469581739+01:00","closed_at":"2025-12-28T11:57:35.469581739+01:00","labels":["application","epic"],"dependencies":[{"issue_id":"atproto-50","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.879118155+01:00","created_by":"daemon","metadata":"{}"}]}
27
+
{"id":"atproto-51","title":"Implement Lexicon schema parser","description":"Implement Lexicon schema parser for AT Protocol. Lexicon is the schema language used to define records and APIs.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/schema.ml *)\ntype lexicon = {\n lexicon: int; (* version, always 1 *)\n id: Nsid.t;\n revision: int option;\n description: string option;\n defs: (string * definition) list;\n}\n\nand definition =\n | Record of record_def\n | Query of query_def\n | Procedure of procedure_def\n | Subscription of subscription_def\n | Object of object_def\n | Array of array_def\n | Token of token_def\n | String of string_def\n (* ... *)\n\nand record_def = {\n description: string option;\n key: record_key;\n record: object_def;\n}\n\nand query_def = {\n description: string option;\n parameters: params_def option;\n output: output_def option;\n errors: error_def list;\n}\n\n(* ... full schema types ... *)\n\nval parse : Jsont.json -\u003e (lexicon, error) result\n\n(* atproto-lexicon/lib/registry.ml *)\ntype t\n\nval create : unit -\u003e t\nval add : t -\u003e lexicon -\u003e t\nval get : t -\u003e Nsid.t -\u003e lexicon option\nval get_def : t -\u003e Nsid.t -\u003e string -\u003e definition option\n```\n\n## Jsont Codecs for Lexicon Schemas\n\n```ocaml\nlet string_def_jsont : string_def Jsont.t =\n Jsont.obj \"string_def\" @@ fun o -\u003e\n let format = Jsont.obj_mem o \"format\" ~opt:true Jsont.string in\n let min_length = Jsont.obj_mem o \"minLength\" ~opt:true Jsont.int in\n let max_length = Jsont.obj_mem o \"maxLength\" ~opt:true Jsont.int in\n let min_graphemes = Jsont.obj_mem o \"minGraphemes\" ~opt:true Jsont.int in\n let max_graphemes = Jsont.obj_mem o \"maxGraphemes\" ~opt:true Jsont.int in\n let enum = Jsont.obj_mem o \"enum\" ~opt:true (Jsont.list Jsont.string) in\n let const = Jsont.obj_mem o \"const\" ~opt:true Jsont.string in\n Jsont.obj_finish o { format; min_length; max_length; min_graphemes; max_graphemes; enum; const }\n\nlet definition_jsont : definition Jsont.t =\n (* Discriminated union based on \"type\" field *)\n Jsont.obj \"definition\" @@ fun o -\u003e\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n match type_ with\n | \"record\" -\u003e Record (decode_record_def o)\n | \"query\" -\u003e Query (decode_query_def o)\n | \"procedure\" -\u003e Procedure (decode_procedure_def o)\n | \"object\" -\u003e Object (decode_object_def o)\n | \"string\" -\u003e String (decode_string_def o)\n | _ -\u003e failwith (\"unknown definition type: \" ^ type_)\n\nlet lexicon_jsont : lexicon Jsont.t =\n Jsont.obj \"lexicon\" @@ fun o -\u003e\n let lexicon = Jsont.obj_mem o \"lexicon\" Jsont.int in\n let id = Jsont.obj_mem o \"id\" nsid_jsont in\n let revision = Jsont.obj_mem o \"revision\" ~opt:true Jsont.int in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let defs = Jsont.obj_mem o \"defs\" (Jsont.obj_map definition_jsont) in\n Jsont.obj_finish o { lexicon; id; revision; description; defs }\n```\n\n## Lexicon Schema Structure\n\n```json\n{\n \"lexicon\": 1,\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": { \"type\": \"record\", ... },\n \"entity\": { \"type\": \"object\", ... }\n }\n}\n```\n\n## Dependencies\n- atproto-syntax\n- jsont","acceptance_criteria":"- Parse all Lexicon schema types (record, query, procedure, subscription)\n- Parse all field types (primitives, containers, refs)\n- Parse all format constraints\n- Schema registry with NSID lookup\n- All lexicon interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:28.701630723+01:00","updated_at":"2025-12-28T10:12:30.084906585+01:00","closed_at":"2025-12-28T10:12:30.084906585+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-51","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:04.743859406+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-51","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:12:08.34127929+01:00","created_by":"daemon","metadata":"{}"}]}
28
+
{"id":"atproto-52","title":"Implement Lexicon validation","description":"Implement Lexicon-based validation for AT Protocol data. This validates records and API payloads against schemas.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/validator.ml *)\ntype validation_error = {\n path: string list;\n message: string;\n}\n\nval validate_record :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n value:Dag_cbor.value -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_params :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n params:(string * string) list -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_input :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n input:Jsont.json -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_output :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n output:Jsont.json -\u003e\n (unit, validation_error list) result\n```\n\n## Constraint Types\n\n- **String**: minLength, maxLength, minGraphemes, maxGraphemes, format, enum, const\n- **Integer**: minimum, maximum, enum, const\n- **Bytes**: minLength, maxLength\n- **Array**: minLength, maxLength, items type\n- **Blob**: maxSize, accept (MIME types)\n- **Union**: open/closed, refs\n\n## Format Validators (Parser-based, NO REGEX)\n\nEach format has a dedicated parser module:\n\n```ocaml\n(* atproto-lexicon/lib/formats.ml *)\n\nlet validate_did s = Did.of_string s |\u003e Result.is_ok\nlet validate_handle s = Handle.of_string s |\u003e Result.is_ok\nlet validate_nsid s = Nsid.of_string s |\u003e Result.is_ok\nlet validate_tid s = Tid.of_string s |\u003e Result.is_ok\nlet validate_cid s = Cid.of_string s |\u003e Result.is_ok\nlet validate_at_uri s = At_uri.of_string s |\u003e Result.is_ok\nlet validate_at_identifier s = \n Did.of_string s |\u003e Result.is_ok || Handle.of_string s |\u003e Result.is_ok\nlet validate_record_key s = Record_key.of_string s |\u003e Result.is_ok\n\nlet validate_datetime s =\n (* Hand-written RFC-3339 parser *)\n parse_datetime s |\u003e Result.is_ok\n\nlet validate_language s =\n (* BCP-47 language tag parser *)\n parse_language_tag s |\u003e Result.is_ok\n\nlet validate_uri s =\n (* RFC-3986 URI parser *)\n Uri.of_string s |\u003e Option.is_some\n```\n\n## Dependencies\n- atproto-lexicon (schema)\n- atproto-syntax (format validators)\n- jsont","acceptance_criteria":"- Validate records against schemas\n- Validate XRPC params, input, output\n- Proper error messages with paths\n- All constraint types supported\n- All record-data interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:39.125440686+01:00","updated_at":"2025-12-28T10:25:46.671434007+01:00","closed_at":"2025-12-28T10:25:46.671434007+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-52","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:05.375287273+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-52","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:09.479940241+01:00","created_by":"daemon","metadata":"{}"}]}
29
+
{"id":"atproto-53","title":"Implement Lexicon code generation","description":"Implement code generation from Lexicon schemas to OCaml types and API bindings.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon-gen/lib/codegen.ml *)\ntype config = {\n output_dir: string;\n module_prefix: string;\n}\n\nval generate_types : config:config -\u003e lexicon:Lexicon.t -\u003e unit\nval generate_client : config:config -\u003e lexicons:Lexicon.t list -\u003e unit\n```\n\n## Generated Code Example\n\nInput Lexicon:\n```json\n{\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": {\n \"type\": \"record\",\n \"record\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": { \"type\": \"string\", \"maxGraphemes\": 300 },\n \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n }\n }\n }\n }\n}\n```\n\nGenerated OCaml:\n```ocaml\nmodule App_bsky_feed_post = struct\n type t = {\n text: string;\n created_at: Ptime.t;\n }\n \n let jsont : t Jsont.t =\n Jsont.obj \"app.bsky.feed.post\" @@ fun o -\u003e\n let text = Jsont.obj_mem o \"text\" Jsont.string in\n let created_at = Jsont.obj_mem o \"createdAt\" Datetime.jsont in\n Jsont.obj_finish o { text; created_at }\n \n val to_dag_cbor : t -\u003e Dag_cbor.value\n val of_dag_cbor : Dag_cbor.value -\u003e (t, error) result\nend\n```\n\n## CLI Tool\n\n```bash\natproto-lexicon-gen --input lexicons/ --output lib/generated/\n```\n\n## Dependencies\n- atproto-lexicon\n- jsont","acceptance_criteria":"- Generate OCaml types from Lexicon schemas\n- Generate encoders/decoders\n- Type-safe API bindings\n- CLI tool for code generation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:47.861552784+01:00","updated_at":"2025-12-28T11:28:03.226633204+01:00","closed_at":"2025-12-28T11:28:03.226633204+01:00","labels":["application","codegen"],"dependencies":[{"issue_id":"atproto-53","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:06.539440409+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-53","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:11.189125052+01:00","created_by":"daemon","metadata":"{}"}]}
30
+
{"id":"atproto-54","title":"Implement high-level API client","description":"Implement high-level API client for AT Protocol / Bluesky. This provides a user-friendly interface for common operations.","design":"## Module Structure\n\n```ocaml\n(* atproto-api/lib/agent.ml *)\ntype t\n\nval create : pds:Uri.t -\u003e t\n\n(* Authentication *)\nval login : t -\u003e identifier:string -\u003e password:string -\u003e (t, error) result\nval login_oauth : t -\u003e tokens:Oauth.tokens -\u003e t\nval refresh_session : t -\u003e (t, error) result\n\n(* Profile *)\nval get_profile : t -\u003e actor:string -\u003e (profile, error) result\nval update_profile : t -\u003e display_name:string option -\u003e ... -\u003e (unit, error) result\n\n(* Posts *)\nval create_post : t -\u003e text:string -\u003e ?reply:reply_ref -\u003e ... -\u003e (post_ref, error) result\nval delete_post : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Social *)\nval like : t -\u003e uri:At_uri.t -\u003e cid:Cid.t -\u003e (like_ref, error) result\nval follow : t -\u003e did:Did.t -\u003e (follow_ref, error) result\nval unfollow : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Feed *)\nval get_timeline : t -\u003e ?cursor:string -\u003e ?limit:int -\u003e (timeline, error) result\nval get_author_feed : t -\u003e actor:string -\u003e ... -\u003e (feed, error) result\n\n(* atproto-api/lib/richtext.ml *)\ntype t\n\nval create : string -\u003e t\nval detect_facets : t -\u003e t (* auto-detect mentions, links *)\nval add_mention : t -\u003e start:int -\u003e end_:int -\u003e did:Did.t -\u003e t\nval add_link : t -\u003e start:int -\u003e end_:int -\u003e uri:Uri.t -\u003e t\nval to_post_record : t -\u003e Dag_cbor.value\n```\n\n## Jsont Codecs for API Types\n\n```ocaml\nlet profile_jsont : profile Jsont.t =\n Jsont.obj \"profile\" @@ fun o -\u003e\n let did = Jsont.obj_mem o \"did\" did_jsont in\n let handle = Jsont.obj_mem o \"handle\" handle_jsont in\n let display_name = Jsont.obj_mem o \"displayName\" ~opt:true Jsont.string in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let avatar = Jsont.obj_mem o \"avatar\" ~opt:true Jsont.string in\n let followers_count = Jsont.obj_mem o \"followersCount\" ~opt:true Jsont.int in\n let follows_count = Jsont.obj_mem o \"followsCount\" ~opt:true Jsont.int in\n let posts_count = Jsont.obj_mem o \"postsCount\" ~opt:true Jsont.int in\n Jsont.obj_finish o { did; handle; display_name; description; avatar; \n followers_count; follows_count; posts_count }\n\nlet facet_jsont : facet Jsont.t =\n Jsont.obj \"facet\" @@ fun o -\u003e\n let index = Jsont.obj_mem o \"index\" byte_slice_jsont in\n let features = Jsont.obj_mem o \"features\" (Jsont.list facet_feature_jsont) in\n Jsont.obj_finish o { index; features }\n```\n\n## RichText Facets\n\n```json\n{\n \"text\": \"Hello @alice.bsky.social!\",\n \"facets\": [\n {\n \"index\": { \"byteStart\": 6, \"byteEnd\": 25 },\n \"features\": [\n { \"$type\": \"app.bsky.richtext.facet#mention\", \"did\": \"did:plc:...\" }\n ]\n }\n ]\n}\n```\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- atproto-repo\n- jsont","acceptance_criteria":"- Session management (login, logout, refresh)\n- Common operations (post, like, follow, etc.)\n- RichText handling (mentions, links, facets)\n- Timeline and feed fetching\n- Profile operations","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:00.736309435+01:00","updated_at":"2025-12-28T11:47:47.071271001+01:00","closed_at":"2025-12-28T11:47:47.071271001+01:00","labels":["api","application"],"dependencies":[{"issue_id":"atproto-54","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:07.636789403+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:12:12.376875324+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-33","type":"blocks","created_at":"2025-12-28T00:12:13.060557136+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:12:13.934360048+01:00","created_by":"daemon","metadata":"{}"}]}
31
+
{"id":"atproto-5l1","title":"Refactor JSON to simdjsont (replace yojson/jsont)","notes":"Migrated atproto-lexicon off Yojson onto simdjsont.Json.t + Simdjsont.decode Codec.value. Updated lib/lexicon/{parser,validator,atproto_lexicon,codegen} and lib/lexicon/dune. Updated test/lexicon/test_lexicon.ml fixtures loader + patterns. Verified: dune runtest test/lexicon OK; dune build @install OK.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T18:06:10.17746938+01:00","updated_at":"2026-01-01T20:42:14.454112697+01:00","closed_at":"2026-01-01T20:42:14.454112697+01:00","dependencies":[{"issue_id":"atproto-5l1","depends_on_id":"atproto-dqs","type":"blocks","created_at":"2026-01-01T18:06:39.648046004+01:00","created_by":"daemon","metadata":"{}"}]}
32
+
{"id":"atproto-60","title":"Implement effects-based I/O abstraction","description":"Implement the effects-based I/O abstraction layer that makes all libraries runtime-agnostic.","design":"## Module Structure\n\n```ocaml\n(* atproto-effects/lib/effects.ml *)\n\n(* HTTP effects *)\ntype http_request = {\n method_: [ `GET | `POST | `PUT | `DELETE ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\ntype http_response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\ntype _ Effect.t +=\n | Http_request : http_request -\u003e http_response Effect.t\n\n(* DNS effects *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Dns_a : string -\u003e string list Effect.t\n\n(* Time effects *)\ntype _ Effect.t +=\n | Now : Ptime.t Effect.t\n | Sleep : float -\u003e unit Effect.t\n\n(* Random effects *)\ntype _ Effect.t +=\n | Random_bytes : int -\u003e bytes Effect.t\n\n(* atproto-effects-eio/lib/handler.ml *)\nval run : (unit -\u003e 'a) -\u003e 'a\n```\n\n## Handler Example (eio)\n\n```ocaml\nlet run f =\n Effect.Deep.match_ f ()\n {\n retc = Fun.id;\n exnc = raise;\n effc = fun (type a) (e : a Effect.t) -\u003e\n match e with\n | Http_request req -\u003e\n Some (fun (k : (a, _) continuation) -\u003e\n let resp = Eio_client.request req in\n continue k resp)\n | Dns_txt domain -\u003e\n Some (fun k -\u003e\n let records = Eio_dns.txt domain in\n continue k records)\n | _ -\u003e None\n }\n```\n\n## Dependencies\n- eio (for testing handler)","acceptance_criteria":"- Effect types for HTTP, DNS, time, random\n- eio-based handler for testing\n- Handler composition utilities\n- Performance benchmarks","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:29.021401617+01:00","updated_at":"2025-12-28T11:57:08.264086142+01:00","closed_at":"2025-12-28T11:57:08.264086142+01:00","labels":["effects","infrastructure"],"dependencies":[{"issue_id":"atproto-60","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:55.467983208+01:00","created_by":"daemon","metadata":"{}"}]}
33
+
{"id":"atproto-61","title":"Set up interoperability test suite","description":"Set up and run the AT Protocol interoperability tests from bluesky-social/atproto-interop-tests.","design":"## Test Structure\n\n```\ntest/\nโโโ interop/\nโ โโโ syntax_test.ml # Handle, DID, NSID, TID, etc.\nโ โโโ crypto_test.ml # Signatures, did:key\nโ โโโ data_model_test.ml # DAG-CBOR, CID\nโ โโโ mst_test.ml # Key heights, tree structure\nโ โโโ lexicon_test.ml # Schema and record validation\nโ โโโ firehose_test.ml # Commit proofs\nโโโ fixtures/ # Cloned from atproto-interop-tests\nโโโ dune\n```\n\n## Test Approach\n\n1. Clone test vectors from GitHub\n2. Parse JSON fixtures using jsont\n3. Parse text fixtures line by line\n4. Run each test case\n5. Compare output to expected values\n\n## Example Test\n\n```ocaml\nlet load_json_fixtures path =\n let json = Jsont.of_file path in\n Jsont.decode (Jsont.list fixture_jsont) json\n\nlet%test \"handle_syntax_valid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_valid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e true\n | Error _ -\u003e false\n ) fixtures\n\nlet%test \"handle_syntax_invalid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_invalid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e false\n | Error _ -\u003e true\n ) fixtures\n\nlet%test \"crypto_signature_fixtures\" =\n let fixtures = load_json_fixtures \"fixtures/crypto/signature-fixtures.json\" in\n List.for_all (fun fixture -\u003e\n let message = Base64.decode fixture.message_base64 in\n let signature = Base64.decode fixture.signature_base64 in\n let key = Did_key.of_string fixture.public_key_did in\n let result = Crypto.verify key message signature in\n result = fixture.valid_signature\n ) fixtures\n```\n\n## Dependencies\n- alcotest or ounit2\n- jsont","acceptance_criteria":"- All syntax interop tests pass\n- All crypto interop tests pass\n- All data-model interop tests pass\n- All MST interop tests pass\n- All lexicon interop tests pass\n- All firehose interop tests pass","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:40.553908313+01:00","updated_at":"2025-12-28T13:25:34.614867702+01:00","closed_at":"2025-12-28T13:25:34.614867702+01:00","labels":["conformance","testing"],"dependencies":[{"issue_id":"atproto-61","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:56.180809368+01:00","created_by":"daemon","metadata":"{}"}]}
34
+
{"id":"atproto-62","title":"Set up monorepo package structure","description":"Set up the monorepo structure for multiple opam packages within a single repository.","design":"## Repository Structure\n\n```\natproto/\nโโโ dune-project # Root with all packages\nโโโ packages/\nโ โโโ atproto-syntax/\nโ โ โโโ lib/\nโ โ โ โโโ dune\nโ โ โ โโโ *.ml\nโ โ โโโ test/\nโ โ โ โโโ dune\nโ โ โ โโโ *_test.ml\nโ โ โโโ atproto-syntax.opam\nโ โโโ atproto-crypto/\nโ โโโ atproto-multibase/\nโ โโโ atproto-ipld/\nโ โโโ atproto-mst/\nโ โโโ atproto-repo/\nโ โโโ atproto-identity/\nโ โโโ atproto-xrpc/\nโ โโโ atproto-sync/\nโ โโโ atproto-lexicon/\nโ โโโ atproto-lexicon-gen/\nโ โโโ atproto-api/\nโ โโโ atproto-effects/\nโโโ examples/\nโ โโโ simple_client/\nโ โโโ firehose_consumer/\nโโโ interop-tests/\n```\n\n## dune-project\n\n```lisp\n(lang dune 3.20)\n(name atproto)\n(generate_opam_files true)\n\n(package\n (name atproto-syntax)\n (synopsis \"AT Protocol identifier syntax parsing\")\n (depends\n (ocaml (\u003e= 5.4))\n re\n ptime))\n\n(package\n (name atproto-crypto)\n ...)\n```\n\n## CI (.github/workflows/ci.yml)\n\n- OCaml 5.4 matrix\n- Build all packages\n- Run all tests\n- Run interop tests","acceptance_criteria":"- Multi-package dune-project structure\n- Separate opam files per package\n- CI pipeline for building and testing\n- Documentation generation setup","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:50.547102438+01:00","updated_at":"2025-12-28T11:57:18.856810633+01:00","closed_at":"2025-12-28T11:57:18.856810633+01:00","labels":["infrastructure","setup"],"dependencies":[{"issue_id":"atproto-62","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:57.015938611+01:00","created_by":"daemon","metadata":"{}"}]}
35
+
{"id":"atproto-8pf","title":"Migrate lib/api JSON parsing from yojson to simdjson","acceptance_criteria":"Build passes (dune build @install). lib/api no longer depends on yojson (lib/api/dune depends on simdjsont). All lib/api JSON encode/decode uses Atproto_xrpc.Client.json (Simdjsont.Json.t). API tests updated and passing (dune runtest test/api). No remaining yojson polymorphic variants in lib/api and test/api.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T20:55:46.43041578+01:00","updated_at":"2026-01-01T21:02:47.086579243+01:00","closed_at":"2026-01-01T21:02:47.086579243+01:00"}
36
+
{"id":"atproto-bsh","title":"Switch atproto-xrpc to use atproto-json wrapper instead of Simdjsont","status":"closed","priority":1,"issue_type":"task","assignee":"gdiazlo","created_at":"2026-01-01T21:08:22.228905638+01:00","updated_at":"2026-01-01T21:10:59.451211971+01:00","closed_at":"2026-01-01T21:10:59.451211971+01:00"}
35
37
{"id":"atproto-cir","title":"Implement DAG-JSON codec","description":"Create lib/ipld/dag_json.ml, encode Links as {\"/\": \"\u003ccid-string\u003e\"}, encode Bytes as {\"/\": {\"bytes\": \"\u003cbase64\u003e\"}}","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:30.578398468+01:00","updated_at":"2025-12-28T16:06:13.475583417+01:00","closed_at":"2025-12-28T16:06:13.475583417+01:00","labels":["compliance","ipld"]}
36
-
{"id":"atproto-dqs","title":"Fix baseline failing crypto tests (signature fixtures)","description":"","notes":"Follow-up: enforced mirage-crypto lower bounds to \u003e= 2.0.2 in dune-project (regenerated opam); pushed commit a6b9a13 to avoid P-256 fixture failures with older versions.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-01T18:06:34.897822579+01:00","updated_at":"2026-01-01T18:52:37.333901891+01:00","closed_at":"2026-01-01T18:52:37.333901891+01:00"}
38
+
{"id":"atproto-dqs","title":"Fix baseline failing crypto tests (signature fixtures)","notes":"Follow-up: enforced mirage-crypto lower bounds to \u003e= 2.0.2 in dune-project (regenerated opam); pushed commit a6b9a13 to avoid P-256 fixture failures with older versions.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-01T18:06:34.897822579+01:00","updated_at":"2026-01-01T18:52:37.333901891+01:00","closed_at":"2026-01-01T18:52:37.333901891+01:00"}
37
39
{"id":"atproto-ex-bot","title":"Implement Bluesky Bot Example (bsky_bot)","description":"Bot that can login, post with rich text, reply to mentions, follow back. Uses atproto-api Agent, Richtext, atproto-xrpc Client. Eio for I/O, Climate for CLI.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T09:07:59.407148786+01:00","updated_at":"2025-12-29T09:16:27.789416628+01:00","closed_at":"2025-12-29T09:16:27.789416628+01:00","labels":["api","bot","cli","example"]}
38
40
{"id":"atproto-ex-feed","title":"Implement Feed Generator Example (feed_generator)","description":"Custom feed algorithm with XRPC server serving app.bsky.feed.getFeedSkeleton. Subscribes to firehose, filters posts by keyword/language. Uses atproto-xrpc Server, atproto-sync Firehose.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T09:08:01.619973249+01:00","updated_at":"2025-12-29T09:18:53.434611454+01:00","closed_at":"2025-12-29T09:18:53.434611454+01:00","labels":["example","feed","firehose","server"]}
39
41
{"id":"atproto-ex-identity","title":"Implement Handle/DID Lookup Tool (identity_tool)","description":"CLI tool using Climate to resolve handles to DIDs, DIDs to documents, verify bidirectional identity links. Uses atproto-identity, atproto-syntax. Eio for I/O.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T09:07:57.316042741+01:00","updated_at":"2025-12-29T09:12:20.634271257+01:00","closed_at":"2025-12-29T09:12:20.634271257+01:00","labels":["cli","example","identity"]}
40
42
{"id":"atproto-ex-repo","title":"Implement Repository Inspector Example (repo_inspector)","description":"Tool to explore AT Protocol repositories: download CAR, parse MST structure, show records with CIDs, verify commit signatures. Uses atproto-repo, atproto-mst, atproto-ipld, atproto-crypto.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T09:08:03.622391028+01:00","updated_at":"2025-12-29T09:21:32.223029796+01:00","closed_at":"2025-12-29T09:21:32.223029796+01:00","labels":["cli","educational","example","repo"]}
41
43
{"id":"atproto-fw8","title":"Add filter option to firehose demo","description":"Add --filter option to firehose demo to filter events by type (posts, likes, follows, reposts, profiles, blocks, lists, etc.). This makes it easier to monitor specific activity types.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T21:08:20.29215305+01:00","updated_at":"2025-12-28T21:13:31.957432542+01:00","closed_at":"2025-12-28T21:13:31.957432542+01:00","labels":["demo","enhancement","firehose"]}
42
-
{"id":"atproto-h09","title":"Add package documentation","description":"Add documentation for each of the 11 AT Protocol packages including:\n- Module-level documentation with examples\n- README.md for the root project\n- CONTRIBUTING.md guide\n- API documentation generation with odoc","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T13:34:10.559554696+01:00","updated_at":"2025-12-28T13:50:20.509417248+01:00","closed_at":"2025-12-28T13:50:20.509417248+01:00","labels":["documentation"],"dependencies":[{"issue_id":"atproto-h09","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:16.081103184+01:00","created_by":"daemon"}]}
44
+
{"id":"atproto-h09","title":"Add package documentation","description":"Add documentation for each of the 11 AT Protocol packages including:\n- Module-level documentation with examples\n- README.md for the root project\n- CONTRIBUTING.md guide\n- API documentation generation with odoc","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T13:34:10.559554696+01:00","updated_at":"2025-12-28T13:50:20.509417248+01:00","closed_at":"2025-12-28T13:50:20.509417248+01:00","labels":["documentation"],"dependencies":[{"issue_id":"atproto-h09","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:16.081103184+01:00","created_by":"daemon","metadata":"{}"}]}
45
+
{"id":"atproto-i1c","title":"Migrate atproto-xrpc JSON from yojson to simdjsont","description":"Migrate all atproto modules to use Atproto_json exclusively. Remove all direct Simdjsont and Yojson usage.\n\n## Completed\n- Added Atproto_json shape helpers (to_object_opt, to_array_opt, to_string_opt, etc.)\n- Migrated lib/xrpc/{client,oauth,server}.ml to use Atproto_json only\n- Fixed lib/ipld/dag_json.ml to use Atproto_json helpers\n\n## Remaining lib/ files\n- lib/lexicon/parser.ml (heavy Simdjsont usage)\n- lib/lexicon/atproto_lexicon.ml (doc examples)\n- lib/crypto/jwt.ml (heavy Simdjsont usage)\n- lib/ipld/blob.ml (1 Simdjsont match)\n\n## Remaining test files\n- test/lexicon/test_lexicon.ml (Simdjsont)\n- test/xrpc/test_xrpc.ml (Simdjsont)\n- test/crypto/test_crypto.ml (Yojson)\n- test/ipld/test_ipld.ml (Yojson)\n- test/mst/test_mst.ml (Yojson)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T20:42:57.768582979+01:00","updated_at":"2026-01-01T22:40:10.309111402+01:00","closed_at":"2026-01-01T22:40:10.309111402+01:00"}
43
46
{"id":"atproto-kc7","title":"Verify/enforce CBOR strictness","description":"Check that CBOR library produces shortest-form encoding, add explicit rejection of indefinite-length items during decode","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:28.665226526+01:00","updated_at":"2025-12-28T16:02:55.549763384+01:00","closed_at":"2025-12-28T16:02:55.549763384+01:00","labels":["compliance","ipld"]}
44
47
{"id":"atproto-kn2","title":"Decode post content from CAR blocks in firehose","description":"Extract and display actual post text content by decoding the CAR blocks in commit events. Currently we only show the path/action, but the actual record data (post text, like subject, etc.) is in the blocks field.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T21:08:24.577853756+01:00","updated_at":"2025-12-28T21:17:19.863093228+01:00","closed_at":"2025-12-28T21:17:19.863093228+01:00","labels":["demo","enhancement","firehose"]}
45
-
{"id":"atproto-ona","title":"Bump mirage-crypto constraints to \u003e= 2.0.2","description":"","notes":"Acceptance criteria:\\n- Pin mirage-crypto* dependencies to \u003e= 2.0.2 in all relevant *.opam files (mirage-crypto, mirage-crypto-pk, etc as applicable)\\n- Ensure CI/local: all tests pass with: dune runtest\\n\\nContext:\\nCrypto tests only pass with mirage-crypto* 2.0.2; enforce lower-bound constraints.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-01T18:43:11.890905354+01:00","updated_at":"2026-01-01T18:43:57.093494274+01:00","closed_at":"2026-01-01T18:43:57.093494274+01:00"}
48
+
{"id":"atproto-ona","title":"Bump mirage-crypto constraints to \u003e= 2.0.2","notes":"Acceptance criteria:\\n- Pin mirage-crypto* dependencies to \u003e= 2.0.2 in all relevant *.opam files (mirage-crypto, mirage-crypto-pk, etc as applicable)\\n- Ensure CI/local: all tests pass with: dune runtest\\n\\nContext:\\nCrypto tests only pass with mirage-crypto* 2.0.2; enforce lower-bound constraints.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-01T18:43:11.890905354+01:00","updated_at":"2026-01-01T18:43:57.093494274+01:00","closed_at":"2026-01-01T18:43:57.093494274+01:00"}
46
49
{"id":"atproto-pg8","title":"Add MST example_keys.txt fixture tests","description":"Add tests using the example_keys.txt fixture file which contains 156 structured MST keys.\n\nTests should:\n1. Load all 156 keys from the fixture\n2. Build an MST containing all keys\n3. Verify all keys are retrievable\n4. Verify iteration order matches sorted key order\n5. Optionally verify tree structure properties","acceptance_criteria":"- example_keys.txt is loaded and all 156 keys are used\n- MST is built with all keys\n- All keys are retrievable after insertion\n- Iteration produces keys in sorted order","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T12:12:19.180139823+01:00","updated_at":"2025-12-28T12:43:14.192342391+01:00","closed_at":"2025-12-28T12:43:14.192342391+01:00","labels":["conformance","mst","testing"]}
47
50
{"id":"atproto-q0h","title":"Add firehose commit-proof-fixtures.json tests","description":"Add tests for the commit-proof-fixtures.json file which contains 6 test cases for MST proof verification:\n\n1. two deep split\n2. two deep leafless split\n3. add on edge with neighbor two layers down\n4. merge and split in multi-op commit\n5. complex multi-op commit\n6. split with earlier leaves on same layer\n\nEach fixture includes:\n- keys (existing keys in MST)\n- adds (keys to add)\n- dels (keys to delete)\n- rootBeforeCommit / rootAfterCommit (expected CIDs)\n- blocksInProof (CIDs of blocks needed for proof)\n\nThis tests the commit proof verification needed for firehose sync.","acceptance_criteria":"- All 6 commit-proof fixtures are tested\n- MST operations (add/delete) produce correct root CIDs\n- Proof blocks are correctly identified\n- Tests verify rootBeforeCommit and rootAfterCommit match","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:34.999268893+01:00","updated_at":"2025-12-28T12:58:39.408679225+01:00","closed_at":"2025-12-28T12:58:39.408679225+01:00","labels":["conformance","firehose","testing"]}
48
51
{"id":"atproto-s19","title":"Add CIDv0 parsing support","description":"Detect base58btc strings starting with \"Qm\", decode as multihash (sha2-256), implicit dag-pb codec","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:26.962896421+01:00","updated_at":"2025-12-28T15:58:38.151319563+01:00","closed_at":"2025-12-28T15:58:38.151319563+01:00","labels":["compliance","ipld"]}
49
52
{"id":"atproto-udz","title":"Add missing data-model conformance tests","description":"Add tests for data-model fixtures that are not currently covered:\n\n1. **data-model-valid.json** (5 entries) - Valid AT Protocol data model examples:\n - trivial record\n - float but integer-like (123.0)\n - empty list and object\n - list of nullable\n - list of lists\n\n2. **data-model-invalid.json** (12 entries) - Invalid examples that must be rejected:\n - top-level not an object\n - non-integer float\n - record with $type null/wrong type/empty\n - blob with string size/missing key\n - bytes with wrong field type/extra fields\n - link with wrong field type/bogus CID/extra fields","acceptance_criteria":"- test_data_model_valid() tests all 5 valid entries\n- test_data_model_invalid() tests all 12 invalid entries\n- Valid entries encode/decode correctly\n- Invalid entries are rejected with appropriate errors","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:14.579573063+01:00","updated_at":"2025-12-28T12:42:16.291981859+01:00","closed_at":"2025-12-28T12:42:16.291981859+01:00","labels":["conformance","ipld","testing"]}
50
-
{"id":"atproto-w5i","title":"Create example applications","description":"Create example applications demonstrating the AT Protocol libraries:\n1. Simple client - authenticate and make posts\n2. Firehose consumer - subscribe to real-time events\n3. Bot example - automated posting/interactions","notes":"Added firehose_demo example showing how to use the firehose module with OCaml 5 effects. Additional examples (client, bot) can be added in future iterations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T13:34:11.928213055+01:00","updated_at":"2025-12-28T13:36:39.963890666+01:00","closed_at":"2025-12-28T13:36:39.963890666+01:00","labels":["documentation","examples"],"dependencies":[{"issue_id":"atproto-w5i","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:17.10878+01:00","created_by":"daemon"}]}
53
+
{"id":"atproto-w30","title":"Migrate test fixture loading from Yojson to Atproto_json","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T22:43:58.821369697+01:00","updated_at":"2026-01-01T22:53:56.562250588+01:00","closed_at":"2026-01-01T22:53:56.562250588+01:00"}
54
+
{"id":"atproto-w5i","title":"Create example applications","description":"Create example applications demonstrating the AT Protocol libraries:\n1. Simple client - authenticate and make posts\n2. Firehose consumer - subscribe to real-time events\n3. Bot example - automated posting/interactions","notes":"Added firehose_demo example showing how to use the firehose module with OCaml 5 effects. Additional examples (client, bot) can be added in future iterations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T13:34:11.928213055+01:00","updated_at":"2025-12-28T13:36:39.963890666+01:00","closed_at":"2025-12-28T13:36:39.963890666+01:00","labels":["documentation","examples"],"dependencies":[{"issue_id":"atproto-w5i","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:17.10878+01:00","created_by":"daemon","metadata":"{}"}]}
51
55
{"id":"atproto-xfg","title":"Add Float support to DAG-CBOR","description":"Add Float of float variant to value type, encode as 64-bit double (CBOR major type 7, additional info 27), reject NaN and Infinity values, keep AT Protocol mode that rejects all floats","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-28T15:47:24.836568239+01:00","updated_at":"2025-12-28T15:53:03.229484165+01:00","closed_at":"2025-12-28T15:53:03.229484165+01:00","labels":["compliance","ipld"]}
52
56
{"id":"atproto-y4h","title":"Add JSON output mode to firehose demo","description":"Add --json flag to output events as JSON lines (JSONL format) instead of human-readable format. This enables piping to jq or other tools for further processing.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-28T21:08:22.085375231+01:00","updated_at":"2025-12-28T21:13:33.122865944+01:00","closed_at":"2025-12-28T21:13:33.122865944+01:00","labels":["demo","enhancement","firehose"]}
+19
-2
.gitattributes
+19
-2
.gitattributes
···
1
+
# Beads merge driver
2
+
.beads/issues.jsonl merge=beads
1
3
2
-
# Use bd merge for beads JSONL files
3
-
.beads/issues.jsonl merge=beads
4
+
# Exclude from git archive (used by opam-publish)
5
+
examples/ export-ignore
6
+
bin/ export-ignore
7
+
test/ export-ignore
8
+
.beads/ export-ignore
9
+
.github/ export-ignore
10
+
.vscode/ export-ignore
11
+
.idea/ export-ignore
12
+
.opencode/ export-ignore
13
+
.ocamlformat export-ignore
14
+
opencode.json export-ignore
15
+
CONTRIBUTING.md export-ignore
16
+
COMPLIANCE.md export-ignore
17
+
compliance-report.html export-ignore
18
+
compliance-report.json export-ignore
19
+
lexicons/ export-ignore
20
+
*.install export-ignore
+1
-1
COMPLIANCE.md
+1
-1
COMPLIANCE.md
+15
LICENSE
+15
LICENSE
···
1
+
ISC License
2
+
3
+
Copyright (c) 2026 Gabriel Dรญaz Lรณpez de la Llave
4
+
5
+
Permission to use, copy, modify, and/or distribute this software for any
6
+
purpose with or without fee is hereby granted, provided that the above
7
+
copyright notice and this permission notice appear in all copies.
8
+
9
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+
PERFORMANCE OF THIS SOFTWARE.
+1
-1
README.md
+1
-1
README.md
+10
-9
atproto-api.opam
+10
-9
atproto-api.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "High-level API client for AT Protocol"
4
5
description:
5
6
"User-friendly API client for AT Protocol with session management, posting, and social actions"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "bluesky" "api" "client"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-syntax" {= version}
17
18
"atproto-xrpc" {= version}
18
19
"atproto-identity" {= version}
19
20
"atproto-ipld" {= version}
20
-
"yojson" {>= "2.0"}
21
+
"atproto-json" {= version}
21
22
"uri" {>= "4.0"}
22
23
"alcotest" {with-test}
23
24
"odoc" {with-doc}
···
36
37
"@doc" {with-doc}
37
38
]
38
39
]
39
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
40
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
40
41
x-maintenance-intent: ["(latest)"]
+10
-9
atproto-crypto.opam
+10
-9
atproto-crypto.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Cryptographic operations for AT Protocol"
4
5
description:
5
6
"P-256 and K-256 elliptic curve support with low-S normalization, did:key encoding"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "crypto" "ecdsa"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-multibase" {= version}
18
+
"atproto-json" {= version}
17
19
"mirage-crypto-ec" {>= "2.0.2"}
18
20
"mirage-crypto-rng" {>= "2.0.2"}
19
21
"digestif" {>= "1.0"}
20
22
"zarith" {>= "1.12"}
21
23
"alcotest" {with-test}
22
-
"yojson" {with-test}
23
24
"odoc" {with-doc}
24
25
]
25
26
build: [
···
36
37
"@doc" {with-doc}
37
38
]
38
39
]
39
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
40
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
40
41
x-maintenance-intent: ["(latest)"]
+9
-8
atproto-effects.opam
+9
-8
atproto-effects.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Effects-based I/O abstraction for AT Protocol"
4
5
description:
5
6
"Unified effect types for HTTP, DNS, WebSocket, time, and random operations. Allows libraries to be runtime-agnostic."
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "effects" "io" "abstraction"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"uri" {>= "4.0"}
17
18
"ptime" {>= "1.0"}
18
19
"alcotest" {with-test}
···
32
33
"@doc" {with-doc}
33
34
]
34
35
]
35
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
36
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
36
37
x-maintenance-intent: ["(latest)"]
+10
-9
atproto-identity.opam
+10
-9
atproto-identity.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "DID and Handle resolution for AT Protocol"
4
5
description:
5
6
"DID and Handle resolution including did:plc, did:web, and DNS/HTTPS handle resolution"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "identity" "did" "handle" "resolution"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-effects" {= version}
17
18
"atproto-syntax" {= version}
18
19
"atproto-crypto" {= version}
19
-
"yojson" {>= "2.0"}
20
+
"atproto-json" {= version}
20
21
"uri" {>= "4.0"}
21
22
"alcotest" {with-test}
22
23
"odoc" {with-doc}
···
35
36
"@doc" {with-doc}
36
37
]
37
38
]
38
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
39
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
39
40
x-maintenance-intent: ["(latest)"]
+10
-9
atproto-ipld.opam
+10
-9
atproto-ipld.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "IPLD support for AT Protocol"
4
5
description:
5
6
"Content Identifiers (CID) and DAG-CBOR encoding for AT Protocol"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "ipld" "cid" "dag-cbor"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-multibase" {= version}
18
+
"atproto-json" {= version}
17
19
"digestif" {>= "1.0"}
18
20
"zarith" {>= "1.12"}
19
21
"cbor" {>= "0.5"}
20
22
"base64" {>= "3.5"}
21
23
"alcotest" {with-test}
22
-
"yojson" {with-test}
23
24
"odoc" {with-doc}
24
25
]
25
26
build: [
···
36
37
"@doc" {with-doc}
37
38
]
38
39
]
39
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
40
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
40
41
x-maintenance-intent: ["(latest)"]
+36
atproto-json.opam
+36
atproto-json.opam
···
1
+
# This file is generated by dune, edit dune-project instead
2
+
opam-version: "2.0"
3
+
version: "0.1.2"
4
+
synopsis: "JSON utilities for AT Protocol"
5
+
description:
6
+
"JSON wrapper used across AT Protocol packages (currently backed by simdjsont)"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
10
+
tags: ["atproto" "json" "simdjson"]
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
14
+
depends: [
15
+
"dune" {>= "3.20"}
16
+
"ocaml" {>= "5.4"}
17
+
"simdjsont" {>= "0.1.0"}
18
+
"alcotest" {with-test}
19
+
"odoc" {with-doc}
20
+
]
21
+
build: [
22
+
["dune" "subst"] {dev}
23
+
[
24
+
"dune"
25
+
"build"
26
+
"-p"
27
+
name
28
+
"-j"
29
+
jobs
30
+
"@install"
31
+
"@runtest" {with-test}
32
+
"@doc" {with-doc}
33
+
]
34
+
]
35
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
36
+
x-maintenance-intent: ["(latest)"]
+10
-9
atproto-lexicon.opam
+10
-9
atproto-lexicon.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Lexicon schema support for AT Protocol"
4
5
description: "Lexicon schema parsing and validation for AT Protocol"
5
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
6
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
license: "MIT"
6
+
maintainer: ["Gabriel Dรญaz"]
7
+
authors: ["Gabriel Dรญaz"]
8
+
license: "ISC"
8
9
tags: ["atproto" "lexicon" "schema"]
9
-
homepage: "https://github.com/gdiazlo/atproto"
10
-
doc: "https://github.com/gdiazlo/atproto"
11
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
10
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
11
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
12
13
depends: [
13
14
"dune" {>= "3.20"}
14
-
"ocaml" {>= "5.1"}
15
+
"ocaml" {>= "5.4"}
15
16
"atproto-syntax" {= version}
16
-
"yojson" {>= "2.0"}
17
+
"atproto-json" {= version}
17
18
"alcotest" {with-test}
18
19
"odoc" {with-doc}
19
20
]
···
31
32
"@doc" {with-doc}
32
33
]
33
34
]
34
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
35
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
35
36
x-maintenance-intent: ["(latest)"]
+9
-9
atproto-mst.opam
+9
-9
atproto-mst.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Merkle Search Tree for AT Protocol"
4
5
description:
5
6
"Content-addressed key-value storage for AT Protocol repositories"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "mst" "merkle" "repository"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-ipld" {= version}
17
18
"digestif" {>= "1.0"}
18
19
"alcotest" {with-test}
19
-
"yojson" {with-test}
20
20
"odoc" {with-doc}
21
21
]
22
22
build: [
···
33
33
"@doc" {with-doc}
34
34
]
35
35
]
36
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
36
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
37
37
x-maintenance-intent: ["(latest)"]
+9
-8
atproto-multibase.opam
+9
-8
atproto-multibase.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Base encoding utilities for AT Protocol"
4
5
description:
5
6
"Multibase encoding/decoding including base32-sortable for TIDs and base58btc for did:key"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "encoding" "multibase" "base32" "base58"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"alcotest" {with-test}
17
18
"odoc" {with-doc}
18
19
]
···
30
31
"@doc" {with-doc}
31
32
]
32
33
]
33
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
34
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
34
35
x-maintenance-intent: ["(latest)"]
+9
-9
atproto-repo.opam
+9
-9
atproto-repo.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Repository support for AT Protocol"
4
5
description:
5
6
"Repository structure, commits, and record operations for AT Protocol"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "repository" "commit" "signing"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-syntax" {= version}
17
18
"atproto-crypto" {= version}
18
19
"atproto-ipld" {= version}
19
20
"atproto-mst" {= version}
20
21
"digestif" {>= "1.0"}
21
22
"alcotest" {with-test}
22
-
"yojson" {with-test}
23
23
"odoc" {with-doc}
24
24
]
25
25
build: [
···
36
36
"@doc" {with-doc}
37
37
]
38
38
]
39
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
39
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
40
40
x-maintenance-intent: ["(latest)"]
+9
-8
atproto-sync.opam
+9
-8
atproto-sync.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Repository sync and event streams for AT Protocol"
4
5
description:
5
6
"Firehose event stream client and repository synchronization for AT Protocol"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "sync" "firehose" "websocket"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-effects" {= version}
17
18
"atproto-syntax" {= version}
18
19
"atproto-ipld" {= version}
···
34
35
"@doc" {with-doc}
35
36
]
36
37
]
37
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
38
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
38
39
x-maintenance-intent: ["(latest)"]
+9
-8
atproto-syntax.opam
+9
-8
atproto-syntax.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "Syntax validation for AT Protocol identifiers"
4
5
description:
5
6
"Parser-based validation for handles, DIDs, NSIDs, TIDs, AT-URIs, and other AT Protocol syntax"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "syntax" "parser" "validation"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-multibase" {= version}
17
18
"alcotest" {with-test}
18
19
"odoc" {with-doc}
···
31
32
"@doc" {with-doc}
32
33
]
33
34
]
34
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
35
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
35
36
x-maintenance-intent: ["(latest)"]
+10
-9
atproto-xrpc.opam
+10
-9
atproto-xrpc.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "XRPC client/server for AT Protocol"
4
5
description:
5
6
"XRPC HTTP API protocol implementation for AT Protocol client-server communication"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "xrpc" "api" "http"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
16
17
"atproto-effects" {= version}
17
18
"atproto-syntax" {= version}
18
19
"atproto-lexicon" {= version}
19
-
"yojson" {>= "2.0"}
20
+
"atproto-json" {= version}
20
21
"uri" {>= "4.0"}
21
22
"alcotest" {with-test}
22
23
"odoc" {with-doc}
···
35
36
"@doc" {with-doc}
36
37
]
37
38
]
38
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
39
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
39
40
x-maintenance-intent: ["(latest)"]
+19
-9
atproto.opam
+19
-9
atproto.opam
···
1
1
# This file is generated by dune, edit dune-project instead
2
2
opam-version: "2.0"
3
+
version: "0.1.2"
3
4
synopsis: "AT Protocol implementation in OCaml"
4
5
description:
5
6
"Complete AT Protocol implementation including syntax validation, cryptography, IPLD, and identity resolution"
6
-
maintainer: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
7
-
authors: ["Guillermo Diaz-Romero <guillermo@bluesky-dev.io>"]
8
-
license: "MIT"
7
+
maintainer: ["Gabriel Dรญaz"]
8
+
authors: ["Gabriel Dรญaz"]
9
+
license: "ISC"
9
10
tags: ["atproto" "bluesky" "decentralized"]
10
-
homepage: "https://github.com/gdiazlo/atproto"
11
-
doc: "https://github.com/gdiazlo/atproto"
12
-
bug-reports: "https://github.com/gdiazlo/atproto/issues"
11
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/atproto"
12
+
doc: "https://tangled.org/gdiazlo.tngl.sh/atproto"
13
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/atproto/issues"
13
14
depends: [
14
15
"dune" {>= "3.20"}
15
-
"ocaml" {>= "5.1"}
16
+
"ocaml" {>= "5.4"}
17
+
"atproto-multibase" {= version}
16
18
"atproto-syntax" {= version}
17
19
"atproto-crypto" {= version}
18
-
"atproto-multibase" {= version}
19
20
"atproto-ipld" {= version}
21
+
"atproto-mst" {= version}
22
+
"atproto-repo" {= version}
23
+
"atproto-json" {= version}
24
+
"atproto-lexicon" {= version}
25
+
"atproto-effects" {= version}
26
+
"atproto-xrpc" {= version}
27
+
"atproto-identity" {= version}
28
+
"atproto-sync" {= version}
29
+
"atproto-api" {= version}
20
30
"odoc" {with-doc}
21
31
]
22
32
build: [
···
33
43
"@doc" {with-doc}
34
44
]
35
45
]
36
-
dev-repo: "git+https://github.com/gdiazlo/atproto.git"
46
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto"
37
47
x-maintenance-intent: ["(latest)"]
-2
bin/dune
-2
bin/dune
+1
-1
compliance-report.html
+1
-1
compliance-report.html
···
129
129
</table>
130
130
</div>
131
131
<div class="meta">
132
-
<p>Generated: 2025-12-28T15:06:07Z</p>
132
+
<p>Generated: 2026-01-01T22:04:38Z</p>
133
133
<p>Repository: <a href="https://github.com/gdiazlo/atproto">https://github.com/gdiazlo/atproto</a></p>
134
134
<p>Test fixtures from <a href="https://github.com/bluesky-social/atproto-interop-tests">AT Protocol Interoperability Tests</a></p>
135
135
</div>
+21
-21
compliance-report.json
+21
-21
compliance-report.json
···
1
1
{
2
2
"title": "AT Protocol Compliance Report",
3
3
"version": "1.0.0",
4
-
"generated_at": "2025-12-28T15:06:07Z",
4
+
"generated_at": "2026-01-01T22:04:38Z",
5
5
"repository": "https://github.com/gdiazlo/atproto",
6
6
"total_tests": 494,
7
7
"total_passed": 494,
8
8
"total_failed": 0,
9
-
"pass_rate": 100.0,
9
+
"pass_rate": 100,
10
10
"suites": [
11
11
{
12
12
"name": "Syntax Validation",
···
14
14
"total": 448,
15
15
"passed": 448,
16
16
"failed": 0,
17
-
"pass_rate": 100.0,
17
+
"pass_rate": 100,
18
18
"categories": [
19
19
{
20
20
"name": "Handle",
···
23
23
"total": 119,
24
24
"passed": 119,
25
25
"failed": 0,
26
-
"pass_rate": 100.0,
26
+
"pass_rate": 100,
27
27
"results": [
28
28
{
29
29
"input": "A.ISI.EDU",
···
867
867
"total": 42,
868
868
"passed": 42,
869
869
"failed": 0,
870
-
"pass_rate": 100.0,
870
+
"pass_rate": 100,
871
871
"results": [
872
872
{
873
873
"input": "did:method:val",
···
1172
1172
"total": 52,
1173
1173
"passed": 52,
1174
1174
"failed": 0,
1175
-
"pass_rate": 100.0,
1175
+
"pass_rate": 100,
1176
1176
"results": [
1177
1177
{
1178
1178
"input": "com.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo",
···
1547
1547
"total": 13,
1548
1548
"passed": 13,
1549
1549
"failed": 0,
1550
-
"pass_rate": 100.0,
1550
+
"pass_rate": 100,
1551
1551
"results": [
1552
1552
{
1553
1553
"input": "3jzfcijpj2z2a",
···
1649
1649
"total": 27,
1650
1650
"passed": 27,
1651
1651
"failed": 0,
1652
-
"pass_rate": 100.0,
1652
+
"pass_rate": 100,
1653
1653
"results": [
1654
1654
{
1655
1655
"input": "self",
···
1849
1849
"total": 95,
1850
1850
"passed": 95,
1851
1851
"failed": 0,
1852
-
"pass_rate": 100.0,
1852
+
"pass_rate": 100,
1853
1853
"results": [
1854
1854
{
1855
1855
"input": "at://did:plc:asdf123",
···
2525
2525
"total": 79,
2526
2526
"passed": 79,
2527
2527
"failed": 0,
2528
-
"pass_rate": 100.0,
2528
+
"pass_rate": 100,
2529
2529
"results": [
2530
2530
{
2531
2531
"input": "1985-04-12T23:20:50.123Z",
···
3089
3089
"total": 21,
3090
3090
"passed": 21,
3091
3091
"failed": 0,
3092
-
"pass_rate": 100.0,
3092
+
"pass_rate": 100,
3093
3093
"results": [
3094
3094
{
3095
3095
"input": "ja",
···
3248
3248
"total": 12,
3249
3249
"passed": 12,
3250
3250
"failed": 0,
3251
-
"pass_rate": 100.0,
3251
+
"pass_rate": 100,
3252
3252
"categories": [
3253
3253
{
3254
3254
"name": "Signature Verification",
···
3257
3257
"total": 6,
3258
3258
"passed": 6,
3259
3259
"failed": 0,
3260
-
"pass_rate": 100.0,
3260
+
"pass_rate": 100,
3261
3261
"results": [
3262
3262
{
3263
3263
"input": "valid P-256 key and signature, with low-S signature",
···
3310
3310
"total": 1,
3311
3311
"passed": 1,
3312
3312
"failed": 0,
3313
-
"pass_rate": 100.0,
3313
+
"pass_rate": 100,
3314
3314
"results": [
3315
3315
{
3316
3316
"input": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
···
3328
3328
"total": 5,
3329
3329
"passed": 5,
3330
3330
"failed": 0,
3331
-
"pass_rate": 100.0,
3331
+
"pass_rate": 100,
3332
3332
"results": [
3333
3333
{
3334
3334
"input": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
···
3375
3375
"total": 21,
3376
3376
"passed": 21,
3377
3377
"failed": 0,
3378
-
"pass_rate": 100.0,
3378
+
"pass_rate": 100,
3379
3379
"categories": [
3380
3380
{
3381
3381
"name": "DAG-CBOR/CID",
···
3384
3384
"total": 3,
3385
3385
"passed": 3,
3386
3386
"failed": 0,
3387
-
"pass_rate": 100.0,
3387
+
"pass_rate": 100,
3388
3388
"results": [
3389
3389
{
3390
3390
"input": "fixture[0]",
···
3416
3416
"total": 18,
3417
3417
"passed": 18,
3418
3418
"failed": 0,
3419
-
"pass_rate": 100.0,
3419
+
"pass_rate": 100,
3420
3420
"results": [
3421
3421
{
3422
3422
"input": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
···
3554
3554
"total": 13,
3555
3555
"passed": 13,
3556
3556
"failed": 0,
3557
-
"pass_rate": 100.0,
3557
+
"pass_rate": 100,
3558
3558
"categories": [
3559
3559
{
3560
3560
"name": "Key Heights",
···
3563
3563
"total": 0,
3564
3564
"passed": 0,
3565
3565
"failed": 0,
3566
-
"pass_rate": 0.0,
3566
+
"pass_rate": 0,
3567
3567
"results": []
3568
3568
},
3569
3569
{
···
3573
3573
"total": 13,
3574
3574
"passed": 13,
3575
3575
"failed": 0,
3576
-
"pass_rate": 100.0,
3576
+
"pass_rate": 100,
3577
3577
"results": [
3578
3578
{
3579
3579
"input": "prefix[0]: , ",
+60
-38
dune-project
+60
-38
dune-project
···
2
2
3
3
(name atproto)
4
4
5
+
(version 0.1.2)
6
+
5
7
(generate_opam_files true)
6
8
7
9
(source
8
-
(github gdiazlo/atproto))
10
+
; (tangled @gdiazlo.tngl.sh/atproto)
11
+
(uri git+https://tangled.org/gdiazlo.tngl.sh/atproto))
9
12
10
-
(authors "Guillermo Diaz-Romero <guillermo@bluesky-dev.io>")
13
+
(authors "Gabriel Dรญaz")
11
14
12
-
(maintainers "Guillermo Diaz-Romero <guillermo@bluesky-dev.io>")
15
+
(maintainers "Gabriel Dรญaz")
13
16
14
-
(license MIT)
15
-
16
-
(documentation https://github.com/gdiazlo/atproto)
17
+
(license ISC)
18
+
(homepage https://tangled.org/gdiazlo.tngl.sh/atproto)
19
+
(bug_reports https://tangled.org/gdiazlo.tngl.sh/atproto/issues)
20
+
(documentation https://tangled.org/gdiazlo.tngl.sh/atproto)
17
21
18
22
; Foundation packages
19
23
(package
···
21
25
(synopsis "Base encoding utilities for AT Protocol")
22
26
(description "Multibase encoding/decoding including base32-sortable for TIDs and base58btc for did:key")
23
27
(depends
24
-
(ocaml (>= 5.1))
28
+
(ocaml (>= 5.4))
25
29
(alcotest :with-test))
26
30
(tags (atproto encoding multibase base32 base58)))
27
31
···
30
34
(synopsis "Syntax validation for AT Protocol identifiers")
31
35
(description "Parser-based validation for handles, DIDs, NSIDs, TIDs, AT-URIs, and other AT Protocol syntax")
32
36
(depends
33
-
(ocaml (>= 5.1))
37
+
(ocaml (>= 5.4))
34
38
(atproto-multibase (= :version))
35
39
(alcotest :with-test))
36
40
(tags (atproto syntax parser validation)))
···
40
44
(synopsis "Cryptographic operations for AT Protocol")
41
45
(description "P-256 and K-256 elliptic curve support with low-S normalization, did:key encoding")
42
46
(depends
43
-
(ocaml (>= 5.1))
47
+
(ocaml (>= 5.4))
44
48
(atproto-multibase (= :version))
49
+
(atproto-json (= :version))
45
50
(mirage-crypto-ec (>= 2.0.2))
46
51
(mirage-crypto-rng (>= 2.0.2))
47
52
(digestif (>= 1.0))
48
53
(zarith (>= 1.12))
49
-
(alcotest :with-test)
50
-
(yojson :with-test))
54
+
(alcotest :with-test))
51
55
(tags (atproto crypto ecdsa)))
52
56
53
57
; Data layer packages
···
56
60
(synopsis "IPLD support for AT Protocol")
57
61
(description "Content Identifiers (CID) and DAG-CBOR encoding for AT Protocol")
58
62
(depends
59
-
(ocaml (>= 5.1))
63
+
(ocaml (>= 5.4))
60
64
(atproto-multibase (= :version))
65
+
(atproto-json (= :version))
61
66
(digestif (>= 1.0))
62
67
(zarith (>= 1.12))
63
68
(cbor (>= 0.5))
64
69
(base64 (>= 3.5))
65
-
(alcotest :with-test)
66
-
(yojson :with-test))
70
+
(alcotest :with-test))
67
71
(tags (atproto ipld cid dag-cbor)))
68
72
69
73
(package
···
71
75
(synopsis "Merkle Search Tree for AT Protocol")
72
76
(description "Content-addressed key-value storage for AT Protocol repositories")
73
77
(depends
74
-
(ocaml (>= 5.1))
78
+
(ocaml (>= 5.4))
75
79
(atproto-ipld (= :version))
76
80
(digestif (>= 1.0))
77
-
(alcotest :with-test)
78
-
(yojson :with-test))
81
+
(alcotest :with-test))
79
82
(tags (atproto mst merkle repository)))
80
83
81
84
(package
···
83
86
(synopsis "Repository support for AT Protocol")
84
87
(description "Repository structure, commits, and record operations for AT Protocol")
85
88
(depends
86
-
(ocaml (>= 5.1))
89
+
(ocaml (>= 5.4))
87
90
(atproto-syntax (= :version))
88
91
(atproto-crypto (= :version))
89
92
(atproto-ipld (= :version))
90
93
(atproto-mst (= :version))
91
94
(digestif (>= 1.0))
92
-
(alcotest :with-test)
93
-
(yojson :with-test))
95
+
(alcotest :with-test))
94
96
(tags (atproto repository commit signing)))
95
97
96
98
(package
···
98
100
(synopsis "Lexicon schema support for AT Protocol")
99
101
(description "Lexicon schema parsing and validation for AT Protocol")
100
102
(depends
101
-
(ocaml (>= 5.1))
103
+
(ocaml (>= 5.4))
102
104
(atproto-syntax (= :version))
103
-
(yojson (>= 2.0))
105
+
(atproto-json (= :version))
104
106
(alcotest :with-test))
105
107
(tags (atproto lexicon schema)))
106
108
107
109
; Network layer packages
108
110
(package
111
+
(name atproto-json)
112
+
(synopsis "JSON utilities for AT Protocol")
113
+
(description "JSON wrapper used across AT Protocol packages (currently backed by simdjsont)")
114
+
(depends
115
+
(ocaml (>= 5.4))
116
+
(simdjsont (>= 0.1.0))
117
+
(alcotest :with-test))
118
+
(tags (atproto json simdjson)))
119
+
120
+
121
+
(package
109
122
(name atproto-xrpc)
110
123
(synopsis "XRPC client/server for AT Protocol")
111
124
(description "XRPC HTTP API protocol implementation for AT Protocol client-server communication")
112
-
(depends
113
-
(ocaml (>= 5.1))
114
-
(atproto-effects (= :version))
115
-
(atproto-syntax (= :version))
116
-
(atproto-lexicon (= :version))
117
-
(yojson (>= 2.0))
118
-
(uri (>= 4.0))
119
-
(alcotest :with-test))
125
+
(depends
126
+
(ocaml (>= 5.4))
127
+
(atproto-effects (= :version))
128
+
(atproto-syntax (= :version))
129
+
(atproto-lexicon (= :version))
130
+
(atproto-json (= :version))
131
+
(uri (>= 4.0))
132
+
(alcotest :with-test))
120
133
(tags (atproto xrpc api http)))
121
134
122
135
; Identity layer packages
···
125
138
(synopsis "DID and Handle resolution for AT Protocol")
126
139
(description "DID and Handle resolution including did:plc, did:web, and DNS/HTTPS handle resolution")
127
140
(depends
128
-
(ocaml (>= 5.1))
141
+
(ocaml (>= 5.4))
129
142
(atproto-effects (= :version))
130
143
(atproto-syntax (= :version))
131
144
(atproto-crypto (= :version))
132
-
(yojson (>= 2.0))
145
+
(atproto-json (= :version))
133
146
(uri (>= 4.0))
134
147
(alcotest :with-test))
135
148
(tags (atproto identity did handle resolution)))
···
140
153
(synopsis "Repository sync and event streams for AT Protocol")
141
154
(description "Firehose event stream client and repository synchronization for AT Protocol")
142
155
(depends
143
-
(ocaml (>= 5.1))
156
+
(ocaml (>= 5.4))
144
157
(atproto-effects (= :version))
145
158
(atproto-syntax (= :version))
146
159
(atproto-ipld (= :version))
···
154
167
(synopsis "High-level API client for AT Protocol")
155
168
(description "User-friendly API client for AT Protocol with session management, posting, and social actions")
156
169
(depends
157
-
(ocaml (>= 5.1))
170
+
(ocaml (>= 5.4))
158
171
(atproto-syntax (= :version))
159
172
(atproto-xrpc (= :version))
160
173
(atproto-identity (= :version))
161
174
(atproto-ipld (= :version))
162
-
(yojson (>= 2.0))
175
+
(atproto-json (= :version))
163
176
(uri (>= 4.0))
164
177
(alcotest :with-test))
165
178
(tags (atproto bluesky api client)))
···
170
183
(synopsis "Effects-based I/O abstraction for AT Protocol")
171
184
(description "Unified effect types for HTTP, DNS, WebSocket, time, and random operations. Allows libraries to be runtime-agnostic.")
172
185
(depends
173
-
(ocaml (>= 5.1))
186
+
(ocaml (>= 5.4))
174
187
(uri (>= 4.0))
175
188
(ptime (>= 1.0))
176
189
(alcotest :with-test))
···
182
195
(synopsis "AT Protocol implementation in OCaml")
183
196
(description "Complete AT Protocol implementation including syntax validation, cryptography, IPLD, and identity resolution")
184
197
(depends
185
-
(ocaml (>= 5.1))
198
+
(ocaml (>= 5.4))
199
+
(atproto-multibase (= :version))
186
200
(atproto-syntax (= :version))
187
201
(atproto-crypto (= :version))
188
-
(atproto-multibase (= :version))
189
202
(atproto-ipld (= :version))
203
+
(atproto-mst (= :version))
204
+
(atproto-repo (= :version))
205
+
(atproto-json (= :version))
206
+
(atproto-lexicon (= :version))
207
+
(atproto-effects (= :version))
208
+
(atproto-xrpc (= :version))
209
+
(atproto-identity (= :version))
210
+
(atproto-sync (= :version))
211
+
(atproto-api (= :version))
190
212
(odoc :with-doc))
191
213
(tags (atproto bluesky decentralized)))
+62
-77
examples/bsky_bot/bsky_bot.ml
+62
-77
examples/bsky_bot/bsky_bot.ml
···
5
5
module Richtext = Atproto_api.Richtext
6
6
module Client = Atproto_xrpc.Client
7
7
8
-
(** {1 HTTP Client with cohttp-eio} *)
9
-
10
-
let http_request ~sw ~client (req : Client.request) : Client.response =
11
-
let headers = Cohttp.Header.of_list req.headers in
12
-
let body =
13
-
match req.body with
14
-
| Some b -> Cohttp_eio.Body.of_string b
15
-
| None -> Cohttp_eio.Body.of_string ""
8
+
let http_request ~hcs_client (req : Client.request) : Client.response =
9
+
let url = Uri.to_string req.uri in
10
+
let result =
11
+
match req.meth with
12
+
| `GET -> Hcs.Client.request hcs_client url
13
+
| `POST ->
14
+
let body = Option.value ~default:"" req.body in
15
+
Hcs.Client.request_post hcs_client url ~body
16
16
in
17
-
let meth = match req.meth with `GET -> `GET | `POST -> `POST in
18
-
try
19
-
let resp, resp_body =
20
-
Cohttp_eio.Client.call ~sw client meth req.uri ~headers ~body
21
-
in
22
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
23
-
let headers = Cohttp.Response.headers resp |> Cohttp.Header.to_list in
24
-
let body =
25
-
Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all)
26
-
in
27
-
{ Client.status; headers; body }
28
-
with e -> { Client.status = 0; headers = []; body = Printexc.to_string e }
29
-
30
-
(** {1 Effect Handler} *)
17
+
match result with
18
+
| Ok resp ->
19
+
{ Client.status = resp.status; headers = resp.headers; body = resp.body }
20
+
| Error e ->
21
+
let msg =
22
+
match e with
23
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
24
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
25
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
26
+
| Hcs.Client.Timeout -> "Timeout"
27
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
28
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
29
+
in
30
+
{ Client.status = 0; headers = []; body = msg }
31
31
32
-
let run_with_eio ~sw ~client f =
32
+
let run_with_hcs ~hcs_client f =
33
33
Effect.Deep.try_with f ()
34
34
{
35
35
effc =
···
38
38
| Client.Http_request req ->
39
39
Some
40
40
(fun (k : (a, _) Effect.Deep.continuation) ->
41
-
Effect.Deep.continue k (http_request ~sw ~client req))
41
+
Effect.Deep.continue k (http_request ~hcs_client req))
42
42
| _ -> None);
43
43
}
44
44
45
-
(** {1 Commands} *)
46
-
47
-
let login ~sw ~client ~pds ~identifier ~password =
48
-
run_with_eio ~sw ~client (fun () ->
45
+
let login ~hcs_client ~pds ~identifier ~password =
46
+
run_with_hcs ~hcs_client (fun () ->
49
47
let agent = Agent.create_from_url ~url:pds in
50
48
match Agent.login agent ~identifier ~password with
51
49
| Error e ->
···
56
54
(Option.value ~default:"?" (Agent.handle agent));
57
55
Some agent)
58
56
59
-
let post ~sw ~client agent text =
60
-
run_with_eio ~sw ~client (fun () ->
57
+
let post ~hcs_client agent text =
58
+
run_with_hcs ~hcs_client (fun () ->
61
59
let rt = Richtext.detect_facets text in
62
60
match Agent.create_post_richtext agent ~richtext:rt () with
63
61
| Error e ->
···
67
65
Printf.printf "Posted: %s\n" r.uri;
68
66
0)
69
67
70
-
let timeline ~sw ~client agent limit =
71
-
run_with_eio ~sw ~client (fun () ->
68
+
let timeline ~hcs_client agent limit =
69
+
run_with_hcs ~hcs_client (fun () ->
72
70
match Agent.get_timeline agent ~limit () with
73
71
| Error e ->
74
72
Printf.printf "Timeline failed: %s\n" (Agent.error_to_string e);
···
81
79
feed.items;
82
80
0)
83
81
84
-
let profile ~sw ~client agent actor =
85
-
run_with_eio ~sw ~client (fun () ->
82
+
let profile ~hcs_client agent actor =
83
+
run_with_hcs ~hcs_client (fun () ->
86
84
match Agent.get_profile agent ~actor with
87
85
| Error e ->
88
86
Printf.printf "Profile failed: %s\n" (Agent.error_to_string e);
···
95
93
p.followers_count p.follows_count p.posts_count;
96
94
0)
97
95
98
-
let follow ~sw ~client agent did =
99
-
run_with_eio ~sw ~client (fun () ->
96
+
let follow ~hcs_client agent did =
97
+
run_with_hcs ~hcs_client (fun () ->
100
98
match Agent.follow agent ~did with
101
99
| Error e ->
102
100
Printf.printf "Follow failed: %s\n" (Agent.error_to_string e);
···
105
103
Printf.printf "Followed: %s\n" r.uri;
106
104
0)
107
105
108
-
(** {1 CLI} *)
109
-
110
106
type cmd =
111
107
| Post of string
112
108
| Timeline of int
···
141
137
Mirage_crypto_rng_unix.use_default ();
142
138
Eio_main.run @@ fun env ->
143
139
Eio.Switch.run @@ fun sw ->
144
-
let https_config =
145
-
match
146
-
Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) ()
147
-
with
148
-
| Ok c -> c
149
-
| Error (`Msg m) -> failwith m
140
+
let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in
141
+
let hcs_client =
142
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
143
+
~clock:(Eio.Stdenv.clock env) ~config ()
150
144
in
151
-
let https uri socket =
152
-
let tls_host =
153
-
match Uri.host uri with
154
-
| Some h -> (
155
-
match Domain_name.of_string h with
156
-
| Error _ -> Option.None
157
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
158
-
| Option.None -> Option.None
159
-
in
160
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
161
-
in
162
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
163
145
let pds, identifier, password, cmd =
164
146
Climate.Command.run ~program_name:(Climate.Program_name.Literal "bsky_bot")
165
147
(Climate.Command.singleton ~doc:"Bluesky bot - post, timeline, follow" cli)
166
148
in
167
-
match cmd with
168
-
| None ->
169
-
Printf.printf
170
-
"Usage: bsky_bot --user USER --password PASS [--post \
171
-
TEXT|--timeline|--profile ACTOR|--follow DID]\n";
172
-
exit 0
173
-
| _ -> (
174
-
match (identifier, password) with
175
-
| Some id, Some pw -> (
176
-
match login ~sw ~client ~pds ~identifier:id ~password:pw with
177
-
| None -> exit 1
178
-
| Some agent ->
179
-
exit
180
-
(match cmd with
181
-
| Post t -> post ~sw ~client agent t
182
-
| Timeline n -> timeline ~sw ~client agent n
183
-
| Profile a -> profile ~sw ~client agent a
184
-
| Follow d -> follow ~sw ~client agent d
149
+
let result =
150
+
match cmd with
151
+
| None ->
152
+
Printf.printf
153
+
"Usage: bsky_bot --user USER --password PASS [--post \
154
+
TEXT|--timeline|--profile ACTOR|--follow DID]\n";
155
+
0
156
+
| _ -> (
157
+
match (identifier, password) with
158
+
| Some id, Some pw -> (
159
+
match login ~hcs_client ~pds ~identifier:id ~password:pw with
160
+
| None -> 1
161
+
| Some agent -> (
162
+
match cmd with
163
+
| Post t -> post ~hcs_client agent t
164
+
| Timeline n -> timeline ~hcs_client agent n
165
+
| Profile a -> profile ~hcs_client agent a
166
+
| Follow d -> follow ~hcs_client agent d
185
167
| None -> 0))
186
-
| _ ->
187
-
Printf.printf "Error: --user and --password required\n";
188
-
exit 1)
168
+
| _ ->
169
+
Printf.printf "Error: --user and --password required\n";
170
+
1)
171
+
in
172
+
Hcs.Client.close hcs_client;
173
+
exit result
+1
-4
examples/bsky_bot/dune
+1
-4
examples/bsky_bot/dune
+2
-8
examples/feed_generator/dune
+2
-8
examples/feed_generator/dune
···
1
1
(executable
2
2
(name feed_generator)
3
-
(public_name feed_generator)
4
-
(package atproto)
5
3
(libraries
6
4
atproto-sync
7
5
atproto-ipld
···
9
7
uri
10
8
eio
11
9
eio_main
12
-
tls-eio
13
-
ca-certs-nss
14
-
mirage-crypto-rng.unix
15
-
base64
16
-
cstruct))
17
-
10
+
hcs
11
+
mirage-crypto-rng.unix))
+47
-250
examples/feed_generator/feed_generator.ml
+47
-250
examples/feed_generator/feed_generator.ml
···
83
83
let base_json = Firehose.operation_to_json op in
84
84
match find_block blocks op with
85
85
| Some cbor ->
86
-
let record_json = Yojson.Safe.from_string (Dag_json.encode_string cbor) in
86
+
let record_json =
87
+
match Atproto_json.decode (Dag_json.encode_string cbor) with
88
+
| Ok j -> j
89
+
| Error _ -> Atproto_json.null
90
+
in
87
91
Firehose.add_record_to_op_json base_json record_json
88
92
| None -> base_json
89
93
90
94
let json_of_event ?(rich = false) event =
91
-
let json =
95
+
let json : Atproto_json.t =
92
96
match event with
93
97
| Firehose.Commit evt when rich -> (
94
98
let blocks = extract_blocks evt.blocks in
95
-
let ops = `List (List.map (json_of_op_with_record blocks) evt.ops) in
99
+
let ops =
100
+
Atproto_json.array (List.map (json_of_op_with_record blocks) evt.ops)
101
+
in
96
102
let base = Firehose.commit_event_to_json evt in
97
-
(* Replace the ops field with our enriched version *)
98
103
match base with
99
-
| `Assoc fields ->
100
-
`Assoc
104
+
| Simdjsont.Json.Object fields ->
105
+
Simdjsont.Json.Object
101
106
(List.map
102
107
(fun (k, v) -> if k = "ops" then (k, ops) else (k, v))
103
108
fields)
104
109
| _ -> base)
105
110
| _ -> Firehose.event_to_json event
106
111
in
107
-
Yojson.Safe.to_string json
112
+
Atproto_json.encode json
108
113
109
114
type filter =
110
115
| Posts
···
173
178
| Firehose.Tombstone _ -> List.mem Tombstones filters
174
179
| Firehose.Info _ | Firehose.StreamError _ -> true)
175
180
176
-
(** {1 Keyword Filtering} *)
177
-
178
181
let contains_keyword keyword text =
179
182
let kw = String.lowercase_ascii keyword in
180
183
let txt = String.lowercase_ascii text in
···
211
214
&& text_matches_keyword kw blocks op)
212
215
evt.ops
213
216
| _ -> false)
214
-
215
-
(** {1 Feed Skeleton} *)
216
217
217
218
let max_feed_size = 1000
218
219
let feed_posts : string Queue.t = Queue.create ()
···
259
260
Printf.printf " ]\n}\n"
260
261
end
261
262
262
-
(** {1 WebSocket Client} *)
263
-
264
-
module Ws = struct
265
-
type conn = {
266
-
socket : Tls_eio.t;
267
-
buf : bytes;
268
-
mutable buf_len : int;
269
-
frag_buf : Buffer.t;
270
-
mutable frag_opcode : int;
271
-
}
272
-
273
-
let ws_nonce () =
274
-
let b = Bytes.create 16 in
275
-
for i = 0 to 15 do
276
-
Bytes.set b i (Char.chr (Random.int 256))
277
-
done;
278
-
Base64.encode_exn (Bytes.to_string b)
279
-
280
-
let parse_frame_header buf off len =
281
-
if len < 2 then None
282
-
else
283
-
let b0, b1 =
284
-
(Char.code (Bytes.get buf off), Char.code (Bytes.get buf (off + 1)))
285
-
in
286
-
let fin, opcode = (b0 land 0x80 <> 0, b0 land 0x0f) in
287
-
let masked, plen = (b1 land 0x80 <> 0, b1 land 0x7f) in
288
-
let hlen, plen =
289
-
if plen = 126 then
290
-
if len < 4 then (0, -1)
291
-
else
292
-
( 4,
293
-
(Char.code (Bytes.get buf (off + 2)) lsl 8)
294
-
lor Char.code (Bytes.get buf (off + 3)) )
295
-
else if plen = 127 then
296
-
if len < 10 then (0, -1)
297
-
else
298
-
( 10,
299
-
(Char.code (Bytes.get buf (off + 6)) lsl 24)
300
-
lor (Char.code (Bytes.get buf (off + 7)) lsl 16)
301
-
lor (Char.code (Bytes.get buf (off + 8)) lsl 8)
302
-
lor Char.code (Bytes.get buf (off + 9)) )
303
-
else (2, plen)
304
-
in
305
-
if plen < 0 then None
306
-
else
307
-
let mlen = if masked then 4 else 0 in
308
-
if len < hlen + mlen then None
309
-
else
310
-
Some
311
-
( fin,
312
-
opcode,
313
-
plen,
314
-
(if masked then Some (Bytes.sub buf (off + hlen) 4) else None),
315
-
hlen + mlen )
316
-
317
-
let make_frame opcode payload =
318
-
let plen = String.length payload in
319
-
let mask = Bytes.init 4 (fun _ -> Char.chr (Random.int 256)) in
320
-
let hdr =
321
-
if plen < 126 then (
322
-
let h = Bytes.create 6 in
323
-
Bytes.set h 0 (Char.chr (0x80 lor opcode));
324
-
Bytes.set h 1 (Char.chr (0x80 lor plen));
325
-
Bytes.blit mask 0 h 2 4;
326
-
Bytes.to_string h)
327
-
else if plen < 65536 then (
328
-
let h = Bytes.create 8 in
329
-
Bytes.set h 0 (Char.chr (0x80 lor opcode));
330
-
Bytes.set h 1 (Char.chr (0x80 lor 126));
331
-
Bytes.set h 2 (Char.chr ((plen lsr 8) land 0xff));
332
-
Bytes.set h 3 (Char.chr (plen land 0xff));
333
-
Bytes.blit mask 0 h 4 4;
334
-
Bytes.to_string h)
335
-
else
336
-
let h = Bytes.create 14 in
337
-
Bytes.set h 0 (Char.chr (0x80 lor opcode));
338
-
Bytes.set h 1 (Char.chr (0x80 lor 127));
339
-
for i = 2 to 5 do
340
-
Bytes.set h i '\000'
341
-
done;
342
-
Bytes.set h 6 (Char.chr ((plen lsr 24) land 0xff));
343
-
Bytes.set h 7 (Char.chr ((plen lsr 16) land 0xff));
344
-
Bytes.set h 8 (Char.chr ((plen lsr 8) land 0xff));
345
-
Bytes.set h 9 (Char.chr (plen land 0xff));
346
-
Bytes.blit mask 0 h 10 4;
347
-
Bytes.to_string h
348
-
in
349
-
let masked =
350
-
Bytes.mapi
351
-
(fun i c ->
352
-
Char.chr (Char.code c lxor Char.code (Bytes.get mask (i mod 4))))
353
-
(Bytes.of_string payload)
354
-
in
355
-
hdr ^ Bytes.to_string masked
356
-
357
-
let connect ~net ~sw uri =
358
-
let host = Uri.host uri |> Option.value ~default:"localhost" in
359
-
let port = Uri.port uri |> Option.value ~default:443 in
360
-
let auth =
361
-
match Ca_certs_nss.authenticator () with
362
-
| Ok a -> a
363
-
| Error (`Msg m) -> failwith m
364
-
in
365
-
let tls_config =
366
-
match
367
-
Tls.Config.client ~authenticator:auth ~alpn_protocols:[ "http/1.1" ] ()
368
-
with
369
-
| Ok c -> c
370
-
| Error (`Msg m) -> failwith m
371
-
in
372
-
let hostname =
373
-
match Domain_name.of_string host with
374
-
| Error _ -> None
375
-
| Ok dn -> (
376
-
match Domain_name.host dn with Ok h -> Some h | Error _ -> None)
377
-
in
378
-
let addr =
379
-
match
380
-
Eio.Net.getaddrinfo_stream net host ~service:(string_of_int port)
381
-
with
382
-
| [] -> failwith ("DNS failed: " ^ host)
383
-
| a :: _ -> a
384
-
in
385
-
let socket = Eio.Net.connect ~sw net addr in
386
-
let tls_socket = Tls_eio.client_of_flow tls_config ?host:hostname socket in
387
-
let nonce = ws_nonce () in
388
-
Eio.Flow.copy_string
389
-
(Printf.sprintf
390
-
"GET %s HTTP/1.1\r\n\
391
-
Host: %s\r\n\
392
-
Upgrade: websocket\r\n\
393
-
Connection: Upgrade\r\n\
394
-
Sec-WebSocket-Key: %s\r\n\
395
-
Sec-WebSocket-Version: 13\r\n\
396
-
\r\n"
397
-
(Uri.path_and_query uri) host nonce)
398
-
tls_socket;
399
-
let resp_buf = Cstruct.create 4096 in
400
-
let n = Eio.Flow.single_read tls_socket resp_buf in
401
-
let resp = Cstruct.to_string (Cstruct.sub resp_buf 0 n) in
402
-
if not (String.length resp >= 12 && String.sub resp 0 12 = "HTTP/1.1 101")
403
-
then
404
-
failwith
405
-
("WebSocket upgrade failed: "
406
-
^ String.sub resp 0 (min 50 (String.length resp)));
407
-
{
408
-
socket = tls_socket;
409
-
buf = Bytes.create 1048576;
410
-
buf_len = 0;
411
-
frag_buf = Buffer.create 65536;
412
-
frag_opcode = 0;
413
-
}
414
-
415
-
let read_more conn =
416
-
let cs = Cstruct.create 65536 in
417
-
let n = Eio.Flow.single_read conn.socket cs in
418
-
Cstruct.blit_to_bytes cs 0 conn.buf conn.buf_len n;
419
-
conn.buf_len <- conn.buf_len + n
420
-
421
-
let recv conn =
422
-
let rec read_frame () =
423
-
if conn.buf_len < 2 then (
424
-
read_more conn;
425
-
read_frame ())
426
-
else
427
-
match parse_frame_header conn.buf 0 conn.buf_len with
428
-
| None ->
429
-
read_more conn;
430
-
read_frame ()
431
-
| Some (fin, opcode, plen, mask, hlen) -> (
432
-
let total = hlen + plen in
433
-
while conn.buf_len < total do
434
-
read_more conn
435
-
done;
436
-
let payload = Bytes.sub conn.buf hlen plen in
437
-
(match mask with
438
-
| Some k ->
439
-
for i = 0 to plen - 1 do
440
-
Bytes.set payload i
441
-
(Char.chr
442
-
(Char.code (Bytes.get payload i)
443
-
lxor Char.code (Bytes.get k (i mod 4))))
444
-
done
445
-
| None -> ());
446
-
let rem = conn.buf_len - total in
447
-
if rem > 0 then Bytes.blit conn.buf total conn.buf 0 rem;
448
-
conn.buf_len <- rem;
449
-
match opcode with
450
-
| 0x0 ->
451
-
Buffer.add_bytes conn.frag_buf payload;
452
-
if fin then (
453
-
let data = Buffer.contents conn.frag_buf in
454
-
Buffer.clear conn.frag_buf;
455
-
if conn.frag_opcode = 0x2 then Ok data else read_frame ())
456
-
else read_frame ()
457
-
| 0x1 ->
458
-
if not fin then (
459
-
Buffer.clear conn.frag_buf;
460
-
Buffer.add_bytes conn.frag_buf payload;
461
-
conn.frag_opcode <- 0x1);
462
-
read_frame ()
463
-
| 0x2 ->
464
-
if fin then Ok (Bytes.to_string payload)
465
-
else (
466
-
Buffer.clear conn.frag_buf;
467
-
Buffer.add_bytes conn.frag_buf payload;
468
-
conn.frag_opcode <- 0x2;
469
-
read_frame ())
470
-
| 0x8 -> Error "Connection closed"
471
-
| 0x9 ->
472
-
Eio.Flow.copy_string
473
-
(make_frame 0xA (Bytes.to_string payload))
474
-
conn.socket;
475
-
read_frame ()
476
-
| _ -> read_frame ())
477
-
in
478
-
try read_frame () with exn -> Error (Printexc.to_string exn)
479
-
480
-
let close _conn = ()
481
-
end
482
-
483
-
let with_websocket ~net ~sw f =
263
+
let with_websocket ~sw ~net f =
484
264
let open Effect.Deep in
485
265
try_with f ()
486
266
{
···
490
270
| Firehose.Ws_connect uri ->
491
271
Some
492
272
(fun (k : (a, _) continuation) ->
493
-
try
494
-
continue k
495
-
(Ok
496
-
(Obj.magic (Ws.connect ~net ~sw uri)
497
-
: Firehose.websocket))
498
-
with exn -> continue k (Error (Printexc.to_string exn)))
273
+
let url = Uri.to_string uri in
274
+
match Hcs.Websocket.connect ~sw ~net url with
275
+
| Ok ws -> continue k (Ok (Obj.magic ws : Firehose.websocket))
276
+
| Error e ->
277
+
let msg =
278
+
match e with
279
+
| Hcs.Websocket.Connection_closed -> "Connection closed"
280
+
| Hcs.Websocket.Protocol_error s ->
281
+
"Protocol error: " ^ s
282
+
| Hcs.Websocket.Io_error s -> "IO error: " ^ s
283
+
| Hcs.Websocket.Payload_too_large n ->
284
+
"Payload too large: " ^ string_of_int n
285
+
in
286
+
continue k (Error msg))
499
287
| Firehose.Ws_recv ws ->
500
-
Some (fun k -> continue k (Ws.recv (Obj.magic ws : Ws.conn)))
288
+
Some
289
+
(fun k ->
290
+
let hcs_ws = (Obj.magic ws : Hcs.Websocket.t) in
291
+
match Hcs.Websocket.recv hcs_ws with
292
+
| Ok frame ->
293
+
if frame.opcode = Hcs.Websocket.Opcode.Binary then
294
+
continue k (Ok frame.content)
295
+
else if frame.opcode = Hcs.Websocket.Opcode.Close then
296
+
continue k (Error "Connection closed")
297
+
else continue k (Ok frame.content)
298
+
| Error Hcs.Websocket.Connection_closed ->
299
+
continue k (Error "Connection closed")
300
+
| Error (Hcs.Websocket.Protocol_error s) ->
301
+
continue k (Error ("Protocol error: " ^ s))
302
+
| Error (Hcs.Websocket.Io_error s) ->
303
+
continue k (Error ("IO error: " ^ s))
304
+
| Error (Hcs.Websocket.Payload_too_large n) ->
305
+
continue k
306
+
(Error ("Payload too large: " ^ string_of_int n)))
501
307
| Firehose.Ws_close ws ->
502
308
Some
503
309
(fun k ->
504
-
Ws.close (Obj.magic ws);
310
+
Hcs.Websocket.close (Obj.magic ws);
505
311
continue k ())
506
312
| _ -> None);
507
313
}
508
314
509
-
(** {1 Configuration} *)
510
-
511
315
type config = {
512
316
cursor : int64 option;
513
317
limit : int option;
···
558
362
Climate.Command.singleton
559
363
~doc:"AT Protocol firehose client and feed generator" config_parser
560
364
561
-
(** {1 Stats} *)
562
-
563
365
type stats = {
564
366
mutable total : int;
565
367
mutable matched : int;
···
577
379
let stats = { total = 0; matched = 0; last_seq = 0L; start = 0. }
578
380
let interrupted = ref false
579
381
580
-
(** {1 Main} *)
581
-
582
382
let run ~net ~sw config =
583
383
let uri =
584
384
Uri.of_string "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"
···
596
396
(match Firehose.event_seq event with
597
397
| Some s -> stats.last_seq <- s
598
398
| None -> ());
599
-
(* Collect posts for skeleton if enabled *)
600
399
(if config.skeleton then
601
400
match event with
602
401
| Firehose.Commit evt -> collect_post_uris evt config.keyword
603
402
| _ -> ());
604
-
(* Check filters *)
605
403
let type_match = event_matches config.filters event in
606
404
let keyword_match = event_matches_keyword config.keyword event in
607
405
if type_match && keyword_match then begin
···
637
435
Sys.set_signal Sys.sigint (Sys.Signal_handle (fun _ -> interrupted := true));
638
436
stats.start <- Unix.gettimeofday ();
639
437
Mirage_crypto_rng_unix.use_default ();
640
-
Random.self_init ();
641
438
(try
642
439
Eio_main.run @@ fun env ->
643
440
Eio.Switch.run @@ fun sw -> run ~net:(Eio.Stdenv.net env) ~sw config
+1
-4
examples/identity_tool/dune
+1
-4
examples/identity_tool/dune
+36
-49
examples/identity_tool/identity_tool.ml
+36
-49
examples/identity_tool/identity_tool.ml
···
5
5
open Atproto_syntax
6
6
open Atproto_identity
7
7
8
-
(** {1 HTTP Client with cohttp-eio} *)
8
+
let http_get ~hcs_client uri =
9
+
let url = Uri.to_string uri in
10
+
match Hcs.Client.request hcs_client url with
11
+
| Ok resp -> Did_resolver.{ status = resp.status; body = resp.body }
12
+
| Error e ->
13
+
let msg =
14
+
match e with
15
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
16
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
17
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
18
+
| Hcs.Client.Timeout -> "Timeout"
19
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
20
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
21
+
in
22
+
Did_resolver.{ status = 0; body = msg }
9
23
10
-
let http_get ~sw ~client uri =
11
-
try
12
-
let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in
13
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
14
-
let body =
15
-
Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all)
16
-
in
17
-
Did_resolver.{ status; body }
18
-
with e -> Did_resolver.{ status = 0; body = Printexc.to_string e }
19
-
20
-
(** {1 Effect Handler} *)
21
-
22
-
let run_with_eio ~sw ~client f =
24
+
let run_with_hcs ~hcs_client f =
23
25
Effect.Deep.try_with f ()
24
26
{
25
27
effc =
···
28
30
| Did_resolver.Http_get uri ->
29
31
Some
30
32
(fun (k : (a, _) Effect.Deep.continuation) ->
31
-
Effect.Deep.continue k (http_get ~sw ~client uri))
33
+
Effect.Deep.continue k (http_get ~hcs_client uri))
32
34
| Handle_resolver.Http_get uri ->
33
35
Some
34
36
(fun k ->
35
-
let r = http_get ~sw ~client uri in
37
+
let r = http_get ~hcs_client uri in
36
38
Effect.Deep.continue k
37
39
Handle_resolver.{ status = r.status; body = r.body })
38
40
| Handle_resolver.Dns_txt _ ->
···
41
43
| _ -> None);
42
44
}
43
45
44
-
(** {1 Commands} *)
45
-
46
-
let resolve_handle ~sw ~client h =
46
+
let resolve_handle ~hcs_client h =
47
47
match Handle.of_string h with
48
48
| Error _ ->
49
49
Printf.printf "Error: Invalid handle\n";
50
50
1
51
51
| Ok handle ->
52
-
run_with_eio ~sw ~client (fun () ->
52
+
run_with_hcs ~hcs_client (fun () ->
53
53
match Handle_resolver.resolve handle with
54
54
| Error e ->
55
55
Printf.printf "Error: %s\n" (Handle_resolver.error_to_string e);
···
59
59
(Did.to_string did);
60
60
0)
61
61
62
-
let resolve_did ~sw ~client d =
62
+
let resolve_did ~hcs_client d =
63
63
match Did.of_string d with
64
64
| Error _ ->
65
65
Printf.printf "Error: Invalid DID\n";
66
66
1
67
67
| Ok did ->
68
-
run_with_eio ~sw ~client (fun () ->
68
+
run_with_hcs ~hcs_client (fun () ->
69
69
match Did_resolver.resolve_did did with
70
70
| Error e ->
71
71
Printf.printf "Error: %s\n" (Did_resolver.error_to_string e);
···
87
87
doc.service;
88
88
0)
89
89
90
-
let verify ~sw ~client id =
90
+
let verify ~hcs_client id =
91
91
let is_did = String.length id > 4 && String.sub id 0 4 = "did:" in
92
-
run_with_eio ~sw ~client (fun () ->
92
+
run_with_hcs ~hcs_client (fun () ->
93
93
let result =
94
94
if is_did then
95
95
match Did.of_string id with
···
116
116
v.pds_endpoint;
117
117
0)
118
118
119
-
(** {1 CLI} *)
120
-
121
119
type mode = Resolve_handle | Resolve_did | Verify
122
120
123
121
let cli =
···
136
134
Mirage_crypto_rng_unix.use_default ();
137
135
Eio_main.run @@ fun env ->
138
136
Eio.Switch.run @@ fun sw ->
139
-
let https_config =
140
-
match
141
-
Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) ()
142
-
with
143
-
| Ok c -> c
144
-
| Error (`Msg m) -> failwith m
145
-
in
146
-
let https uri socket =
147
-
let tls_host =
148
-
match Uri.host uri with
149
-
| Some h -> (
150
-
match Domain_name.of_string h with
151
-
| Error _ -> None
152
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
153
-
| None -> None
154
-
in
155
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
137
+
let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in
138
+
let hcs_client =
139
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
140
+
~clock:(Eio.Stdenv.clock env) ~config ()
156
141
in
157
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
158
142
let mode, id =
159
143
Climate.Command.run
160
144
~program_name:(Climate.Program_name.Literal "identity_tool")
161
145
(Climate.Command.singleton ~doc:"AT Protocol identity lookup tool" cli)
162
146
in
163
-
exit
164
-
(match mode with
165
-
| Resolve_handle -> resolve_handle ~sw ~client id
166
-
| Resolve_did -> resolve_did ~sw ~client id
167
-
| Verify -> verify ~sw ~client id)
147
+
let result =
148
+
match mode with
149
+
| Resolve_handle -> resolve_handle ~hcs_client id
150
+
| Resolve_did -> resolve_did ~hcs_client id
151
+
| Verify -> verify ~hcs_client id
152
+
in
153
+
Hcs.Client.close hcs_client;
154
+
exit result
+1
-5
examples/repo_inspector/dune
+1
-5
examples/repo_inspector/dune
···
1
1
(executable
2
2
(name repo_inspector)
3
-
(public_name repo_inspector)
4
-
(package atproto)
5
3
(libraries
6
4
atproto-repo
7
5
atproto-mst
···
11
9
atproto-crypto
12
10
climate
13
11
eio_main
14
-
cohttp-eio
15
-
tls-eio
16
-
ca-certs-nss
12
+
hcs
17
13
base64
18
14
mirage-crypto-rng.unix
19
15
uri))
+35
-57
examples/repo_inspector/repo_inspector.ml
+35
-57
examples/repo_inspector/repo_inspector.ml
···
18
18
module Did = Atproto_syntax.Did
19
19
module Did_resolver = Atproto_identity.Did_resolver
20
20
21
-
(** {1 HTTP Client with cohttp-eio} *)
22
-
23
-
let http_get ~sw ~client uri =
24
-
try
25
-
let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in
26
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
27
-
let body =
28
-
Eio.Buf_read.(of_flow ~max_size:(100 * 1024 * 1024) resp_body |> take_all)
29
-
in
30
-
(status, body)
31
-
with e -> (0, Printexc.to_string e)
32
-
33
-
(** {1 DID Resolution Effect Handler} *)
21
+
let http_get ~hcs_client uri =
22
+
let url = Uri.to_string uri in
23
+
match Hcs.Client.request hcs_client url with
24
+
| Ok resp -> (resp.status, resp.body)
25
+
| Error e ->
26
+
let msg =
27
+
match e with
28
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
29
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
30
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
31
+
| Hcs.Client.Timeout -> "Timeout"
32
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
33
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
34
+
in
35
+
(0, msg)
34
36
35
-
let run_with_resolver ~sw ~client f =
37
+
let run_with_resolver ~hcs_client f =
36
38
Effect.Deep.try_with f ()
37
39
{
38
40
effc =
···
41
43
| Did_resolver.Http_get uri ->
42
44
Some
43
45
(fun (k : (a, _) Effect.Deep.continuation) ->
44
-
let status, body = http_get ~sw ~client uri in
46
+
let status, body = http_get ~hcs_client uri in
45
47
Effect.Deep.continue k Did_resolver.{ status; body })
46
48
| _ -> None);
47
49
}
48
50
49
-
(** {1 PDS Resolution} *)
50
-
51
-
let resolve_pds ~sw ~client did_str =
51
+
let resolve_pds ~hcs_client did_str =
52
52
match Did.of_string did_str with
53
53
| Error _ -> Error "Invalid DID format"
54
54
| Ok did ->
55
-
run_with_resolver ~sw ~client (fun () ->
55
+
run_with_resolver ~hcs_client (fun () ->
56
56
match Did_resolver.resolve_did did with
57
57
| Error e -> Error (Did_resolver.error_to_string e)
58
58
| Ok doc -> (
59
-
(* Find AtprotoPersonalDataServer service *)
60
59
match
61
60
List.find_opt
62
61
(fun (s : Did_resolver.service) ->
···
66
65
| Some s -> Ok s.service_endpoint
67
66
| None -> Error "No PDS service found in DID document"))
68
67
69
-
(** {1 Repository Fetching} *)
70
-
71
-
let fetch_repo ~sw ~client ~pds_url did =
68
+
let fetch_repo ~hcs_client ~pds_url did =
72
69
let uri =
73
70
Uri.of_string (pds_url ^ "/xrpc/com.atproto.sync.getRepo")
74
71
|> Fun.flip Uri.add_query_param ("did", [ did ])
75
72
in
76
73
Printf.printf "Fetching %s...\n%!" (Uri.to_string uri);
77
-
let status, body = http_get ~sw ~client uri in
74
+
let status, body = http_get ~hcs_client uri in
78
75
if status = 200 then Ok body
79
76
else
80
77
Error
81
78
(Printf.sprintf "HTTP %d: %s" status
82
79
(String.sub body 0 (min 200 (String.length body))))
83
80
84
-
(** {1 Repository Analysis} *)
85
-
86
81
let truncate n s =
87
82
if String.length s <= n then s else String.sub s 0 (n - 3) ^ "..."
88
83
···
110
105
(fun r -> Printf.printf " - %s\n" (Cid.to_string r))
111
106
header.roots;
112
107
Printf.printf "Blocks: %d\n\n" (List.length blocks);
113
-
(* Build blockstore *)
114
108
let store = Blockstore.create () in
115
109
List.iter
116
110
(fun (b : Car.block) -> Blockstore.put store b.cid b.data)
117
111
blocks;
118
-
(* Find commit *)
119
112
let commit_cid = List.hd header.roots in
120
113
let commit_data = Blockstore.get store commit_cid in
121
114
match commit_data with
···
181
174
(fun (key, cid) ->
182
175
if !shown < limit then begin
183
176
Printf.printf "%s\n CID: %s\n" key (Cid.to_string cid);
184
-
(* Try to get and show content preview *)
185
177
(match Blockstore.get store cid with
186
178
| Some data -> (
187
179
match Dag_cbor.decode data with
···
207
199
else "(none)"
208
200
in
209
201
Printf.printf "Signature: %s\n" sig_b64;
210
-
(* Get signing key from DID - would need identity resolution *)
211
202
Printf.printf
212
203
"Status: Signature present but key resolution not implemented\n";
213
204
Printf.printf " (would need to resolve DID document to verify)\n"
214
205
215
-
(** {1 CLI} *)
216
-
217
206
type mode = Summary | Collections | Records of string option | Verify
218
207
219
208
let cli =
···
239
228
Mirage_crypto_rng_unix.use_default ();
240
229
Eio_main.run @@ fun env ->
241
230
Eio.Switch.run @@ fun sw ->
242
-
let auth =
243
-
match Ca_certs_nss.authenticator () with
244
-
| Ok a -> a
245
-
| Error (`Msg m) -> failwith m
231
+
let config =
232
+
Hcs.Client.default_config |> Hcs.Client.with_max_response_body 104857600L
246
233
in
247
-
let https_config =
248
-
match Tls.Config.client ~authenticator:auth () with
249
-
| Ok c -> c
250
-
| Error (`Msg m) -> failwith m
251
-
in
252
-
let https uri socket =
253
-
let tls_host =
254
-
match Uri.host uri with
255
-
| Some h -> (
256
-
match Domain_name.of_string h with
257
-
| Error _ -> None
258
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
259
-
| None -> None
260
-
in
261
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
234
+
let hcs_client =
235
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
236
+
~clock:(Eio.Stdenv.clock env) ~config ()
262
237
in
263
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
264
238
let did, pds_opt, mode, limit =
265
239
Climate.Command.run
266
240
~program_name:(Climate.Program_name.Literal "repo_inspector")
267
241
(Climate.Command.singleton ~doc:"AT Protocol repository inspector" cli)
268
242
in
269
243
Printf.printf "Repository Inspector\n====================\n\n";
270
-
(* Resolve PDS if not provided *)
271
244
let pds_url =
272
245
match pds_opt with
273
246
| Some url -> url
274
247
| None -> (
275
248
Printf.printf "Resolving PDS for %s...\n%!" did;
276
-
match resolve_pds ~sw ~client did with
249
+
match resolve_pds ~hcs_client did with
277
250
| Ok url ->
278
251
Printf.printf "PDS: %s\n\n%!" url;
279
252
url
280
253
| Error e ->
281
254
Printf.printf "Error resolving PDS: %s\n" e;
255
+
Hcs.Client.close hcs_client;
282
256
exit 1)
283
257
in
284
-
match fetch_repo ~sw ~client ~pds_url did with
258
+
(match fetch_repo ~hcs_client ~pds_url did with
285
259
| Error e ->
286
260
Printf.printf "Error: %s\n" e;
261
+
Hcs.Client.close hcs_client;
287
262
exit 1
288
263
| Ok car_data -> (
289
264
match analyze_repo car_data with
290
-
| None -> exit 1
265
+
| None ->
266
+
Hcs.Client.close hcs_client;
267
+
exit 1
291
268
| Some (store, commit) -> (
292
269
show_commit commit;
293
270
match mode with
294
271
| Summary -> show_collections store commit
295
272
| Collections -> show_collections store commit
296
273
| Records coll -> show_records ~limit ~collection:coll store commit
297
-
| Verify -> verify_commit store commit))
274
+
| Verify -> verify_commit store commit)));
275
+
Hcs.Client.close hcs_client
+666
lexicons/app/bsky/actor/defs.json
+666
lexicons/app/bsky/actor/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.defs",
4
+
"defs": {
5
+
"profileViewBasic": {
6
+
"type": "object",
7
+
"required": ["did", "handle"],
8
+
"properties": {
9
+
"did": { "type": "string", "format": "did" },
10
+
"handle": { "type": "string", "format": "handle" },
11
+
"displayName": {
12
+
"type": "string",
13
+
"maxGraphemes": 64,
14
+
"maxLength": 640
15
+
},
16
+
"pronouns": { "type": "string" },
17
+
"avatar": { "type": "string", "format": "uri" },
18
+
"associated": {
19
+
"type": "ref",
20
+
"ref": "#profileAssociated"
21
+
},
22
+
"viewer": { "type": "ref", "ref": "#viewerState" },
23
+
"labels": {
24
+
"type": "array",
25
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
26
+
},
27
+
"createdAt": { "type": "string", "format": "datetime" },
28
+
"verification": {
29
+
"type": "ref",
30
+
"ref": "#verificationState"
31
+
},
32
+
"status": {
33
+
"type": "ref",
34
+
"ref": "#statusView"
35
+
},
36
+
"debug": {
37
+
"type": "unknown",
38
+
"description": "Debug information for internal development"
39
+
}
40
+
}
41
+
},
42
+
"profileView": {
43
+
"type": "object",
44
+
"required": ["did", "handle"],
45
+
"properties": {
46
+
"did": { "type": "string", "format": "did" },
47
+
"handle": { "type": "string", "format": "handle" },
48
+
"displayName": {
49
+
"type": "string",
50
+
"maxGraphemes": 64,
51
+
"maxLength": 640
52
+
},
53
+
"pronouns": { "type": "string" },
54
+
"description": {
55
+
"type": "string",
56
+
"maxGraphemes": 256,
57
+
"maxLength": 2560
58
+
},
59
+
"avatar": { "type": "string", "format": "uri" },
60
+
"associated": {
61
+
"type": "ref",
62
+
"ref": "#profileAssociated"
63
+
},
64
+
"indexedAt": { "type": "string", "format": "datetime" },
65
+
"createdAt": { "type": "string", "format": "datetime" },
66
+
"viewer": { "type": "ref", "ref": "#viewerState" },
67
+
"labels": {
68
+
"type": "array",
69
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
70
+
},
71
+
"verification": {
72
+
"type": "ref",
73
+
"ref": "#verificationState"
74
+
},
75
+
"status": {
76
+
"type": "ref",
77
+
"ref": "#statusView"
78
+
},
79
+
"debug": {
80
+
"type": "unknown",
81
+
"description": "Debug information for internal development"
82
+
}
83
+
}
84
+
},
85
+
"profileViewDetailed": {
86
+
"type": "object",
87
+
"required": ["did", "handle"],
88
+
"properties": {
89
+
"did": { "type": "string", "format": "did" },
90
+
"handle": { "type": "string", "format": "handle" },
91
+
"displayName": {
92
+
"type": "string",
93
+
"maxGraphemes": 64,
94
+
"maxLength": 640
95
+
},
96
+
"description": {
97
+
"type": "string",
98
+
"maxGraphemes": 256,
99
+
"maxLength": 2560
100
+
},
101
+
"pronouns": { "type": "string" },
102
+
"website": { "type": "string", "format": "uri" },
103
+
"avatar": { "type": "string", "format": "uri" },
104
+
"banner": { "type": "string", "format": "uri" },
105
+
"followersCount": { "type": "integer" },
106
+
"followsCount": { "type": "integer" },
107
+
"postsCount": { "type": "integer" },
108
+
"associated": {
109
+
"type": "ref",
110
+
"ref": "#profileAssociated"
111
+
},
112
+
"joinedViaStarterPack": {
113
+
"type": "ref",
114
+
"ref": "app.bsky.graph.defs#starterPackViewBasic"
115
+
},
116
+
"indexedAt": { "type": "string", "format": "datetime" },
117
+
"createdAt": { "type": "string", "format": "datetime" },
118
+
"viewer": { "type": "ref", "ref": "#viewerState" },
119
+
"labels": {
120
+
"type": "array",
121
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
122
+
},
123
+
"pinnedPost": {
124
+
"type": "ref",
125
+
"ref": "com.atproto.repo.strongRef"
126
+
},
127
+
"verification": {
128
+
"type": "ref",
129
+
"ref": "#verificationState"
130
+
},
131
+
"status": {
132
+
"type": "ref",
133
+
"ref": "#statusView"
134
+
},
135
+
"debug": {
136
+
"type": "unknown",
137
+
"description": "Debug information for internal development"
138
+
}
139
+
}
140
+
},
141
+
"profileAssociated": {
142
+
"type": "object",
143
+
"properties": {
144
+
"lists": { "type": "integer" },
145
+
"feedgens": { "type": "integer" },
146
+
"starterPacks": { "type": "integer" },
147
+
"labeler": { "type": "boolean" },
148
+
"chat": { "type": "ref", "ref": "#profileAssociatedChat" },
149
+
"activitySubscription": {
150
+
"type": "ref",
151
+
"ref": "#profileAssociatedActivitySubscription"
152
+
}
153
+
}
154
+
},
155
+
"profileAssociatedChat": {
156
+
"type": "object",
157
+
"required": ["allowIncoming"],
158
+
"properties": {
159
+
"allowIncoming": {
160
+
"type": "string",
161
+
"knownValues": ["all", "none", "following"]
162
+
}
163
+
}
164
+
},
165
+
"profileAssociatedActivitySubscription": {
166
+
"type": "object",
167
+
"required": ["allowSubscriptions"],
168
+
"properties": {
169
+
"allowSubscriptions": {
170
+
"type": "string",
171
+
"knownValues": ["followers", "mutuals", "none"]
172
+
}
173
+
}
174
+
},
175
+
"viewerState": {
176
+
"type": "object",
177
+
"description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.",
178
+
"properties": {
179
+
"muted": { "type": "boolean" },
180
+
"mutedByList": {
181
+
"type": "ref",
182
+
"ref": "app.bsky.graph.defs#listViewBasic"
183
+
},
184
+
"blockedBy": { "type": "boolean" },
185
+
"blocking": { "type": "string", "format": "at-uri" },
186
+
"blockingByList": {
187
+
"type": "ref",
188
+
"ref": "app.bsky.graph.defs#listViewBasic"
189
+
},
190
+
"following": { "type": "string", "format": "at-uri" },
191
+
"followedBy": { "type": "string", "format": "at-uri" },
192
+
"knownFollowers": {
193
+
"description": "This property is present only in selected cases, as an optimization.",
194
+
"type": "ref",
195
+
"ref": "#knownFollowers"
196
+
},
197
+
"activitySubscription": {
198
+
"description": "This property is present only in selected cases, as an optimization.",
199
+
"type": "ref",
200
+
"ref": "app.bsky.notification.defs#activitySubscription"
201
+
}
202
+
}
203
+
},
204
+
"knownFollowers": {
205
+
"type": "object",
206
+
"description": "The subject's followers whom you also follow",
207
+
"required": ["count", "followers"],
208
+
"properties": {
209
+
"count": { "type": "integer" },
210
+
"followers": {
211
+
"type": "array",
212
+
"minLength": 0,
213
+
"maxLength": 5,
214
+
"items": {
215
+
"type": "ref",
216
+
"ref": "#profileViewBasic"
217
+
}
218
+
}
219
+
}
220
+
},
221
+
"verificationState": {
222
+
"type": "object",
223
+
"description": "Represents the verification information about the user this object is attached to.",
224
+
"required": ["verifications", "verifiedStatus", "trustedVerifierStatus"],
225
+
"properties": {
226
+
"verifications": {
227
+
"type": "array",
228
+
"description": "All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.",
229
+
"items": { "type": "ref", "ref": "#verificationView" }
230
+
},
231
+
"verifiedStatus": {
232
+
"type": "string",
233
+
"description": "The user's status as a verified account.",
234
+
"knownValues": ["valid", "invalid", "none"]
235
+
},
236
+
"trustedVerifierStatus": {
237
+
"type": "string",
238
+
"description": "The user's status as a trusted verifier.",
239
+
"knownValues": ["valid", "invalid", "none"]
240
+
}
241
+
}
242
+
},
243
+
"verificationView": {
244
+
"type": "object",
245
+
"description": "An individual verification for an associated subject.",
246
+
"required": ["issuer", "uri", "isValid", "createdAt"],
247
+
"properties": {
248
+
"issuer": {
249
+
"type": "string",
250
+
"description": "The user who issued this verification.",
251
+
"format": "did"
252
+
},
253
+
"uri": {
254
+
"type": "string",
255
+
"description": "The AT-URI of the verification record.",
256
+
"format": "at-uri"
257
+
},
258
+
"isValid": {
259
+
"type": "boolean",
260
+
"description": "True if the verification passes validation, otherwise false."
261
+
},
262
+
"createdAt": {
263
+
"type": "string",
264
+
"description": "Timestamp when the verification was created.",
265
+
"format": "datetime"
266
+
}
267
+
}
268
+
},
269
+
"preferences": {
270
+
"type": "array",
271
+
"items": {
272
+
"type": "union",
273
+
"refs": [
274
+
"#adultContentPref",
275
+
"#contentLabelPref",
276
+
"#savedFeedsPref",
277
+
"#savedFeedsPrefV2",
278
+
"#personalDetailsPref",
279
+
"#declaredAgePref",
280
+
"#feedViewPref",
281
+
"#threadViewPref",
282
+
"#interestsPref",
283
+
"#mutedWordsPref",
284
+
"#hiddenPostsPref",
285
+
"#bskyAppStatePref",
286
+
"#labelersPref",
287
+
"#postInteractionSettingsPref",
288
+
"#verificationPrefs"
289
+
]
290
+
}
291
+
},
292
+
"adultContentPref": {
293
+
"type": "object",
294
+
"required": ["enabled"],
295
+
"properties": {
296
+
"enabled": { "type": "boolean", "default": false }
297
+
}
298
+
},
299
+
"contentLabelPref": {
300
+
"type": "object",
301
+
"required": ["label", "visibility"],
302
+
"properties": {
303
+
"labelerDid": {
304
+
"type": "string",
305
+
"description": "Which labeler does this preference apply to? If undefined, applies globally.",
306
+
"format": "did"
307
+
},
308
+
"label": { "type": "string" },
309
+
"visibility": {
310
+
"type": "string",
311
+
"knownValues": ["ignore", "show", "warn", "hide"]
312
+
}
313
+
}
314
+
},
315
+
"savedFeed": {
316
+
"type": "object",
317
+
"required": ["id", "type", "value", "pinned"],
318
+
"properties": {
319
+
"id": {
320
+
"type": "string"
321
+
},
322
+
"type": {
323
+
"type": "string",
324
+
"knownValues": ["feed", "list", "timeline"]
325
+
},
326
+
"value": {
327
+
"type": "string"
328
+
},
329
+
"pinned": {
330
+
"type": "boolean"
331
+
}
332
+
}
333
+
},
334
+
"savedFeedsPrefV2": {
335
+
"type": "object",
336
+
"required": ["items"],
337
+
"properties": {
338
+
"items": {
339
+
"type": "array",
340
+
"items": {
341
+
"type": "ref",
342
+
"ref": "app.bsky.actor.defs#savedFeed"
343
+
}
344
+
}
345
+
}
346
+
},
347
+
"savedFeedsPref": {
348
+
"type": "object",
349
+
"required": ["pinned", "saved"],
350
+
"properties": {
351
+
"pinned": {
352
+
"type": "array",
353
+
"items": {
354
+
"type": "string",
355
+
"format": "at-uri"
356
+
}
357
+
},
358
+
"saved": {
359
+
"type": "array",
360
+
"items": {
361
+
"type": "string",
362
+
"format": "at-uri"
363
+
}
364
+
},
365
+
"timelineIndex": {
366
+
"type": "integer"
367
+
}
368
+
}
369
+
},
370
+
"personalDetailsPref": {
371
+
"type": "object",
372
+
"properties": {
373
+
"birthDate": {
374
+
"type": "string",
375
+
"format": "datetime",
376
+
"description": "The birth date of account owner."
377
+
}
378
+
}
379
+
},
380
+
"declaredAgePref": {
381
+
"type": "object",
382
+
"description": "Read-only preference containing value(s) inferred from the user's declared birthdate. Absence of this preference object in the response indicates that the user has not made a declaration.",
383
+
"properties": {
384
+
"isOverAge13": {
385
+
"type": "boolean",
386
+
"description": "Indicates if the user has declared that they are over 13 years of age."
387
+
},
388
+
"isOverAge16": {
389
+
"type": "boolean",
390
+
"description": "Indicates if the user has declared that they are over 16 years of age."
391
+
},
392
+
"isOverAge18": {
393
+
"type": "boolean",
394
+
"description": "Indicates if the user has declared that they are over 18 years of age."
395
+
}
396
+
}
397
+
},
398
+
"feedViewPref": {
399
+
"type": "object",
400
+
"required": ["feed"],
401
+
"properties": {
402
+
"feed": {
403
+
"type": "string",
404
+
"description": "The URI of the feed, or an identifier which describes the feed."
405
+
},
406
+
"hideReplies": {
407
+
"type": "boolean",
408
+
"description": "Hide replies in the feed."
409
+
},
410
+
"hideRepliesByUnfollowed": {
411
+
"type": "boolean",
412
+
"description": "Hide replies in the feed if they are not by followed users.",
413
+
"default": true
414
+
},
415
+
"hideRepliesByLikeCount": {
416
+
"type": "integer",
417
+
"description": "Hide replies in the feed if they do not have this number of likes."
418
+
},
419
+
"hideReposts": {
420
+
"type": "boolean",
421
+
"description": "Hide reposts in the feed."
422
+
},
423
+
"hideQuotePosts": {
424
+
"type": "boolean",
425
+
"description": "Hide quote posts in the feed."
426
+
}
427
+
}
428
+
},
429
+
"threadViewPref": {
430
+
"type": "object",
431
+
"properties": {
432
+
"sort": {
433
+
"type": "string",
434
+
"description": "Sorting mode for threads.",
435
+
"knownValues": ["oldest", "newest", "most-likes", "random", "hotness"]
436
+
}
437
+
}
438
+
},
439
+
"interestsPref": {
440
+
"type": "object",
441
+
"required": ["tags"],
442
+
"properties": {
443
+
"tags": {
444
+
"type": "array",
445
+
"maxLength": 100,
446
+
"items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 },
447
+
"description": "A list of tags which describe the account owner's interests gathered during onboarding."
448
+
}
449
+
}
450
+
},
451
+
"mutedWordTarget": {
452
+
"type": "string",
453
+
"knownValues": ["content", "tag"],
454
+
"maxLength": 640,
455
+
"maxGraphemes": 64
456
+
},
457
+
"mutedWord": {
458
+
"type": "object",
459
+
"description": "A word that the account owner has muted.",
460
+
"required": ["value", "targets"],
461
+
"properties": {
462
+
"id": { "type": "string" },
463
+
"value": {
464
+
"type": "string",
465
+
"description": "The muted word itself.",
466
+
"maxLength": 10000,
467
+
"maxGraphemes": 1000
468
+
},
469
+
"targets": {
470
+
"type": "array",
471
+
"description": "The intended targets of the muted word.",
472
+
"items": {
473
+
"type": "ref",
474
+
"ref": "app.bsky.actor.defs#mutedWordTarget"
475
+
}
476
+
},
477
+
"actorTarget": {
478
+
"type": "string",
479
+
"description": "Groups of users to apply the muted word to. If undefined, applies to all users.",
480
+
"knownValues": ["all", "exclude-following"],
481
+
"default": "all"
482
+
},
483
+
"expiresAt": {
484
+
"type": "string",
485
+
"format": "datetime",
486
+
"description": "The date and time at which the muted word will expire and no longer be applied."
487
+
}
488
+
}
489
+
},
490
+
"mutedWordsPref": {
491
+
"type": "object",
492
+
"required": ["items"],
493
+
"properties": {
494
+
"items": {
495
+
"type": "array",
496
+
"items": {
497
+
"type": "ref",
498
+
"ref": "app.bsky.actor.defs#mutedWord"
499
+
},
500
+
"description": "A list of words the account owner has muted."
501
+
}
502
+
}
503
+
},
504
+
"hiddenPostsPref": {
505
+
"type": "object",
506
+
"required": ["items"],
507
+
"properties": {
508
+
"items": {
509
+
"type": "array",
510
+
"items": { "type": "string", "format": "at-uri" },
511
+
"description": "A list of URIs of posts the account owner has hidden."
512
+
}
513
+
}
514
+
},
515
+
"labelersPref": {
516
+
"type": "object",
517
+
"required": ["labelers"],
518
+
"properties": {
519
+
"labelers": {
520
+
"type": "array",
521
+
"items": {
522
+
"type": "ref",
523
+
"ref": "#labelerPrefItem"
524
+
}
525
+
}
526
+
}
527
+
},
528
+
"labelerPrefItem": {
529
+
"type": "object",
530
+
"required": ["did"],
531
+
"properties": {
532
+
"did": {
533
+
"type": "string",
534
+
"format": "did"
535
+
}
536
+
}
537
+
},
538
+
"bskyAppStatePref": {
539
+
"description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.",
540
+
"type": "object",
541
+
"properties": {
542
+
"activeProgressGuide": {
543
+
"type": "ref",
544
+
"ref": "#bskyAppProgressGuide"
545
+
},
546
+
"queuedNudges": {
547
+
"description": "An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.",
548
+
"type": "array",
549
+
"maxLength": 1000,
550
+
"items": { "type": "string", "maxLength": 100 }
551
+
},
552
+
"nuxs": {
553
+
"description": "Storage for NUXs the user has encountered.",
554
+
"type": "array",
555
+
"maxLength": 100,
556
+
"items": {
557
+
"type": "ref",
558
+
"ref": "app.bsky.actor.defs#nux"
559
+
}
560
+
}
561
+
}
562
+
},
563
+
"bskyAppProgressGuide": {
564
+
"description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.",
565
+
"type": "object",
566
+
"required": ["guide"],
567
+
"properties": {
568
+
"guide": { "type": "string", "maxLength": 100 }
569
+
}
570
+
},
571
+
"nux": {
572
+
"type": "object",
573
+
"description": "A new user experiences (NUX) storage object",
574
+
"required": ["id", "completed"],
575
+
"properties": {
576
+
"id": {
577
+
"type": "string",
578
+
"maxLength": 100
579
+
},
580
+
"completed": {
581
+
"type": "boolean",
582
+
"default": false
583
+
},
584
+
"data": {
585
+
"description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.",
586
+
"type": "string",
587
+
"maxLength": 3000,
588
+
"maxGraphemes": 300
589
+
},
590
+
"expiresAt": {
591
+
"type": "string",
592
+
"format": "datetime",
593
+
"description": "The date and time at which the NUX will expire and should be considered completed."
594
+
}
595
+
}
596
+
},
597
+
"verificationPrefs": {
598
+
"type": "object",
599
+
"description": "Preferences for how verified accounts appear in the app.",
600
+
"required": [],
601
+
"properties": {
602
+
"hideBadges": {
603
+
"description": "Hide the blue check badges for verified accounts and trusted verifiers.",
604
+
"type": "boolean",
605
+
"default": false
606
+
}
607
+
}
608
+
},
609
+
"postInteractionSettingsPref": {
610
+
"type": "object",
611
+
"description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly.",
612
+
"required": [],
613
+
"properties": {
614
+
"threadgateAllowRules": {
615
+
"description": "Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
616
+
"type": "array",
617
+
"maxLength": 5,
618
+
"items": {
619
+
"type": "union",
620
+
"refs": [
621
+
"app.bsky.feed.threadgate#mentionRule",
622
+
"app.bsky.feed.threadgate#followerRule",
623
+
"app.bsky.feed.threadgate#followingRule",
624
+
"app.bsky.feed.threadgate#listRule"
625
+
]
626
+
}
627
+
},
628
+
"postgateEmbeddingRules": {
629
+
"description": "Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.",
630
+
"type": "array",
631
+
"maxLength": 5,
632
+
"items": {
633
+
"type": "union",
634
+
"refs": ["app.bsky.feed.postgate#disableRule"]
635
+
}
636
+
}
637
+
}
638
+
},
639
+
"statusView": {
640
+
"type": "object",
641
+
"required": ["status", "record"],
642
+
"properties": {
643
+
"status": {
644
+
"type": "string",
645
+
"description": "The status for the account.",
646
+
"knownValues": ["app.bsky.actor.status#live"]
647
+
},
648
+
"record": { "type": "unknown" },
649
+
"embed": {
650
+
"type": "union",
651
+
"description": "An optional embed associated with the status.",
652
+
"refs": ["app.bsky.embed.external#view"]
653
+
},
654
+
"expiresAt": {
655
+
"type": "string",
656
+
"description": "The date when this status will expire. The application might choose to no longer return the status after expiration.",
657
+
"format": "datetime"
658
+
},
659
+
"isActive": {
660
+
"type": "boolean",
661
+
"description": "True if the status is not expired, false if it is expired. Only present if expiration was set."
662
+
}
663
+
}
664
+
}
665
+
}
666
+
}
+27
lexicons/app/bsky/actor/getPreferences.json
+27
lexicons/app/bsky/actor/getPreferences.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.getPreferences",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["preferences"],
17
+
"properties": {
18
+
"preferences": {
19
+
"type": "ref",
20
+
"ref": "app.bsky.actor.defs#preferences"
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+28
lexicons/app/bsky/actor/getProfile.json
+28
lexicons/app/bsky/actor/getProfile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.getProfile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "Handle or DID of account to fetch profile of."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "ref",
23
+
"ref": "app.bsky.actor.defs#profileViewDetailed"
24
+
}
25
+
}
26
+
}
27
+
}
28
+
}
+37
lexicons/app/bsky/actor/getProfiles.json
+37
lexicons/app/bsky/actor/getProfiles.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.getProfiles",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get detailed profile views of multiple actors.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actors"],
11
+
"properties": {
12
+
"actors": {
13
+
"type": "array",
14
+
"items": { "type": "string", "format": "at-identifier" },
15
+
"maxLength": 25
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["profiles"],
24
+
"properties": {
25
+
"profiles": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.actor.defs#profileViewDetailed"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+43
lexicons/app/bsky/actor/getSuggestions.json
+43
lexicons/app/bsky/actor/getSuggestions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.getSuggestions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["actors"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"actors": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.actor.defs#profileView"
32
+
}
33
+
},
34
+
"recId": {
35
+
"type": "integer",
36
+
"description": "Snowflake for this recommendation, use when submitting recommendation events."
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+60
lexicons/app/bsky/actor/profile.json
+60
lexicons/app/bsky/actor/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of a Bluesky account profile.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"properties": {
12
+
"displayName": {
13
+
"type": "string",
14
+
"maxGraphemes": 64,
15
+
"maxLength": 640
16
+
},
17
+
"description": {
18
+
"type": "string",
19
+
"description": "Free-form profile description text.",
20
+
"maxGraphemes": 256,
21
+
"maxLength": 2560
22
+
},
23
+
"pronouns": {
24
+
"type": "string",
25
+
"description": "Free-form pronouns text.",
26
+
"maxGraphemes": 20,
27
+
"maxLength": 200
28
+
},
29
+
"website": { "type": "string", "format": "uri" },
30
+
"avatar": {
31
+
"type": "blob",
32
+
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
33
+
"accept": ["image/png", "image/jpeg"],
34
+
"maxSize": 1000000
35
+
},
36
+
"banner": {
37
+
"type": "blob",
38
+
"description": "Larger horizontal image to display behind profile view.",
39
+
"accept": ["image/png", "image/jpeg"],
40
+
"maxSize": 1000000
41
+
},
42
+
"labels": {
43
+
"type": "union",
44
+
"description": "Self-label values, specific to the Bluesky application, on the overall account.",
45
+
"refs": ["com.atproto.label.defs#selfLabels"]
46
+
},
47
+
"joinedViaStarterPack": {
48
+
"type": "ref",
49
+
"ref": "com.atproto.repo.strongRef"
50
+
},
51
+
"pinnedPost": {
52
+
"type": "ref",
53
+
"ref": "com.atproto.repo.strongRef"
54
+
},
55
+
"createdAt": { "type": "string", "format": "datetime" }
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
+23
lexicons/app/bsky/actor/putPreferences.json
+23
lexicons/app/bsky/actor/putPreferences.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.putPreferences",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Set the private preferences attached to the account.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["preferences"],
13
+
"properties": {
14
+
"preferences": {
15
+
"type": "ref",
16
+
"ref": "app.bsky.actor.defs#preferences"
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+47
lexicons/app/bsky/actor/searchActors.json
+47
lexicons/app/bsky/actor/searchActors.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.searchActors",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find actors (profiles) matching search criteria. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"term": {
12
+
"type": "string",
13
+
"description": "DEPRECATED: use 'q' instead."
14
+
},
15
+
"q": {
16
+
"type": "string",
17
+
"description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
18
+
},
19
+
"limit": {
20
+
"type": "integer",
21
+
"minimum": 1,
22
+
"maximum": 100,
23
+
"default": 25
24
+
},
25
+
"cursor": { "type": "string" }
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["actors"],
33
+
"properties": {
34
+
"cursor": { "type": "string" },
35
+
"actors": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "ref",
39
+
"ref": "app.bsky.actor.defs#profileView"
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
+45
lexicons/app/bsky/actor/searchActorsTypeahead.json
+45
lexicons/app/bsky/actor/searchActorsTypeahead.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.searchActorsTypeahead",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"term": {
12
+
"type": "string",
13
+
"description": "DEPRECATED: use 'q' instead."
14
+
},
15
+
"q": {
16
+
"type": "string",
17
+
"description": "Search query prefix; not a full query string."
18
+
},
19
+
"limit": {
20
+
"type": "integer",
21
+
"minimum": 1,
22
+
"maximum": 100,
23
+
"default": 10
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["actors"],
32
+
"properties": {
33
+
"actors": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.actor.defs#profileViewBasic"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+37
lexicons/app/bsky/actor/status.json
+37
lexicons/app/bsky/actor/status.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.status",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of a Bluesky account status.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["status", "createdAt"],
12
+
"properties": {
13
+
"status": {
14
+
"type": "string",
15
+
"description": "The status for the account.",
16
+
"knownValues": ["app.bsky.actor.status#live"]
17
+
},
18
+
"embed": {
19
+
"type": "union",
20
+
"description": "An optional embed associated with the status.",
21
+
"refs": ["app.bsky.embed.external"]
22
+
},
23
+
"durationMinutes": {
24
+
"type": "integer",
25
+
"description": "The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.",
26
+
"minimum": 1
27
+
},
28
+
"createdAt": { "type": "string", "format": "datetime" }
29
+
}
30
+
}
31
+
},
32
+
"live": {
33
+
"type": "token",
34
+
"description": "Advertises an account as currently offering live content."
35
+
}
36
+
}
37
+
}
+48
lexicons/app/bsky/ageassurance/begin.json
+48
lexicons/app/bsky/ageassurance/begin.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.ageassurance.begin",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Initiate Age Assurance for an account.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["email", "language", "countryCode"],
13
+
"properties": {
14
+
"email": {
15
+
"type": "string",
16
+
"description": "The user's email address to receive Age Assurance instructions."
17
+
},
18
+
"language": {
19
+
"type": "string",
20
+
"description": "The user's preferred language for communication during the Age Assurance process."
21
+
},
22
+
"countryCode": {
23
+
"type": "string",
24
+
"description": "An ISO 3166-1 alpha-2 code of the user's location."
25
+
},
26
+
"regionCode": {
27
+
"type": "string",
28
+
"description": "An optional ISO 3166-2 code of the user's region or state within the country."
29
+
}
30
+
}
31
+
}
32
+
},
33
+
"output": {
34
+
"encoding": "application/json",
35
+
"schema": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.ageassurance.defs#state"
38
+
}
39
+
},
40
+
"errors": [
41
+
{ "name": "InvalidEmail" },
42
+
{ "name": "DidTooLong" },
43
+
{ "name": "InvalidInitiation" },
44
+
{ "name": "RegionNotSupported" }
45
+
]
46
+
}
47
+
}
48
+
}
+251
lexicons/app/bsky/ageassurance/defs.json
+251
lexicons/app/bsky/ageassurance/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.ageassurance.defs",
4
+
"defs": {
5
+
"access": {
6
+
"description": "The access level granted based on Age Assurance data we've processed.",
7
+
"type": "string",
8
+
"knownValues": ["unknown", "none", "safe", "full"]
9
+
},
10
+
"status": {
11
+
"type": "string",
12
+
"description": "The status of the Age Assurance process.",
13
+
"knownValues": ["unknown", "pending", "assured", "blocked"]
14
+
},
15
+
"state": {
16
+
"type": "object",
17
+
"description": "The user's computed Age Assurance state.",
18
+
"required": ["status", "access"],
19
+
"properties": {
20
+
"lastInitiatedAt": {
21
+
"type": "string",
22
+
"format": "datetime",
23
+
"description": "The timestamp when this state was last updated."
24
+
},
25
+
"status": {
26
+
"type": "ref",
27
+
"ref": "app.bsky.ageassurance.defs#status"
28
+
},
29
+
"access": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.ageassurance.defs#access"
32
+
}
33
+
}
34
+
},
35
+
"stateMetadata": {
36
+
"type": "object",
37
+
"description": "Additional metadata needed to compute Age Assurance state client-side.",
38
+
"required": [],
39
+
"properties": {
40
+
"accountCreatedAt": {
41
+
"type": "string",
42
+
"format": "datetime",
43
+
"description": "The account creation timestamp."
44
+
}
45
+
}
46
+
},
47
+
"config": {
48
+
"type": "object",
49
+
"description": "",
50
+
"required": ["regions"],
51
+
"properties": {
52
+
"regions": {
53
+
"type": "array",
54
+
"description": "The per-region Age Assurance configuration.",
55
+
"items": {
56
+
"type": "ref",
57
+
"ref": "app.bsky.ageassurance.defs#configRegion"
58
+
}
59
+
}
60
+
}
61
+
},
62
+
"configRegion": {
63
+
"type": "object",
64
+
"description": "The Age Assurance configuration for a specific region.",
65
+
"required": ["countryCode", "rules"],
66
+
"properties": {
67
+
"countryCode": {
68
+
"type": "string",
69
+
"description": "The ISO 3166-1 alpha-2 country code this configuration applies to."
70
+
},
71
+
"regionCode": {
72
+
"type": "string",
73
+
"description": "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country."
74
+
},
75
+
"rules": {
76
+
"type": "array",
77
+
"description": "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.",
78
+
"items": {
79
+
"type": "union",
80
+
"refs": [
81
+
"#configRegionRuleDefault",
82
+
"#configRegionRuleIfDeclaredOverAge",
83
+
"#configRegionRuleIfDeclaredUnderAge",
84
+
"#configRegionRuleIfAssuredOverAge",
85
+
"#configRegionRuleIfAssuredUnderAge",
86
+
"#configRegionRuleIfAccountNewerThan",
87
+
"#configRegionRuleIfAccountOlderThan"
88
+
]
89
+
}
90
+
}
91
+
}
92
+
},
93
+
"configRegionRuleDefault": {
94
+
"type": "object",
95
+
"description": "Age Assurance rule that applies by default.",
96
+
"required": ["access"],
97
+
"properties": {
98
+
"access": {
99
+
"type": "ref",
100
+
"ref": "app.bsky.ageassurance.defs#access"
101
+
}
102
+
}
103
+
},
104
+
"configRegionRuleIfDeclaredOverAge": {
105
+
"type": "object",
106
+
"description": "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.",
107
+
"required": ["age", "access"],
108
+
"properties": {
109
+
"age": {
110
+
"type": "integer",
111
+
"description": "The age threshold as a whole integer."
112
+
},
113
+
"access": {
114
+
"type": "ref",
115
+
"ref": "app.bsky.ageassurance.defs#access"
116
+
}
117
+
}
118
+
},
119
+
"configRegionRuleIfDeclaredUnderAge": {
120
+
"type": "object",
121
+
"description": "Age Assurance rule that applies if the user has declared themselves under a certain age.",
122
+
"required": ["age", "access"],
123
+
"properties": {
124
+
"age": {
125
+
"type": "integer",
126
+
"description": "The age threshold as a whole integer."
127
+
},
128
+
"access": {
129
+
"type": "ref",
130
+
"ref": "app.bsky.ageassurance.defs#access"
131
+
}
132
+
}
133
+
},
134
+
"configRegionRuleIfAssuredOverAge": {
135
+
"type": "object",
136
+
"description": "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.",
137
+
"required": ["age", "access"],
138
+
"properties": {
139
+
"age": {
140
+
"type": "integer",
141
+
"description": "The age threshold as a whole integer."
142
+
},
143
+
"access": {
144
+
"type": "ref",
145
+
"ref": "app.bsky.ageassurance.defs#access"
146
+
}
147
+
}
148
+
},
149
+
"configRegionRuleIfAssuredUnderAge": {
150
+
"type": "object",
151
+
"description": "Age Assurance rule that applies if the user has been assured to be under a certain age.",
152
+
"required": ["age", "access"],
153
+
"properties": {
154
+
"age": {
155
+
"type": "integer",
156
+
"description": "The age threshold as a whole integer."
157
+
},
158
+
"access": {
159
+
"type": "ref",
160
+
"ref": "app.bsky.ageassurance.defs#access"
161
+
}
162
+
}
163
+
},
164
+
"configRegionRuleIfAccountNewerThan": {
165
+
"type": "object",
166
+
"description": "Age Assurance rule that applies if the account is equal-to or newer than a certain date.",
167
+
"required": ["date", "access"],
168
+
"properties": {
169
+
"date": {
170
+
"type": "string",
171
+
"format": "datetime",
172
+
"description": "The date threshold as a datetime string."
173
+
},
174
+
"access": {
175
+
"type": "ref",
176
+
"ref": "app.bsky.ageassurance.defs#access"
177
+
}
178
+
}
179
+
},
180
+
"configRegionRuleIfAccountOlderThan": {
181
+
"type": "object",
182
+
"description": "Age Assurance rule that applies if the account is older than a certain date.",
183
+
"required": ["date", "access"],
184
+
"properties": {
185
+
"date": {
186
+
"type": "string",
187
+
"format": "datetime",
188
+
"description": "The date threshold as a datetime string."
189
+
},
190
+
"access": {
191
+
"type": "ref",
192
+
"ref": "app.bsky.ageassurance.defs#access"
193
+
}
194
+
}
195
+
},
196
+
"event": {
197
+
"type": "object",
198
+
"description": "Object used to store Age Assurance data in stash.",
199
+
"required": ["createdAt", "status", "access", "attemptId", "countryCode"],
200
+
"properties": {
201
+
"createdAt": {
202
+
"type": "string",
203
+
"format": "datetime",
204
+
"description": "The date and time of this write operation."
205
+
},
206
+
"attemptId": {
207
+
"type": "string",
208
+
"description": "The unique identifier for this instance of the Age Assurance flow, in UUID format."
209
+
},
210
+
"status": {
211
+
"type": "string",
212
+
"description": "The status of the Age Assurance process.",
213
+
"knownValues": ["unknown", "pending", "assured", "blocked"]
214
+
},
215
+
"access": {
216
+
"description": "The access level granted based on Age Assurance data we've processed.",
217
+
"type": "string",
218
+
"knownValues": ["unknown", "none", "safe", "full"]
219
+
},
220
+
"countryCode": {
221
+
"type": "string",
222
+
"description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow."
223
+
},
224
+
"regionCode": {
225
+
"type": "string",
226
+
"description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow."
227
+
},
228
+
"email": {
229
+
"type": "string",
230
+
"description": "The email used for Age Assurance."
231
+
},
232
+
"initIp": {
233
+
"type": "string",
234
+
"description": "The IP address used when initiating the Age Assurance flow."
235
+
},
236
+
"initUa": {
237
+
"type": "string",
238
+
"description": "The user agent used when initiating the Age Assurance flow."
239
+
},
240
+
"completeIp": {
241
+
"type": "string",
242
+
"description": "The IP address used when completing the Age Assurance flow."
243
+
},
244
+
"completeUa": {
245
+
"type": "string",
246
+
"description": "The user agent used when completing the Age Assurance flow."
247
+
}
248
+
}
249
+
}
250
+
}
251
+
}
+17
lexicons/app/bsky/ageassurance/getConfig.json
+17
lexicons/app/bsky/ageassurance/getConfig.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.ageassurance.getConfig",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns Age Assurance configuration for use on the client.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "ref",
12
+
"ref": "app.bsky.ageassurance.defs#config"
13
+
}
14
+
}
15
+
}
16
+
}
17
+
}
+35
lexicons/app/bsky/ageassurance/getState.json
+35
lexicons/app/bsky/ageassurance/getState.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.ageassurance.getState",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["countryCode"],
11
+
"properties": {
12
+
"countryCode": { "type": "string" },
13
+
"regionCode": { "type": "string" }
14
+
}
15
+
},
16
+
"output": {
17
+
"encoding": "application/json",
18
+
"schema": {
19
+
"type": "object",
20
+
"required": ["state", "metadata"],
21
+
"properties": {
22
+
"state": {
23
+
"type": "ref",
24
+
"ref": "app.bsky.ageassurance.defs#state"
25
+
},
26
+
"metadata": {
27
+
"type": "ref",
28
+
"ref": "app.bsky.ageassurance.defs#stateMetadata"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
+35
lexicons/app/bsky/authCreatePosts.json
+35
lexicons/app/bsky/authCreatePosts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authCreatePosts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Create Bluesky Posts",
8
+
"title:lang": {},
9
+
"detail": "Can not update or delete posts.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "rpc",
15
+
"inheritAud": true,
16
+
"lxm": [
17
+
"app.bsky.video.uploadVideo",
18
+
"app.bsky.video.getJobStatus",
19
+
"app.bsky.video.getUploadLimits"
20
+
]
21
+
},
22
+
{
23
+
"type": "permission",
24
+
"resource": "repo",
25
+
"action": ["create"],
26
+
"collection": [
27
+
"app.bsky.feed.post",
28
+
"app.bsky.feed.postgate",
29
+
"app.bsky.feed.threadgate"
30
+
]
31
+
}
32
+
]
33
+
}
34
+
}
35
+
}
+137
lexicons/app/bsky/authFullApp.json
+137
lexicons/app/bsky/authFullApp.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authFullApp",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Full Bluesky Social App Permissions",
8
+
"title:lang": {},
9
+
"detail": "Manage all public content and interactions, private preferences and subscriptions, and other Bluesky-specific app features and data.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "rpc",
15
+
"inheritAud": true,
16
+
"lxm": [
17
+
"app.bsky.actor.getPreferences",
18
+
"app.bsky.actor.getProfile",
19
+
"app.bsky.actor.getProfiles",
20
+
"app.bsky.actor.getSuggestions",
21
+
"app.bsky.actor.putPreferences",
22
+
"app.bsky.actor.searchActors",
23
+
"app.bsky.actor.searchActorsTypeahead",
24
+
"app.bsky.bookmark.createBookmark",
25
+
"app.bsky.bookmark.deleteBookmark",
26
+
"app.bsky.bookmark.getBookmarks",
27
+
"app.bsky.contact.dismissMatch",
28
+
"app.bsky.contact.getMatches",
29
+
"app.bsky.contact.getSyncStatus",
30
+
"app.bsky.contact.importContacts",
31
+
"app.bsky.contact.removeData",
32
+
"app.bsky.contact.startPhoneVerification",
33
+
"app.bsky.contact.verifyPhone",
34
+
"app.bsky.feed.describeFeedGenerator",
35
+
"app.bsky.feed.getActorFeeds",
36
+
"app.bsky.feed.getActorLikes",
37
+
"app.bsky.feed.getAuthorFeed",
38
+
"app.bsky.feed.getFeed",
39
+
"app.bsky.feed.getFeedGenerator",
40
+
"app.bsky.feed.getFeedGenerators",
41
+
"app.bsky.feed.getFeedSkeleton",
42
+
"app.bsky.feed.getLikes",
43
+
"app.bsky.feed.getListFeed",
44
+
"app.bsky.feed.getPostThread",
45
+
"app.bsky.feed.getPosts",
46
+
"app.bsky.feed.getQuotes",
47
+
"app.bsky.feed.getRepostedBy",
48
+
"app.bsky.feed.getSuggestedFeeds",
49
+
"app.bsky.feed.getTimeline",
50
+
"app.bsky.feed.searchPosts",
51
+
"app.bsky.feed.sendInteractions",
52
+
"app.bsky.graph.getActorStarterPacks",
53
+
"app.bsky.graph.getBlocks",
54
+
"app.bsky.graph.getFollowers",
55
+
"app.bsky.graph.getFollows",
56
+
"app.bsky.graph.getKnownFollowers",
57
+
"app.bsky.graph.getList",
58
+
"app.bsky.graph.getListBlocks",
59
+
"app.bsky.graph.getListMutes",
60
+
"app.bsky.graph.getLists",
61
+
"app.bsky.graph.getListsWithMembership",
62
+
"app.bsky.graph.getMutes",
63
+
"app.bsky.graph.getRelationships",
64
+
"app.bsky.graph.getStarterPack",
65
+
"app.bsky.graph.getStarterPacks",
66
+
"app.bsky.graph.getStarterPacksWithMembership",
67
+
"app.bsky.graph.getSuggestedFollowsByActor",
68
+
"app.bsky.graph.muteActor",
69
+
"app.bsky.graph.muteActorList",
70
+
"app.bsky.graph.muteThread",
71
+
"app.bsky.graph.searchStarterPacks",
72
+
"app.bsky.graph.unmuteActor",
73
+
"app.bsky.graph.unmuteActorList",
74
+
"app.bsky.graph.unmuteThread",
75
+
"app.bsky.labeler.getServices",
76
+
"app.bsky.notification.getPreferences",
77
+
"app.bsky.notification.getUnreadCount",
78
+
"app.bsky.notification.listActivitySubscriptions",
79
+
"app.bsky.notification.listNotifications",
80
+
"app.bsky.notification.putActivitySubscription",
81
+
"app.bsky.notification.putPreferences",
82
+
"app.bsky.notification.putPreferencesV2",
83
+
"app.bsky.notification.registerPush",
84
+
"app.bsky.notification.registerPush",
85
+
"app.bsky.notification.unregisterPush",
86
+
"app.bsky.notification.updateSeen",
87
+
"app.bsky.unspecced.getAgeAssuranceState",
88
+
"app.bsky.unspecced.getConfig",
89
+
"app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
90
+
"app.bsky.unspecced.getPopularFeedGenerators",
91
+
"app.bsky.unspecced.getPostThreadOtherV2",
92
+
"app.bsky.unspecced.getPostThreadV2",
93
+
"app.bsky.unspecced.getSuggestedFeeds",
94
+
"app.bsky.unspecced.getSuggestedFeedsSkeleton",
95
+
"app.bsky.unspecced.getSuggestedStarterPacks",
96
+
"app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
97
+
"app.bsky.unspecced.getSuggestedUsers",
98
+
"app.bsky.unspecced.getSuggestedUsersSkeleton",
99
+
"app.bsky.unspecced.getSuggestionsSkeleton",
100
+
"app.bsky.unspecced.getTaggedSuggestions",
101
+
"app.bsky.unspecced.getTrendingTopics",
102
+
"app.bsky.unspecced.getTrends",
103
+
"app.bsky.unspecced.getTrendsSkeleton",
104
+
"app.bsky.unspecced.initAgeAssurance",
105
+
"app.bsky.unspecced.searchActorsSkeleton",
106
+
"app.bsky.unspecced.searchPostsSkeleton",
107
+
"app.bsky.unspecced.searchStarterPacksSkeleton",
108
+
"app.bsky.video.getJobStatus",
109
+
"app.bsky.video.getUploadLimits",
110
+
"app.bsky.video.uploadVideo"
111
+
]
112
+
},
113
+
{
114
+
"type": "permission",
115
+
"resource": "repo",
116
+
"action": ["create", "update", "delete"],
117
+
"collection": [
118
+
"app.bsky.actor.profile",
119
+
"app.bsky.actor.status",
120
+
"app.bsky.feed.like",
121
+
"app.bsky.feed.post",
122
+
"app.bsky.feed.postgate",
123
+
"app.bsky.feed.repost",
124
+
"app.bsky.feed.threadgate",
125
+
"app.bsky.graph.block",
126
+
"app.bsky.graph.follow",
127
+
"app.bsky.graph.list",
128
+
"app.bsky.graph.listblock",
129
+
"app.bsky.graph.listitem",
130
+
"app.bsky.graph.starterpack",
131
+
"app.bsky.notification.declaration"
132
+
]
133
+
}
134
+
]
135
+
}
136
+
}
137
+
}
+21
lexicons/app/bsky/authManageFeedDeclaration.json
+21
lexicons/app/bsky/authManageFeedDeclaration.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authManageFeedDeclarations",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Manage Hosted Feeds",
8
+
"title:lang": {},
9
+
"detail": "Configure feed generator declaration records.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "repo",
15
+
"action": ["create", "update", "delete"],
16
+
"collection": ["app.bsky.feed.generator"]
17
+
}
18
+
]
19
+
}
20
+
}
21
+
}
+21
lexicons/app/bsky/authManageLabelerService.json
+21
lexicons/app/bsky/authManageLabelerService.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authManageLabelerService",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Manage Hosted Labeling Service",
8
+
"title:lang": {},
9
+
"detail": "Configure labeler declaration records.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "repo",
15
+
"action": ["create", "update", "delete"],
16
+
"collection": ["app.bsky.labeler.service"]
17
+
}
18
+
]
19
+
}
20
+
}
21
+
}
+36
lexicons/app/bsky/authManageModeration.json
+36
lexicons/app/bsky/authManageModeration.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authManageModeration",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Manage Personal Moderation",
8
+
"title:lang": {},
9
+
"detail": "Control over blocks, mutes, mod lists, mod services, and preferences.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "rpc",
15
+
"inheritAud": true,
16
+
"lxm": [
17
+
"app.bsky.actor.getPreferences",
18
+
"app.bsky.actor.putPreferences",
19
+
"app.bsky.graph.muteActor",
20
+
"app.bsky.graph.muteActorList",
21
+
"app.bsky.graph.muteThread",
22
+
"app.bsky.graph.unmuteActor",
23
+
"app.bsky.graph.unmuteActorList",
24
+
"app.bsky.graph.unmuteThread"
25
+
]
26
+
},
27
+
{
28
+
"type": "permission",
29
+
"resource": "repo",
30
+
"action": ["create", "update", "delete"],
31
+
"collection": ["app.bsky.graph.block", "app.bsky.graph.listblock"]
32
+
}
33
+
]
34
+
}
35
+
}
36
+
}
+32
lexicons/app/bsky/authManageNotifications.json
+32
lexicons/app/bsky/authManageNotifications.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authManageNotifications",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Manage Bluesky Notifications",
8
+
"title:lang": {},
9
+
"detail": "View and configure notifications for the Bluesky app.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "rpc",
15
+
"inheritAud": true,
16
+
"lxm": [
17
+
"app.bsky.notification.getPreferences",
18
+
"app.bsky.notification.getUnreadCount",
19
+
"app.bsky.notification.listActivitySubscriptions",
20
+
"app.bsky.notification.listNotifications",
21
+
"app.bsky.notification.putActivitySubscription",
22
+
"app.bsky.notification.putPreferences",
23
+
"app.bsky.notification.putPreferencesV2",
24
+
"app.bsky.notification.registerPush",
25
+
"app.bsky.notification.unregisterPush",
26
+
"app.bsky.notification.updateSeen"
27
+
]
28
+
}
29
+
]
30
+
}
31
+
}
32
+
}
+25
lexicons/app/bsky/authManageProfile.json
+25
lexicons/app/bsky/authManageProfile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authManageProfile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Manage Bluesky Profile",
8
+
"title:lang": {},
9
+
"detail": "Update profile data, as well as status and public chat visibility.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "repo",
15
+
"action": ["create", "update", "delete"],
16
+
"collection": [
17
+
"app.bsky.actor.profile",
18
+
"app.bsky.actor.status",
19
+
"app.bsky.notification.declaration"
20
+
]
21
+
}
22
+
]
23
+
}
24
+
}
25
+
}
+88
lexicons/app/bsky/authViewAll.json
+88
lexicons/app/bsky/authViewAll.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.authViewAll",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Read-only access to all content",
8
+
"title:lang": {},
9
+
"detail": "View Bluesky network content from account perspective, and read all notifications and preferences.",
10
+
"detail:lang": {},
11
+
"permissions": [
12
+
{
13
+
"type": "permission",
14
+
"resource": "rpc",
15
+
"inheritAud": true,
16
+
"lxm": [
17
+
"app.bsky.actor.getProfile",
18
+
"app.bsky.actor.getProfiles",
19
+
"app.bsky.actor.getSuggestions",
20
+
"app.bsky.actor.searchActors",
21
+
"app.bsky.actor.searchActorsTypeahead",
22
+
"app.bsky.bookmark.getBookmarks",
23
+
"app.bsky.feed.describeFeedGenerator",
24
+
"app.bsky.feed.getActorFeeds",
25
+
"app.bsky.feed.getActorLikes",
26
+
"app.bsky.feed.getAuthorFeed",
27
+
"app.bsky.feed.getFeed",
28
+
"app.bsky.feed.getFeedGenerator",
29
+
"app.bsky.feed.getFeedGenerators",
30
+
"app.bsky.feed.getFeedSkeleton",
31
+
"app.bsky.feed.getLikes",
32
+
"app.bsky.feed.getListFeed",
33
+
"app.bsky.feed.getPostThread",
34
+
"app.bsky.feed.getPosts",
35
+
"app.bsky.feed.getQuotes",
36
+
"app.bsky.feed.getRepostedBy",
37
+
"app.bsky.feed.getSuggestedFeeds",
38
+
"app.bsky.feed.getTimeline",
39
+
"app.bsky.feed.searchPosts",
40
+
"app.bsky.graph.getActorStarterPacks",
41
+
"app.bsky.graph.getBlocks",
42
+
"app.bsky.graph.getFollowers",
43
+
"app.bsky.graph.getFollows",
44
+
"app.bsky.graph.getKnownFollowers",
45
+
"app.bsky.graph.getListBlocks",
46
+
"app.bsky.graph.getListMutes",
47
+
"app.bsky.graph.getLists",
48
+
"app.bsky.graph.getListsWithMembership",
49
+
"app.bsky.graph.getMutes",
50
+
"app.bsky.graph.getRelationships",
51
+
"app.bsky.graph.getStarterPack",
52
+
"app.bsky.graph.getStarterPacks",
53
+
"app.bsky.graph.getStarterPacksWithMembership",
54
+
"app.bsky.graph.getSuggestedFollowsByActor",
55
+
"app.bsky.graph.searchStarterPacks",
56
+
"app.bsky.labeler.getServices",
57
+
"app.bsky.notification.getPreferences",
58
+
"app.bsky.notification.getUnreadCount",
59
+
"app.bsky.notification.listActivitySubscriptions",
60
+
"app.bsky.notification.listNotifications",
61
+
"app.bsky.notification.updateSeen",
62
+
"app.bsky.unspecced.getAgeAssuranceState",
63
+
"app.bsky.unspecced.getConfig",
64
+
"app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
65
+
"app.bsky.unspecced.getPopularFeedGenerators",
66
+
"app.bsky.unspecced.getPostThreadOtherV2",
67
+
"app.bsky.unspecced.getPostThreadV2",
68
+
"app.bsky.unspecced.getSuggestedFeeds",
69
+
"app.bsky.unspecced.getSuggestedFeedsSkeleton",
70
+
"app.bsky.unspecced.getSuggestedStarterPacks",
71
+
"app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
72
+
"app.bsky.unspecced.getSuggestedUsers",
73
+
"app.bsky.unspecced.getSuggestedUsersSkeleton",
74
+
"app.bsky.unspecced.getSuggestionsSkeleton",
75
+
"app.bsky.unspecced.getTaggedSuggestions",
76
+
"app.bsky.unspecced.getTrendingTopics",
77
+
"app.bsky.unspecced.getTrends",
78
+
"app.bsky.unspecced.getTrendsSkeleton",
79
+
"app.bsky.unspecced.searchActorsSkeleton",
80
+
"app.bsky.unspecced.searchPostsSkeleton",
81
+
"app.bsky.unspecced.searchStarterPacksSkeleton",
82
+
"app.bsky.video.getUploadLimits"
83
+
]
84
+
}
85
+
]
86
+
}
87
+
}
88
+
}
+27
lexicons/app/bsky/bookmark/createBookmark.json
+27
lexicons/app/bsky/bookmark/createBookmark.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.bookmark.createBookmark",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["uri", "cid"],
13
+
"properties": {
14
+
"uri": { "type": "string", "format": "at-uri" },
15
+
"cid": { "type": "string", "format": "cid" }
16
+
}
17
+
}
18
+
},
19
+
"errors": [
20
+
{
21
+
"name": "UnsupportedCollection",
22
+
"description": "The URI to be bookmarked is for an unsupported collection."
23
+
}
24
+
]
25
+
}
26
+
}
27
+
}
+38
lexicons/app/bsky/bookmark/defs.json
+38
lexicons/app/bsky/bookmark/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.bookmark.defs",
4
+
"defs": {
5
+
"bookmark": {
6
+
"description": "Object used to store bookmark data in stash.",
7
+
"type": "object",
8
+
"required": ["subject"],
9
+
"properties": {
10
+
"subject": {
11
+
"description": "A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.",
12
+
"type": "ref",
13
+
"ref": "com.atproto.repo.strongRef"
14
+
}
15
+
}
16
+
},
17
+
"bookmarkView": {
18
+
"type": "object",
19
+
"required": ["subject", "item"],
20
+
"properties": {
21
+
"subject": {
22
+
"description": "A strong ref to the bookmarked record.",
23
+
"type": "ref",
24
+
"ref": "com.atproto.repo.strongRef"
25
+
},
26
+
"createdAt": { "type": "string", "format": "datetime" },
27
+
"item": {
28
+
"type": "union",
29
+
"refs": [
30
+
"app.bsky.feed.defs#blockedPost",
31
+
"app.bsky.feed.defs#notFoundPost",
32
+
"app.bsky.feed.defs#postView"
33
+
]
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
+26
lexicons/app/bsky/bookmark/deleteBookmark.json
+26
lexicons/app/bsky/bookmark/deleteBookmark.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.bookmark.deleteBookmark",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["uri"],
13
+
"properties": {
14
+
"uri": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
},
18
+
"errors": [
19
+
{
20
+
"name": "UnsupportedCollection",
21
+
"description": "The URI to be bookmarked is for an unsupported collection."
22
+
}
23
+
]
24
+
}
25
+
}
26
+
}
+39
lexicons/app/bsky/bookmark/getBookmarks.json
+39
lexicons/app/bsky/bookmark/getBookmarks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.bookmark.getBookmarks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Gets views of records bookmarked by the authenticated user. Requires authentication.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["bookmarks"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"bookmarks": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.bookmark.defs#bookmarkView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+57
lexicons/app/bsky/contact/defs.json
+57
lexicons/app/bsky/contact/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.defs",
4
+
"defs": {
5
+
"matchAndContactIndex": {
6
+
"description": "Associates a profile with the positional index of the contact import input in the call to `app.bsky.contact.importContacts`, so clients can know which phone caused a particular match.",
7
+
"type": "object",
8
+
"required": ["match", "contactIndex"],
9
+
"properties": {
10
+
"match": {
11
+
"description": "Profile of the matched user.",
12
+
"type": "ref",
13
+
"ref": "app.bsky.actor.defs#profileView"
14
+
},
15
+
"contactIndex": {
16
+
"description": "The index of this match in the import contact input.",
17
+
"type": "integer",
18
+
"minimum": 0,
19
+
"maximum": 999
20
+
}
21
+
}
22
+
},
23
+
"syncStatus": {
24
+
"type": "object",
25
+
"required": ["syncedAt", "matchesCount"],
26
+
"properties": {
27
+
"syncedAt": {
28
+
"description": "Last date when contacts where imported.",
29
+
"type": "string",
30
+
"format": "datetime"
31
+
},
32
+
"matchesCount": {
33
+
"description": "Number of existing contact matches resulting of the user imports and of their imported contacts having imported the user. Matches stop being counted when the user either follows the matched contact or dismisses the match.",
34
+
"type": "integer",
35
+
"minimum": 0
36
+
}
37
+
}
38
+
},
39
+
"notification": {
40
+
"description": "A stash object to be sent via bsync representing a notification to be created.",
41
+
"type": "object",
42
+
"required": ["from", "to"],
43
+
"properties": {
44
+
"from": {
45
+
"description": "The DID of who this notification comes from.",
46
+
"type": "string",
47
+
"format": "did"
48
+
},
49
+
"to": {
50
+
"description": "The DID of who this notification should go to.",
51
+
"type": "string",
52
+
"format": "did"
53
+
}
54
+
}
55
+
}
56
+
}
57
+
}
+39
lexicons/app/bsky/contact/dismissMatch.json
+39
lexicons/app/bsky/contact/dismissMatch.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.dismissMatch",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["subject"],
13
+
"properties": {
14
+
"subject": {
15
+
"description": "The subject's DID to dismiss the match with.",
16
+
"type": "string",
17
+
"format": "did"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"properties": {}
27
+
}
28
+
},
29
+
"errors": [
30
+
{
31
+
"name": "InvalidDid"
32
+
},
33
+
{
34
+
"name": "InternalError"
35
+
}
36
+
]
37
+
}
38
+
}
39
+
}
+53
lexicons/app/bsky/contact/getMatches.json
+53
lexicons/app/bsky/contact/getMatches.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.getMatches",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["matches"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"matches": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.actor.defs#profileView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
},
37
+
"errors": [
38
+
{
39
+
"name": "InvalidDid"
40
+
},
41
+
{
42
+
"name": "InvalidLimit"
43
+
},
44
+
{
45
+
"name": "InvalidCursor"
46
+
},
47
+
{
48
+
"name": "InternalError"
49
+
}
50
+
]
51
+
}
52
+
}
53
+
}
+35
lexicons/app/bsky/contact/getSyncStatus.json
+35
lexicons/app/bsky/contact/getSyncStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.getSyncStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Gets the user's current contact import status. Requires authentication.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"properties": {
17
+
"syncStatus": {
18
+
"description": "If present, indicates the user has imported their contacts. If not present, indicates the user never used the feature or called `app.bsky.contact.removeData` and didn't import again since.",
19
+
"type": "ref",
20
+
"ref": "app.bsky.contact.defs#syncStatus"
21
+
}
22
+
}
23
+
}
24
+
},
25
+
"errors": [
26
+
{
27
+
"name": "InvalidDid"
28
+
},
29
+
{
30
+
"name": "InternalError"
31
+
}
32
+
]
33
+
}
34
+
}
35
+
}
+66
lexicons/app/bsky/contact/importContacts.json
+66
lexicons/app/bsky/contact/importContacts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.importContacts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["token", "contacts"],
13
+
"properties": {
14
+
"token": {
15
+
"description": "JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.",
16
+
"type": "string"
17
+
},
18
+
"contacts": {
19
+
"description": "List of phone numbers in global E.164 format (e.g., '+12125550123'). Phone numbers that cannot be normalized into a valid phone number will be discarded. Should not repeat the 'phone' input used in `app.bsky.contact.verifyPhone`.",
20
+
"type": "array",
21
+
"items": {
22
+
"type": "string"
23
+
},
24
+
"minLength": 1,
25
+
"maxLength": 1000
26
+
}
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"required": ["matchesAndContactIndexes"],
35
+
"properties": {
36
+
"matchesAndContactIndexes": {
37
+
"description": "The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.",
38
+
"type": "array",
39
+
"items": {
40
+
"type": "ref",
41
+
"ref": "app.bsky.contact.defs#matchAndContactIndex"
42
+
}
43
+
}
44
+
}
45
+
}
46
+
},
47
+
"errors": [
48
+
{
49
+
"name": "InvalidDid"
50
+
},
51
+
{
52
+
"name": "InvalidContacts"
53
+
},
54
+
{
55
+
"name": "TooManyContacts"
56
+
},
57
+
{
58
+
"name": "InvalidToken"
59
+
},
60
+
{
61
+
"name": "InternalError"
62
+
}
63
+
]
64
+
}
65
+
}
66
+
}
+32
lexicons/app/bsky/contact/removeData.json
+32
lexicons/app/bsky/contact/removeData.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.removeData",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {}
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "object",
19
+
"properties": {}
20
+
}
21
+
},
22
+
"errors": [
23
+
{
24
+
"name": "InvalidDid"
25
+
},
26
+
{
27
+
"name": "InternalError"
28
+
}
29
+
]
30
+
}
31
+
}
32
+
}
+36
lexicons/app/bsky/contact/sendNotification.json
+36
lexicons/app/bsky/contact/sendNotification.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.sendNotification",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "System endpoint to send notifications related to contact imports. Requires role authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["from", "to"],
13
+
"properties": {
14
+
"from": {
15
+
"description": "The DID of who this notification comes from.",
16
+
"type": "string",
17
+
"format": "did"
18
+
},
19
+
"to": {
20
+
"description": "The DID of who this notification should go to.",
21
+
"type": "string",
22
+
"format": "did"
23
+
}
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"properties": {}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+44
lexicons/app/bsky/contact/startPhoneVerification.json
+44
lexicons/app/bsky/contact/startPhoneVerification.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.startPhoneVerification",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Starts a phone verification flow. The phone passed will receive a code via SMS that should be passed to `app.bsky.contact.verifyPhone`. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["phone"],
13
+
"properties": {
14
+
"phone": {
15
+
"description": "The phone number to receive the code via SMS.",
16
+
"type": "string"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"properties": {}
26
+
}
27
+
},
28
+
"errors": [
29
+
{
30
+
"name": "RateLimitExceeded"
31
+
},
32
+
{
33
+
"name": "InvalidDid"
34
+
},
35
+
{
36
+
"name": "InvalidPhone"
37
+
},
38
+
{
39
+
"name": "InternalError"
40
+
}
41
+
]
42
+
}
43
+
}
44
+
}
+57
lexicons/app/bsky/contact/verifyPhone.json
+57
lexicons/app/bsky/contact/verifyPhone.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.contact.verifyPhone",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["phone", "code"],
13
+
"properties": {
14
+
"phone": {
15
+
"description": "The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.",
16
+
"type": "string"
17
+
},
18
+
"code": {
19
+
"description": "The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.",
20
+
"type": "string"
21
+
}
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["token"],
30
+
"properties": {
31
+
"token": {
32
+
"description": "JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.",
33
+
"type": "string"
34
+
}
35
+
}
36
+
}
37
+
},
38
+
"errors": [
39
+
{
40
+
"name": "RateLimitExceeded"
41
+
},
42
+
{
43
+
"name": "InvalidDid"
44
+
},
45
+
{
46
+
"name": "InvalidPhone"
47
+
},
48
+
{
49
+
"name": "InvalidCode"
50
+
},
51
+
{
52
+
"name": "InternalError"
53
+
}
54
+
]
55
+
}
56
+
}
57
+
}
+15
lexicons/app/bsky/embed/defs.json
+15
lexicons/app/bsky/embed/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.defs",
4
+
"defs": {
5
+
"aspectRatio": {
6
+
"type": "object",
7
+
"description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.",
8
+
"required": ["width", "height"],
9
+
"properties": {
10
+
"width": { "type": "integer", "minimum": 1 },
11
+
"height": { "type": "integer", "minimum": 1 }
12
+
}
13
+
}
14
+
}
15
+
}
+51
lexicons/app/bsky/embed/external.json
+51
lexicons/app/bsky/embed/external.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.external",
4
+
"defs": {
5
+
"main": {
6
+
"type": "object",
7
+
"description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).",
8
+
"required": ["external"],
9
+
"properties": {
10
+
"external": {
11
+
"type": "ref",
12
+
"ref": "#external"
13
+
}
14
+
}
15
+
},
16
+
"external": {
17
+
"type": "object",
18
+
"required": ["uri", "title", "description"],
19
+
"properties": {
20
+
"uri": { "type": "string", "format": "uri" },
21
+
"title": { "type": "string" },
22
+
"description": { "type": "string" },
23
+
"thumb": {
24
+
"type": "blob",
25
+
"accept": ["image/*"],
26
+
"maxSize": 1000000
27
+
}
28
+
}
29
+
},
30
+
"view": {
31
+
"type": "object",
32
+
"required": ["external"],
33
+
"properties": {
34
+
"external": {
35
+
"type": "ref",
36
+
"ref": "#viewExternal"
37
+
}
38
+
}
39
+
},
40
+
"viewExternal": {
41
+
"type": "object",
42
+
"required": ["uri", "title", "description"],
43
+
"properties": {
44
+
"uri": { "type": "string", "format": "uri" },
45
+
"title": { "type": "string" },
46
+
"description": { "type": "string" },
47
+
"thumb": { "type": "string", "format": "uri" }
48
+
}
49
+
}
50
+
}
51
+
}
+72
lexicons/app/bsky/embed/images.json
+72
lexicons/app/bsky/embed/images.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.images",
4
+
"description": "A set of images embedded in a Bluesky record (eg, a post).",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["images"],
9
+
"properties": {
10
+
"images": {
11
+
"type": "array",
12
+
"items": { "type": "ref", "ref": "#image" },
13
+
"maxLength": 4
14
+
}
15
+
}
16
+
},
17
+
"image": {
18
+
"type": "object",
19
+
"required": ["image", "alt"],
20
+
"properties": {
21
+
"image": {
22
+
"type": "blob",
23
+
"accept": ["image/*"],
24
+
"maxSize": 1000000
25
+
},
26
+
"alt": {
27
+
"type": "string",
28
+
"description": "Alt text description of the image, for accessibility."
29
+
},
30
+
"aspectRatio": {
31
+
"type": "ref",
32
+
"ref": "app.bsky.embed.defs#aspectRatio"
33
+
}
34
+
}
35
+
},
36
+
"view": {
37
+
"type": "object",
38
+
"required": ["images"],
39
+
"properties": {
40
+
"images": {
41
+
"type": "array",
42
+
"items": { "type": "ref", "ref": "#viewImage" },
43
+
"maxLength": 4
44
+
}
45
+
}
46
+
},
47
+
"viewImage": {
48
+
"type": "object",
49
+
"required": ["thumb", "fullsize", "alt"],
50
+
"properties": {
51
+
"thumb": {
52
+
"type": "string",
53
+
"format": "uri",
54
+
"description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."
55
+
},
56
+
"fullsize": {
57
+
"type": "string",
58
+
"format": "uri",
59
+
"description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."
60
+
},
61
+
"alt": {
62
+
"type": "string",
63
+
"description": "Alt text description of the image, for accessibility."
64
+
},
65
+
"aspectRatio": {
66
+
"type": "ref",
67
+
"ref": "app.bsky.embed.defs#aspectRatio"
68
+
}
69
+
}
70
+
}
71
+
}
72
+
}
+96
lexicons/app/bsky/embed/record.json
+96
lexicons/app/bsky/embed/record.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.record",
4
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["record"],
9
+
"properties": {
10
+
"record": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
11
+
}
12
+
},
13
+
"view": {
14
+
"type": "object",
15
+
"required": ["record"],
16
+
"properties": {
17
+
"record": {
18
+
"type": "union",
19
+
"refs": [
20
+
"#viewRecord",
21
+
"#viewNotFound",
22
+
"#viewBlocked",
23
+
"#viewDetached",
24
+
"app.bsky.feed.defs#generatorView",
25
+
"app.bsky.graph.defs#listView",
26
+
"app.bsky.labeler.defs#labelerView",
27
+
"app.bsky.graph.defs#starterPackViewBasic"
28
+
]
29
+
}
30
+
}
31
+
},
32
+
"viewRecord": {
33
+
"type": "object",
34
+
"required": ["uri", "cid", "author", "value", "indexedAt"],
35
+
"properties": {
36
+
"uri": { "type": "string", "format": "at-uri" },
37
+
"cid": { "type": "string", "format": "cid" },
38
+
"author": {
39
+
"type": "ref",
40
+
"ref": "app.bsky.actor.defs#profileViewBasic"
41
+
},
42
+
"value": {
43
+
"type": "unknown",
44
+
"description": "The record data itself."
45
+
},
46
+
"labels": {
47
+
"type": "array",
48
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
49
+
},
50
+
"replyCount": { "type": "integer" },
51
+
"repostCount": { "type": "integer" },
52
+
"likeCount": { "type": "integer" },
53
+
"quoteCount": { "type": "integer" },
54
+
"embeds": {
55
+
"type": "array",
56
+
"items": {
57
+
"type": "union",
58
+
"refs": [
59
+
"app.bsky.embed.images#view",
60
+
"app.bsky.embed.video#view",
61
+
"app.bsky.embed.external#view",
62
+
"app.bsky.embed.record#view",
63
+
"app.bsky.embed.recordWithMedia#view"
64
+
]
65
+
}
66
+
},
67
+
"indexedAt": { "type": "string", "format": "datetime" }
68
+
}
69
+
},
70
+
"viewNotFound": {
71
+
"type": "object",
72
+
"required": ["uri", "notFound"],
73
+
"properties": {
74
+
"uri": { "type": "string", "format": "at-uri" },
75
+
"notFound": { "type": "boolean", "const": true }
76
+
}
77
+
},
78
+
"viewBlocked": {
79
+
"type": "object",
80
+
"required": ["uri", "blocked", "author"],
81
+
"properties": {
82
+
"uri": { "type": "string", "format": "at-uri" },
83
+
"blocked": { "type": "boolean", "const": true },
84
+
"author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" }
85
+
}
86
+
},
87
+
"viewDetached": {
88
+
"type": "object",
89
+
"required": ["uri", "detached"],
90
+
"properties": {
91
+
"uri": { "type": "string", "format": "at-uri" },
92
+
"detached": { "type": "boolean", "const": true }
93
+
}
94
+
}
95
+
}
96
+
}
+43
lexicons/app/bsky/embed/recordWithMedia.json
+43
lexicons/app/bsky/embed/recordWithMedia.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.recordWithMedia",
4
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["record", "media"],
9
+
"properties": {
10
+
"record": {
11
+
"type": "ref",
12
+
"ref": "app.bsky.embed.record"
13
+
},
14
+
"media": {
15
+
"type": "union",
16
+
"refs": [
17
+
"app.bsky.embed.images",
18
+
"app.bsky.embed.video",
19
+
"app.bsky.embed.external"
20
+
]
21
+
}
22
+
}
23
+
},
24
+
"view": {
25
+
"type": "object",
26
+
"required": ["record", "media"],
27
+
"properties": {
28
+
"record": {
29
+
"type": "ref",
30
+
"ref": "app.bsky.embed.record#view"
31
+
},
32
+
"media": {
33
+
"type": "union",
34
+
"refs": [
35
+
"app.bsky.embed.images#view",
36
+
"app.bsky.embed.video#view",
37
+
"app.bsky.embed.external#view"
38
+
]
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+67
lexicons/app/bsky/embed/video.json
+67
lexicons/app/bsky/embed/video.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.video",
4
+
"description": "A video embedded in a Bluesky record (eg, a post).",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["video"],
9
+
"properties": {
10
+
"video": {
11
+
"type": "blob",
12
+
"description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb.",
13
+
"accept": ["video/mp4"],
14
+
"maxSize": 100000000
15
+
},
16
+
"captions": {
17
+
"type": "array",
18
+
"items": { "type": "ref", "ref": "#caption" },
19
+
"maxLength": 20
20
+
},
21
+
"alt": {
22
+
"type": "string",
23
+
"description": "Alt text description of the video, for accessibility.",
24
+
"maxGraphemes": 1000,
25
+
"maxLength": 10000
26
+
},
27
+
"aspectRatio": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.embed.defs#aspectRatio"
30
+
}
31
+
}
32
+
},
33
+
"caption": {
34
+
"type": "object",
35
+
"required": ["lang", "file"],
36
+
"properties": {
37
+
"lang": {
38
+
"type": "string",
39
+
"format": "language"
40
+
},
41
+
"file": {
42
+
"type": "blob",
43
+
"accept": ["text/vtt"],
44
+
"maxSize": 20000
45
+
}
46
+
}
47
+
},
48
+
"view": {
49
+
"type": "object",
50
+
"required": ["cid", "playlist"],
51
+
"properties": {
52
+
"cid": { "type": "string", "format": "cid" },
53
+
"playlist": { "type": "string", "format": "uri" },
54
+
"thumbnail": { "type": "string", "format": "uri" },
55
+
"alt": {
56
+
"type": "string",
57
+
"maxGraphemes": 1000,
58
+
"maxLength": 10000
59
+
},
60
+
"aspectRatio": {
61
+
"type": "ref",
62
+
"ref": "app.bsky.embed.defs#aspectRatio"
63
+
}
64
+
}
65
+
}
66
+
}
67
+
}
+331
lexicons/app/bsky/feed/defs.json
+331
lexicons/app/bsky/feed/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.defs",
4
+
"defs": {
5
+
"postView": {
6
+
"type": "object",
7
+
"required": ["uri", "cid", "author", "record", "indexedAt"],
8
+
"properties": {
9
+
"uri": { "type": "string", "format": "at-uri" },
10
+
"cid": { "type": "string", "format": "cid" },
11
+
"author": {
12
+
"type": "ref",
13
+
"ref": "app.bsky.actor.defs#profileViewBasic"
14
+
},
15
+
"record": { "type": "unknown" },
16
+
"embed": {
17
+
"type": "union",
18
+
"refs": [
19
+
"app.bsky.embed.images#view",
20
+
"app.bsky.embed.video#view",
21
+
"app.bsky.embed.external#view",
22
+
"app.bsky.embed.record#view",
23
+
"app.bsky.embed.recordWithMedia#view"
24
+
]
25
+
},
26
+
"bookmarkCount": { "type": "integer" },
27
+
"replyCount": { "type": "integer" },
28
+
"repostCount": { "type": "integer" },
29
+
"likeCount": { "type": "integer" },
30
+
"quoteCount": { "type": "integer" },
31
+
"indexedAt": { "type": "string", "format": "datetime" },
32
+
"viewer": { "type": "ref", "ref": "#viewerState" },
33
+
"labels": {
34
+
"type": "array",
35
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
36
+
},
37
+
"threadgate": { "type": "ref", "ref": "#threadgateView" },
38
+
"debug": {
39
+
"type": "unknown",
40
+
"description": "Debug information for internal development"
41
+
}
42
+
}
43
+
},
44
+
"viewerState": {
45
+
"type": "object",
46
+
"description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.",
47
+
"properties": {
48
+
"repost": { "type": "string", "format": "at-uri" },
49
+
"like": { "type": "string", "format": "at-uri" },
50
+
"bookmarked": { "type": "boolean" },
51
+
"threadMuted": { "type": "boolean" },
52
+
"replyDisabled": { "type": "boolean" },
53
+
"embeddingDisabled": { "type": "boolean" },
54
+
"pinned": { "type": "boolean" }
55
+
}
56
+
},
57
+
"threadContext": {
58
+
"type": "object",
59
+
"description": "Metadata about this post within the context of the thread it is in.",
60
+
"properties": {
61
+
"rootAuthorLike": { "type": "string", "format": "at-uri" }
62
+
}
63
+
},
64
+
"feedViewPost": {
65
+
"type": "object",
66
+
"required": ["post"],
67
+
"properties": {
68
+
"post": { "type": "ref", "ref": "#postView" },
69
+
"reply": { "type": "ref", "ref": "#replyRef" },
70
+
"reason": { "type": "union", "refs": ["#reasonRepost", "#reasonPin"] },
71
+
"feedContext": {
72
+
"type": "string",
73
+
"description": "Context provided by feed generator that may be passed back alongside interactions.",
74
+
"maxLength": 2000
75
+
},
76
+
"reqId": {
77
+
"type": "string",
78
+
"description": "Unique identifier per request that may be passed back alongside interactions.",
79
+
"maxLength": 100
80
+
}
81
+
}
82
+
},
83
+
"replyRef": {
84
+
"type": "object",
85
+
"required": ["root", "parent"],
86
+
"properties": {
87
+
"root": {
88
+
"type": "union",
89
+
"refs": ["#postView", "#notFoundPost", "#blockedPost"]
90
+
},
91
+
"parent": {
92
+
"type": "union",
93
+
"refs": ["#postView", "#notFoundPost", "#blockedPost"]
94
+
},
95
+
"grandparentAuthor": {
96
+
"type": "ref",
97
+
"ref": "app.bsky.actor.defs#profileViewBasic",
98
+
"description": "When parent is a reply to another post, this is the author of that post."
99
+
}
100
+
}
101
+
},
102
+
"reasonRepost": {
103
+
"type": "object",
104
+
"required": ["by", "indexedAt"],
105
+
"properties": {
106
+
"by": { "type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic" },
107
+
"uri": { "type": "string", "format": "at-uri" },
108
+
"cid": { "type": "string", "format": "cid" },
109
+
"indexedAt": { "type": "string", "format": "datetime" }
110
+
}
111
+
},
112
+
"reasonPin": {
113
+
"type": "object",
114
+
"properties": {}
115
+
},
116
+
"threadViewPost": {
117
+
"type": "object",
118
+
"required": ["post"],
119
+
"properties": {
120
+
"post": { "type": "ref", "ref": "#postView" },
121
+
"parent": {
122
+
"type": "union",
123
+
"refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]
124
+
},
125
+
"replies": {
126
+
"type": "array",
127
+
"items": {
128
+
"type": "union",
129
+
"refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]
130
+
}
131
+
},
132
+
"threadContext": { "type": "ref", "ref": "#threadContext" }
133
+
}
134
+
},
135
+
"notFoundPost": {
136
+
"type": "object",
137
+
"required": ["uri", "notFound"],
138
+
"properties": {
139
+
"uri": { "type": "string", "format": "at-uri" },
140
+
"notFound": { "type": "boolean", "const": true }
141
+
}
142
+
},
143
+
"blockedPost": {
144
+
"type": "object",
145
+
"required": ["uri", "blocked", "author"],
146
+
"properties": {
147
+
"uri": { "type": "string", "format": "at-uri" },
148
+
"blocked": { "type": "boolean", "const": true },
149
+
"author": { "type": "ref", "ref": "#blockedAuthor" }
150
+
}
151
+
},
152
+
"blockedAuthor": {
153
+
"type": "object",
154
+
"required": ["did"],
155
+
"properties": {
156
+
"did": { "type": "string", "format": "did" },
157
+
"viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" }
158
+
}
159
+
},
160
+
"generatorView": {
161
+
"type": "object",
162
+
"required": ["uri", "cid", "did", "creator", "displayName", "indexedAt"],
163
+
"properties": {
164
+
"uri": { "type": "string", "format": "at-uri" },
165
+
"cid": { "type": "string", "format": "cid" },
166
+
"did": { "type": "string", "format": "did" },
167
+
"creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
168
+
"displayName": { "type": "string" },
169
+
"description": {
170
+
"type": "string",
171
+
"maxGraphemes": 300,
172
+
"maxLength": 3000
173
+
},
174
+
"descriptionFacets": {
175
+
"type": "array",
176
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
177
+
},
178
+
"avatar": { "type": "string", "format": "uri" },
179
+
"likeCount": { "type": "integer", "minimum": 0 },
180
+
"acceptsInteractions": { "type": "boolean" },
181
+
"labels": {
182
+
"type": "array",
183
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
184
+
},
185
+
"viewer": { "type": "ref", "ref": "#generatorViewerState" },
186
+
"contentMode": {
187
+
"type": "string",
188
+
"knownValues": [
189
+
"app.bsky.feed.defs#contentModeUnspecified",
190
+
"app.bsky.feed.defs#contentModeVideo"
191
+
]
192
+
},
193
+
"indexedAt": { "type": "string", "format": "datetime" }
194
+
}
195
+
},
196
+
"generatorViewerState": {
197
+
"type": "object",
198
+
"properties": {
199
+
"like": { "type": "string", "format": "at-uri" }
200
+
}
201
+
},
202
+
"skeletonFeedPost": {
203
+
"type": "object",
204
+
"required": ["post"],
205
+
"properties": {
206
+
"post": { "type": "string", "format": "at-uri" },
207
+
"reason": {
208
+
"type": "union",
209
+
"refs": ["#skeletonReasonRepost", "#skeletonReasonPin"]
210
+
},
211
+
"feedContext": {
212
+
"type": "string",
213
+
"description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions.",
214
+
"maxLength": 2000
215
+
}
216
+
}
217
+
},
218
+
"skeletonReasonRepost": {
219
+
"type": "object",
220
+
"required": ["repost"],
221
+
"properties": {
222
+
"repost": { "type": "string", "format": "at-uri" }
223
+
}
224
+
},
225
+
"skeletonReasonPin": {
226
+
"type": "object",
227
+
"properties": {}
228
+
},
229
+
"threadgateView": {
230
+
"type": "object",
231
+
"properties": {
232
+
"uri": { "type": "string", "format": "at-uri" },
233
+
"cid": { "type": "string", "format": "cid" },
234
+
"record": { "type": "unknown" },
235
+
"lists": {
236
+
"type": "array",
237
+
"items": { "type": "ref", "ref": "app.bsky.graph.defs#listViewBasic" }
238
+
}
239
+
}
240
+
},
241
+
"interaction": {
242
+
"type": "object",
243
+
"properties": {
244
+
"item": { "type": "string", "format": "at-uri" },
245
+
"event": {
246
+
"type": "string",
247
+
"knownValues": [
248
+
"app.bsky.feed.defs#requestLess",
249
+
"app.bsky.feed.defs#requestMore",
250
+
"app.bsky.feed.defs#clickthroughItem",
251
+
"app.bsky.feed.defs#clickthroughAuthor",
252
+
"app.bsky.feed.defs#clickthroughReposter",
253
+
"app.bsky.feed.defs#clickthroughEmbed",
254
+
"app.bsky.feed.defs#interactionSeen",
255
+
"app.bsky.feed.defs#interactionLike",
256
+
"app.bsky.feed.defs#interactionRepost",
257
+
"app.bsky.feed.defs#interactionReply",
258
+
"app.bsky.feed.defs#interactionQuote",
259
+
"app.bsky.feed.defs#interactionShare"
260
+
]
261
+
},
262
+
"feedContext": {
263
+
"type": "string",
264
+
"description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.",
265
+
"maxLength": 2000
266
+
},
267
+
"reqId": {
268
+
"type": "string",
269
+
"description": "Unique identifier per request that may be passed back alongside interactions.",
270
+
"maxLength": 100
271
+
}
272
+
}
273
+
},
274
+
"requestLess": {
275
+
"type": "token",
276
+
"description": "Request that less content like the given feed item be shown in the feed"
277
+
},
278
+
"requestMore": {
279
+
"type": "token",
280
+
"description": "Request that more content like the given feed item be shown in the feed"
281
+
},
282
+
"clickthroughItem": {
283
+
"type": "token",
284
+
"description": "User clicked through to the feed item"
285
+
},
286
+
"clickthroughAuthor": {
287
+
"type": "token",
288
+
"description": "User clicked through to the author of the feed item"
289
+
},
290
+
"clickthroughReposter": {
291
+
"type": "token",
292
+
"description": "User clicked through to the reposter of the feed item"
293
+
},
294
+
"clickthroughEmbed": {
295
+
"type": "token",
296
+
"description": "User clicked through to the embedded content of the feed item"
297
+
},
298
+
"contentModeUnspecified": {
299
+
"type": "token",
300
+
"description": "Declares the feed generator returns any types of posts."
301
+
},
302
+
"contentModeVideo": {
303
+
"type": "token",
304
+
"description": "Declares the feed generator returns posts containing app.bsky.embed.video embeds."
305
+
},
306
+
"interactionSeen": {
307
+
"type": "token",
308
+
"description": "Feed item was seen by user"
309
+
},
310
+
"interactionLike": {
311
+
"type": "token",
312
+
"description": "User liked the feed item"
313
+
},
314
+
"interactionRepost": {
315
+
"type": "token",
316
+
"description": "User reposted the feed item"
317
+
},
318
+
"interactionReply": {
319
+
"type": "token",
320
+
"description": "User replied to the feed item"
321
+
},
322
+
"interactionQuote": {
323
+
"type": "token",
324
+
"description": "User quoted the feed item"
325
+
},
326
+
"interactionShare": {
327
+
"type": "token",
328
+
"description": "User shared the feed item"
329
+
}
330
+
}
331
+
}
+39
lexicons/app/bsky/feed/describeFeedGenerator.json
+39
lexicons/app/bsky/feed/describeFeedGenerator.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.describeFeedGenerator",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "feeds"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"feeds": {
16
+
"type": "array",
17
+
"items": { "type": "ref", "ref": "#feed" }
18
+
},
19
+
"links": { "type": "ref", "ref": "#links" }
20
+
}
21
+
}
22
+
}
23
+
},
24
+
"feed": {
25
+
"type": "object",
26
+
"required": ["uri"],
27
+
"properties": {
28
+
"uri": { "type": "string", "format": "at-uri" }
29
+
}
30
+
},
31
+
"links": {
32
+
"type": "object",
33
+
"properties": {
34
+
"privacyPolicy": { "type": "string" },
35
+
"termsOfService": { "type": "string" }
36
+
}
37
+
}
38
+
}
39
+
}
+54
lexicons/app/bsky/feed/generator.json
+54
lexicons/app/bsky/feed/generator.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.generator",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.",
8
+
"key": "any",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["did", "displayName", "createdAt"],
12
+
"properties": {
13
+
"did": { "type": "string", "format": "did" },
14
+
"displayName": {
15
+
"type": "string",
16
+
"maxGraphemes": 24,
17
+
"maxLength": 240
18
+
},
19
+
"description": {
20
+
"type": "string",
21
+
"maxGraphemes": 300,
22
+
"maxLength": 3000
23
+
},
24
+
"descriptionFacets": {
25
+
"type": "array",
26
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
27
+
},
28
+
"avatar": {
29
+
"type": "blob",
30
+
"accept": ["image/png", "image/jpeg"],
31
+
"maxSize": 1000000
32
+
},
33
+
"acceptsInteractions": {
34
+
"type": "boolean",
35
+
"description": "Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions"
36
+
},
37
+
"labels": {
38
+
"type": "union",
39
+
"description": "Self-label values",
40
+
"refs": ["com.atproto.label.defs#selfLabels"]
41
+
},
42
+
"contentMode": {
43
+
"type": "string",
44
+
"knownValues": [
45
+
"app.bsky.feed.defs#contentModeUnspecified",
46
+
"app.bsky.feed.defs#contentModeVideo"
47
+
]
48
+
},
49
+
"createdAt": { "type": "string", "format": "datetime" }
50
+
}
51
+
}
52
+
}
53
+
}
54
+
}
+41
lexicons/app/bsky/feed/getActorFeeds.json
+41
lexicons/app/bsky/feed/getActorFeeds.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getActorFeeds",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["feeds"],
27
+
"properties": {
28
+
"cursor": { "type": "string" },
29
+
"feeds": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.feed.defs#generatorView"
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
+42
lexicons/app/bsky/feed/getActorLikes.json
+42
lexicons/app/bsky/feed/getActorLikes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getActorLikes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["feed"],
27
+
"properties": {
28
+
"cursor": { "type": "string" },
29
+
"feed": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.feed.defs#feedViewPost"
34
+
}
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }]
40
+
}
41
+
}
42
+
}
+58
lexicons/app/bsky/feed/getAuthorFeed.json
+58
lexicons/app/bsky/feed/getAuthorFeed.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getAuthorFeed",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" },
20
+
"filter": {
21
+
"type": "string",
22
+
"description": "Combinations of post/repost types to include in response.",
23
+
"knownValues": [
24
+
"posts_with_replies",
25
+
"posts_no_replies",
26
+
"posts_with_media",
27
+
"posts_and_author_threads",
28
+
"posts_with_video"
29
+
],
30
+
"default": "posts_with_replies"
31
+
},
32
+
"includePins": {
33
+
"type": "boolean",
34
+
"default": false
35
+
}
36
+
}
37
+
},
38
+
"output": {
39
+
"encoding": "application/json",
40
+
"schema": {
41
+
"type": "object",
42
+
"required": ["feed"],
43
+
"properties": {
44
+
"cursor": { "type": "string" },
45
+
"feed": {
46
+
"type": "array",
47
+
"items": {
48
+
"type": "ref",
49
+
"ref": "app.bsky.feed.defs#feedViewPost"
50
+
}
51
+
}
52
+
}
53
+
}
54
+
},
55
+
"errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }]
56
+
}
57
+
}
58
+
}
+42
lexicons/app/bsky/feed/getFeed.json
+42
lexicons/app/bsky/feed/getFeed.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getFeed",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a hydrated feed from an actor's selected feed generator. Implemented by App View.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["feed"],
11
+
"properties": {
12
+
"feed": { "type": "string", "format": "at-uri" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["feed"],
27
+
"properties": {
28
+
"cursor": { "type": "string" },
29
+
"feed": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.feed.defs#feedViewPost"
34
+
}
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"errors": [{ "name": "UnknownFeed" }]
40
+
}
41
+
}
42
+
}
+42
lexicons/app/bsky/feed/getFeedGenerator.json
+42
lexicons/app/bsky/feed/getFeedGenerator.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getFeedGenerator",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about a feed generator. Implemented by AppView.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["feed"],
11
+
"properties": {
12
+
"feed": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "AT-URI of the feed generator record."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["view", "isOnline", "isValid"],
24
+
"properties": {
25
+
"view": {
26
+
"type": "ref",
27
+
"ref": "app.bsky.feed.defs#generatorView"
28
+
},
29
+
"isOnline": {
30
+
"type": "boolean",
31
+
"description": "Indicates whether the feed generator service has been online recently, or else seems to be inactive."
32
+
},
33
+
"isValid": {
34
+
"type": "boolean",
35
+
"description": "Indicates whether the feed generator service is compatible with the record declaration."
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+36
lexicons/app/bsky/feed/getFeedGenerators.json
+36
lexicons/app/bsky/feed/getFeedGenerators.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getFeedGenerators",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about a list of feed generators.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["feeds"],
11
+
"properties": {
12
+
"feeds": {
13
+
"type": "array",
14
+
"items": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["feeds"],
23
+
"properties": {
24
+
"feeds": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "ref",
28
+
"ref": "app.bsky.feed.defs#generatorView"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+51
lexicons/app/bsky/feed/getFeedSkeleton.json
+51
lexicons/app/bsky/feed/getFeedSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getFeedSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["feed"],
11
+
"properties": {
12
+
"feed": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference to feed generator record describing the specific feed being requested."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" }
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["feed"],
31
+
"properties": {
32
+
"cursor": { "type": "string" },
33
+
"feed": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.feed.defs#skeletonFeedPost"
38
+
}
39
+
},
40
+
"reqId": {
41
+
"type": "string",
42
+
"description": "Unique identifier per request that may be passed back alongside interactions.",
43
+
"maxLength": 100
44
+
}
45
+
}
46
+
}
47
+
},
48
+
"errors": [{ "name": "UnknownFeed" }]
49
+
}
50
+
}
51
+
}
+58
lexicons/app/bsky/feed/getLikes.json
+58
lexicons/app/bsky/feed/getLikes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getLikes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get like records which reference a subject (by AT-URI and CID).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uri"],
11
+
"properties": {
12
+
"uri": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "AT-URI of the subject (eg, a post record)."
16
+
},
17
+
"cid": {
18
+
"type": "string",
19
+
"format": "cid",
20
+
"description": "CID of the subject record (aka, specific version of record), to filter likes."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 100,
26
+
"default": 50
27
+
},
28
+
"cursor": { "type": "string" }
29
+
}
30
+
},
31
+
"output": {
32
+
"encoding": "application/json",
33
+
"schema": {
34
+
"type": "object",
35
+
"required": ["uri", "likes"],
36
+
"properties": {
37
+
"uri": { "type": "string", "format": "at-uri" },
38
+
"cid": { "type": "string", "format": "cid" },
39
+
"cursor": { "type": "string" },
40
+
"likes": {
41
+
"type": "array",
42
+
"items": { "type": "ref", "ref": "#like" }
43
+
}
44
+
}
45
+
}
46
+
}
47
+
},
48
+
"like": {
49
+
"type": "object",
50
+
"required": ["indexedAt", "createdAt", "actor"],
51
+
"properties": {
52
+
"indexedAt": { "type": "string", "format": "datetime" },
53
+
"createdAt": { "type": "string", "format": "datetime" },
54
+
"actor": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }
55
+
}
56
+
}
57
+
}
58
+
}
+46
lexicons/app/bsky/feed/getListFeed.json
+46
lexicons/app/bsky/feed/getListFeed.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getListFeed",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["list"],
11
+
"properties": {
12
+
"list": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) to the list record."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" }
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["feed"],
31
+
"properties": {
32
+
"cursor": { "type": "string" },
33
+
"feed": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.feed.defs#feedViewPost"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
},
43
+
"errors": [{ "name": "UnknownList" }]
44
+
}
45
+
}
46
+
}
+57
lexicons/app/bsky/feed/getPostThread.json
+57
lexicons/app/bsky/feed/getPostThread.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getPostThread",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uri"],
11
+
"properties": {
12
+
"uri": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) to post record."
16
+
},
17
+
"depth": {
18
+
"type": "integer",
19
+
"description": "How many levels of reply depth should be included in response.",
20
+
"default": 6,
21
+
"minimum": 0,
22
+
"maximum": 1000
23
+
},
24
+
"parentHeight": {
25
+
"type": "integer",
26
+
"description": "How many levels of parent (and grandparent, etc) post to include.",
27
+
"default": 80,
28
+
"minimum": 0,
29
+
"maximum": 1000
30
+
}
31
+
}
32
+
},
33
+
"output": {
34
+
"encoding": "application/json",
35
+
"schema": {
36
+
"type": "object",
37
+
"required": ["thread"],
38
+
"properties": {
39
+
"thread": {
40
+
"type": "union",
41
+
"refs": [
42
+
"app.bsky.feed.defs#threadViewPost",
43
+
"app.bsky.feed.defs#notFoundPost",
44
+
"app.bsky.feed.defs#blockedPost"
45
+
]
46
+
},
47
+
"threadgate": {
48
+
"type": "ref",
49
+
"ref": "app.bsky.feed.defs#threadgateView"
50
+
}
51
+
}
52
+
}
53
+
},
54
+
"errors": [{ "name": "NotFound" }]
55
+
}
56
+
}
57
+
}
+35
lexicons/app/bsky/feed/getPosts.json
+35
lexicons/app/bsky/feed/getPosts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getPosts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uris"],
11
+
"properties": {
12
+
"uris": {
13
+
"type": "array",
14
+
"description": "List of post AT-URIs to return hydrated views for.",
15
+
"items": { "type": "string", "format": "at-uri" },
16
+
"maxLength": 25
17
+
}
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["posts"],
25
+
"properties": {
26
+
"posts": {
27
+
"type": "array",
28
+
"items": { "type": "ref", "ref": "app.bsky.feed.defs#postView" }
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
+52
lexicons/app/bsky/feed/getQuotes.json
+52
lexicons/app/bsky/feed/getQuotes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getQuotes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of quotes for a given post.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uri"],
11
+
"properties": {
12
+
"uri": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) of post record"
16
+
},
17
+
"cid": {
18
+
"type": "string",
19
+
"format": "cid",
20
+
"description": "If supplied, filters to quotes of specific version (by CID) of the post record."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 100,
26
+
"default": 50
27
+
},
28
+
"cursor": { "type": "string" }
29
+
}
30
+
},
31
+
"output": {
32
+
"encoding": "application/json",
33
+
"schema": {
34
+
"type": "object",
35
+
"required": ["uri", "posts"],
36
+
"properties": {
37
+
"uri": { "type": "string", "format": "at-uri" },
38
+
"cid": { "type": "string", "format": "cid" },
39
+
"cursor": { "type": "string" },
40
+
"posts": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "ref",
44
+
"ref": "app.bsky.feed.defs#postView"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}
+52
lexicons/app/bsky/feed/getRepostedBy.json
+52
lexicons/app/bsky/feed/getRepostedBy.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getRepostedBy",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of reposts for a given post.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uri"],
11
+
"properties": {
12
+
"uri": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) of post record"
16
+
},
17
+
"cid": {
18
+
"type": "string",
19
+
"format": "cid",
20
+
"description": "If supplied, filters to reposts of specific version (by CID) of the post record."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 100,
26
+
"default": 50
27
+
},
28
+
"cursor": { "type": "string" }
29
+
}
30
+
},
31
+
"output": {
32
+
"encoding": "application/json",
33
+
"schema": {
34
+
"type": "object",
35
+
"required": ["uri", "repostedBy"],
36
+
"properties": {
37
+
"uri": { "type": "string", "format": "at-uri" },
38
+
"cid": { "type": "string", "format": "cid" },
39
+
"cursor": { "type": "string" },
40
+
"repostedBy": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "ref",
44
+
"ref": "app.bsky.actor.defs#profileView"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}
+39
lexicons/app/bsky/feed/getSuggestedFeeds.json
+39
lexicons/app/bsky/feed/getSuggestedFeeds.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getSuggestedFeeds",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested feeds (feed generators) for the requesting account.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["feeds"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"feeds": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.feed.defs#generatorView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+43
lexicons/app/bsky/feed/getTimeline.json
+43
lexicons/app/bsky/feed/getTimeline.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.getTimeline",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"algorithm": {
12
+
"type": "string",
13
+
"description": "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism."
14
+
},
15
+
"limit": {
16
+
"type": "integer",
17
+
"minimum": 1,
18
+
"maximum": 100,
19
+
"default": 50
20
+
},
21
+
"cursor": { "type": "string" }
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["feed"],
29
+
"properties": {
30
+
"cursor": { "type": "string" },
31
+
"feed": {
32
+
"type": "array",
33
+
"items": {
34
+
"type": "ref",
35
+
"ref": "app.bsky.feed.defs#feedViewPost"
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+20
lexicons/app/bsky/feed/like.json
+20
lexicons/app/bsky/feed/like.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.like",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a 'like' of a piece of subject content.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "createdAt"],
12
+
"properties": {
13
+
"subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
14
+
"createdAt": { "type": "string", "format": "datetime" },
15
+
"via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+96
lexicons/app/bsky/feed/post.json
+96
lexicons/app/bsky/feed/post.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.post",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record containing a Bluesky post.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["text", "createdAt"],
12
+
"properties": {
13
+
"text": {
14
+
"type": "string",
15
+
"maxLength": 3000,
16
+
"maxGraphemes": 300,
17
+
"description": "The primary post content. May be an empty string, if there are embeds."
18
+
},
19
+
"entities": {
20
+
"type": "array",
21
+
"description": "DEPRECATED: replaced by app.bsky.richtext.facet.",
22
+
"items": { "type": "ref", "ref": "#entity" }
23
+
},
24
+
"facets": {
25
+
"type": "array",
26
+
"description": "Annotations of text (mentions, URLs, hashtags, etc)",
27
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
28
+
},
29
+
"reply": { "type": "ref", "ref": "#replyRef" },
30
+
"embed": {
31
+
"type": "union",
32
+
"refs": [
33
+
"app.bsky.embed.images",
34
+
"app.bsky.embed.video",
35
+
"app.bsky.embed.external",
36
+
"app.bsky.embed.record",
37
+
"app.bsky.embed.recordWithMedia"
38
+
]
39
+
},
40
+
"langs": {
41
+
"type": "array",
42
+
"description": "Indicates human language of post primary text content.",
43
+
"maxLength": 3,
44
+
"items": { "type": "string", "format": "language" }
45
+
},
46
+
"labels": {
47
+
"type": "union",
48
+
"description": "Self-label values for this post. Effectively content warnings.",
49
+
"refs": ["com.atproto.label.defs#selfLabels"]
50
+
},
51
+
"tags": {
52
+
"type": "array",
53
+
"description": "Additional hashtags, in addition to any included in post text and facets.",
54
+
"maxLength": 8,
55
+
"items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
56
+
},
57
+
"createdAt": {
58
+
"type": "string",
59
+
"format": "datetime",
60
+
"description": "Client-declared timestamp when this post was originally created."
61
+
}
62
+
}
63
+
}
64
+
},
65
+
"replyRef": {
66
+
"type": "object",
67
+
"required": ["root", "parent"],
68
+
"properties": {
69
+
"root": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
70
+
"parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
71
+
}
72
+
},
73
+
"entity": {
74
+
"type": "object",
75
+
"description": "Deprecated: use facets instead.",
76
+
"required": ["index", "type", "value"],
77
+
"properties": {
78
+
"index": { "type": "ref", "ref": "#textSlice" },
79
+
"type": {
80
+
"type": "string",
81
+
"description": "Expected values are 'mention' and 'link'."
82
+
},
83
+
"value": { "type": "string" }
84
+
}
85
+
},
86
+
"textSlice": {
87
+
"type": "object",
88
+
"description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.",
89
+
"required": ["start", "end"],
90
+
"properties": {
91
+
"start": { "type": "integer", "minimum": 0 },
92
+
"end": { "type": "integer", "minimum": 0 }
93
+
}
94
+
}
95
+
}
96
+
}
+46
lexicons/app/bsky/feed/postgate.json
+46
lexicons/app/bsky/feed/postgate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.postgate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"key": "tid",
8
+
"description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["post", "createdAt"],
12
+
"properties": {
13
+
"createdAt": { "type": "string", "format": "datetime" },
14
+
"post": {
15
+
"type": "string",
16
+
"format": "at-uri",
17
+
"description": "Reference (AT-URI) to the post record."
18
+
},
19
+
"detachedEmbeddingUris": {
20
+
"type": "array",
21
+
"maxLength": 50,
22
+
"items": {
23
+
"type": "string",
24
+
"format": "at-uri"
25
+
},
26
+
"description": "List of AT-URIs embedding this post that the author has detached from."
27
+
},
28
+
"embeddingRules": {
29
+
"description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.",
30
+
"type": "array",
31
+
"maxLength": 5,
32
+
"items": {
33
+
"type": "union",
34
+
"refs": ["#disableRule"]
35
+
}
36
+
}
37
+
}
38
+
}
39
+
},
40
+
"disableRule": {
41
+
"type": "object",
42
+
"description": "Disables embedding of this post.",
43
+
"properties": {}
44
+
}
45
+
}
46
+
}
+20
lexicons/app/bsky/feed/repost.json
+20
lexicons/app/bsky/feed/repost.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.repost",
4
+
"defs": {
5
+
"main": {
6
+
"description": "Record representing a 'repost' of an existing Bluesky post.",
7
+
"type": "record",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "createdAt"],
12
+
"properties": {
13
+
"subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
14
+
"createdAt": { "type": "string", "format": "datetime" },
15
+
"via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+99
lexicons/app/bsky/feed/searchPosts.json
+99
lexicons/app/bsky/feed/searchPosts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.searchPosts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
15
+
},
16
+
"sort": {
17
+
"type": "string",
18
+
"knownValues": ["top", "latest"],
19
+
"default": "latest",
20
+
"description": "Specifies the ranking order of results."
21
+
},
22
+
"since": {
23
+
"type": "string",
24
+
"description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)."
25
+
},
26
+
"until": {
27
+
"type": "string",
28
+
"description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)."
29
+
},
30
+
"mentions": {
31
+
"type": "string",
32
+
"format": "at-identifier",
33
+
"description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions."
34
+
},
35
+
"author": {
36
+
"type": "string",
37
+
"format": "at-identifier",
38
+
"description": "Filter to posts by the given account. Handles are resolved to DID before query-time."
39
+
},
40
+
"lang": {
41
+
"type": "string",
42
+
"format": "language",
43
+
"description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection."
44
+
},
45
+
"domain": {
46
+
"type": "string",
47
+
"description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization."
48
+
},
49
+
"url": {
50
+
"type": "string",
51
+
"format": "uri",
52
+
"description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching."
53
+
},
54
+
"tag": {
55
+
"type": "array",
56
+
"items": {
57
+
"type": "string",
58
+
"maxLength": 640,
59
+
"maxGraphemes": 64
60
+
},
61
+
"description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching."
62
+
},
63
+
"limit": {
64
+
"type": "integer",
65
+
"minimum": 1,
66
+
"maximum": 100,
67
+
"default": 25
68
+
},
69
+
"cursor": {
70
+
"type": "string",
71
+
"description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
72
+
}
73
+
}
74
+
},
75
+
"output": {
76
+
"encoding": "application/json",
77
+
"schema": {
78
+
"type": "object",
79
+
"required": ["posts"],
80
+
"properties": {
81
+
"cursor": { "type": "string" },
82
+
"hitsTotal": {
83
+
"type": "integer",
84
+
"description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
85
+
},
86
+
"posts": {
87
+
"type": "array",
88
+
"items": {
89
+
"type": "ref",
90
+
"ref": "app.bsky.feed.defs#postView"
91
+
}
92
+
}
93
+
}
94
+
}
95
+
},
96
+
"errors": [{ "name": "BadQueryString" }]
97
+
}
98
+
}
99
+
}
+33
lexicons/app/bsky/feed/sendInteractions.json
+33
lexicons/app/bsky/feed/sendInteractions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.sendInteractions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Send information about interactions with feed items back to the feed generator that served them.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["interactions"],
13
+
"properties": {
14
+
"interactions": {
15
+
"type": "array",
16
+
"items": {
17
+
"type": "ref",
18
+
"ref": "app.bsky.feed.defs#interaction"
19
+
}
20
+
}
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"properties": {}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
+69
lexicons/app/bsky/feed/threadgate.json
+69
lexicons/app/bsky/feed/threadgate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.threadgate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"key": "tid",
8
+
"description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["post", "createdAt"],
12
+
"properties": {
13
+
"post": {
14
+
"type": "string",
15
+
"format": "at-uri",
16
+
"description": "Reference (AT-URI) to the post record."
17
+
},
18
+
"allow": {
19
+
"description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
20
+
"type": "array",
21
+
"maxLength": 5,
22
+
"items": {
23
+
"type": "union",
24
+
"refs": [
25
+
"#mentionRule",
26
+
"#followerRule",
27
+
"#followingRule",
28
+
"#listRule"
29
+
]
30
+
}
31
+
},
32
+
"createdAt": { "type": "string", "format": "datetime" },
33
+
"hiddenReplies": {
34
+
"type": "array",
35
+
"maxLength": 300,
36
+
"items": {
37
+
"type": "string",
38
+
"format": "at-uri"
39
+
},
40
+
"description": "List of hidden reply URIs."
41
+
}
42
+
}
43
+
}
44
+
},
45
+
"mentionRule": {
46
+
"type": "object",
47
+
"description": "Allow replies from actors mentioned in your post.",
48
+
"properties": {}
49
+
},
50
+
"followerRule": {
51
+
"type": "object",
52
+
"description": "Allow replies from actors who follow you.",
53
+
"properties": {}
54
+
},
55
+
"followingRule": {
56
+
"type": "object",
57
+
"description": "Allow replies from actors you follow.",
58
+
"properties": {}
59
+
},
60
+
"listRule": {
61
+
"type": "object",
62
+
"description": "Allow replies from actors on a list.",
63
+
"required": ["list"],
64
+
"properties": {
65
+
"list": { "type": "string", "format": "at-uri" }
66
+
}
67
+
}
68
+
}
69
+
}
+23
lexicons/app/bsky/graph/block.json
+23
lexicons/app/bsky/graph/block.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.block",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "createdAt"],
12
+
"properties": {
13
+
"subject": {
14
+
"type": "string",
15
+
"format": "did",
16
+
"description": "DID of the account to be blocked."
17
+
},
18
+
"createdAt": { "type": "string", "format": "datetime" }
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+186
lexicons/app/bsky/graph/defs.json
+186
lexicons/app/bsky/graph/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.defs",
4
+
"defs": {
5
+
"listViewBasic": {
6
+
"type": "object",
7
+
"required": ["uri", "cid", "name", "purpose"],
8
+
"properties": {
9
+
"uri": { "type": "string", "format": "at-uri" },
10
+
"cid": { "type": "string", "format": "cid" },
11
+
"name": { "type": "string", "maxLength": 64, "minLength": 1 },
12
+
"purpose": { "type": "ref", "ref": "#listPurpose" },
13
+
"avatar": { "type": "string", "format": "uri" },
14
+
"listItemCount": { "type": "integer", "minimum": 0 },
15
+
"labels": {
16
+
"type": "array",
17
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
18
+
},
19
+
"viewer": { "type": "ref", "ref": "#listViewerState" },
20
+
"indexedAt": { "type": "string", "format": "datetime" }
21
+
}
22
+
},
23
+
"listView": {
24
+
"type": "object",
25
+
"required": ["uri", "cid", "creator", "name", "purpose", "indexedAt"],
26
+
"properties": {
27
+
"uri": { "type": "string", "format": "at-uri" },
28
+
"cid": { "type": "string", "format": "cid" },
29
+
"creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
30
+
"name": { "type": "string", "maxLength": 64, "minLength": 1 },
31
+
"purpose": { "type": "ref", "ref": "#listPurpose" },
32
+
"description": {
33
+
"type": "string",
34
+
"maxGraphemes": 300,
35
+
"maxLength": 3000
36
+
},
37
+
"descriptionFacets": {
38
+
"type": "array",
39
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
40
+
},
41
+
"avatar": { "type": "string", "format": "uri" },
42
+
"listItemCount": { "type": "integer", "minimum": 0 },
43
+
"labels": {
44
+
"type": "array",
45
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
46
+
},
47
+
"viewer": { "type": "ref", "ref": "#listViewerState" },
48
+
"indexedAt": { "type": "string", "format": "datetime" }
49
+
}
50
+
},
51
+
"listItemView": {
52
+
"type": "object",
53
+
"required": ["uri", "subject"],
54
+
"properties": {
55
+
"uri": { "type": "string", "format": "at-uri" },
56
+
"subject": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }
57
+
}
58
+
},
59
+
"starterPackView": {
60
+
"type": "object",
61
+
"required": ["uri", "cid", "record", "creator", "indexedAt"],
62
+
"properties": {
63
+
"uri": { "type": "string", "format": "at-uri" },
64
+
"cid": { "type": "string", "format": "cid" },
65
+
"record": { "type": "unknown" },
66
+
"creator": {
67
+
"type": "ref",
68
+
"ref": "app.bsky.actor.defs#profileViewBasic"
69
+
},
70
+
"list": { "type": "ref", "ref": "#listViewBasic" },
71
+
"listItemsSample": {
72
+
"type": "array",
73
+
"maxLength": 12,
74
+
"items": { "type": "ref", "ref": "#listItemView" }
75
+
},
76
+
"feeds": {
77
+
"type": "array",
78
+
"maxLength": 3,
79
+
"items": { "type": "ref", "ref": "app.bsky.feed.defs#generatorView" }
80
+
},
81
+
"joinedWeekCount": { "type": "integer", "minimum": 0 },
82
+
"joinedAllTimeCount": { "type": "integer", "minimum": 0 },
83
+
"labels": {
84
+
"type": "array",
85
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
86
+
},
87
+
"indexedAt": { "type": "string", "format": "datetime" }
88
+
}
89
+
},
90
+
"starterPackViewBasic": {
91
+
"type": "object",
92
+
"required": ["uri", "cid", "record", "creator", "indexedAt"],
93
+
"properties": {
94
+
"uri": { "type": "string", "format": "at-uri" },
95
+
"cid": { "type": "string", "format": "cid" },
96
+
"record": { "type": "unknown" },
97
+
"creator": {
98
+
"type": "ref",
99
+
"ref": "app.bsky.actor.defs#profileViewBasic"
100
+
},
101
+
"listItemCount": { "type": "integer", "minimum": 0 },
102
+
"joinedWeekCount": { "type": "integer", "minimum": 0 },
103
+
"joinedAllTimeCount": { "type": "integer", "minimum": 0 },
104
+
"labels": {
105
+
"type": "array",
106
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
107
+
},
108
+
"indexedAt": { "type": "string", "format": "datetime" }
109
+
}
110
+
},
111
+
"listPurpose": {
112
+
"type": "string",
113
+
"knownValues": [
114
+
"app.bsky.graph.defs#modlist",
115
+
"app.bsky.graph.defs#curatelist",
116
+
"app.bsky.graph.defs#referencelist"
117
+
]
118
+
},
119
+
"modlist": {
120
+
"type": "token",
121
+
"description": "A list of actors to apply an aggregate moderation action (mute/block) on."
122
+
},
123
+
"curatelist": {
124
+
"type": "token",
125
+
"description": "A list of actors used for curation purposes such as list feeds or interaction gating."
126
+
},
127
+
"referencelist": {
128
+
"type": "token",
129
+
"description": "A list of actors used for only for reference purposes such as within a starter pack."
130
+
},
131
+
"listViewerState": {
132
+
"type": "object",
133
+
"properties": {
134
+
"muted": { "type": "boolean" },
135
+
"blocked": { "type": "string", "format": "at-uri" }
136
+
}
137
+
},
138
+
"notFoundActor": {
139
+
"type": "object",
140
+
"description": "indicates that a handle or DID could not be resolved",
141
+
"required": ["actor", "notFound"],
142
+
"properties": {
143
+
"actor": { "type": "string", "format": "at-identifier" },
144
+
"notFound": { "type": "boolean", "const": true }
145
+
}
146
+
},
147
+
"relationship": {
148
+
"type": "object",
149
+
"description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)",
150
+
"required": ["did"],
151
+
"properties": {
152
+
"did": { "type": "string", "format": "did" },
153
+
"following": {
154
+
"type": "string",
155
+
"format": "at-uri",
156
+
"description": "if the actor follows this DID, this is the AT-URI of the follow record"
157
+
},
158
+
"followedBy": {
159
+
"type": "string",
160
+
"format": "at-uri",
161
+
"description": "if the actor is followed by this DID, contains the AT-URI of the follow record"
162
+
},
163
+
"blocking": {
164
+
"type": "string",
165
+
"format": "at-uri",
166
+
"description": "if the actor blocks this DID, this is the AT-URI of the block record"
167
+
},
168
+
"blockedBy": {
169
+
"type": "string",
170
+
"format": "at-uri",
171
+
"description": "if the actor is blocked by this DID, contains the AT-URI of the block record"
172
+
},
173
+
"blockingByList": {
174
+
"type": "string",
175
+
"format": "at-uri",
176
+
"description": "if the actor blocks this DID via a block list, this is the AT-URI of the listblock record"
177
+
},
178
+
"blockedByList": {
179
+
"type": "string",
180
+
"format": "at-uri",
181
+
"description": "if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record"
182
+
}
183
+
}
184
+
}
185
+
}
186
+
}
+20
lexicons/app/bsky/graph/follow.json
+20
lexicons/app/bsky/graph/follow.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.follow",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "createdAt"],
12
+
"properties": {
13
+
"subject": { "type": "string", "format": "did" },
14
+
"createdAt": { "type": "string", "format": "datetime" },
15
+
"via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+41
lexicons/app/bsky/graph/getActorStarterPacks.json
+41
lexicons/app/bsky/graph/getActorStarterPacks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getActorStarterPacks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of starter packs created by the actor.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["starterPacks"],
27
+
"properties": {
28
+
"cursor": { "type": "string" },
29
+
"starterPacks": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.graph.defs#starterPackViewBasic"
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
+39
lexicons/app/bsky/graph/getBlocks.json
+39
lexicons/app/bsky/graph/getBlocks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getBlocks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates which accounts the requesting account is currently blocking. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["blocks"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"blocks": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.actor.defs#profileView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+45
lexicons/app/bsky/graph/getFollowers.json
+45
lexicons/app/bsky/graph/getFollowers.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getFollowers",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates accounts which follow a specified account (actor).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["subject", "followers"],
27
+
"properties": {
28
+
"subject": {
29
+
"type": "ref",
30
+
"ref": "app.bsky.actor.defs#profileView"
31
+
},
32
+
"cursor": { "type": "string" },
33
+
"followers": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.actor.defs#profileView"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+45
lexicons/app/bsky/graph/getFollows.json
+45
lexicons/app/bsky/graph/getFollows.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getFollows",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates accounts which a specified account (actor) follows.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["subject", "follows"],
27
+
"properties": {
28
+
"subject": {
29
+
"type": "ref",
30
+
"ref": "app.bsky.actor.defs#profileView"
31
+
},
32
+
"cursor": { "type": "string" },
33
+
"follows": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.actor.defs#profileView"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+45
lexicons/app/bsky/graph/getKnownFollowers.json
+45
lexicons/app/bsky/graph/getKnownFollowers.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getKnownFollowers",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates accounts which follow a specified account (actor) and are followed by the viewer.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
},
19
+
"cursor": { "type": "string" }
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["subject", "followers"],
27
+
"properties": {
28
+
"subject": {
29
+
"type": "ref",
30
+
"ref": "app.bsky.actor.defs#profileView"
31
+
},
32
+
"cursor": { "type": "string" },
33
+
"followers": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "app.bsky.actor.defs#profileView"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+46
lexicons/app/bsky/graph/getList.json
+46
lexicons/app/bsky/graph/getList.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getList",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Gets a 'view' (with additional context) of a specified list.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["list"],
11
+
"properties": {
12
+
"list": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) of the list record to hydrate."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" }
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["list", "items"],
31
+
"properties": {
32
+
"cursor": { "type": "string" },
33
+
"list": { "type": "ref", "ref": "app.bsky.graph.defs#listView" },
34
+
"items": {
35
+
"type": "array",
36
+
"items": {
37
+
"type": "ref",
38
+
"ref": "app.bsky.graph.defs#listItemView"
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
+36
lexicons/app/bsky/graph/getListBlocks.json
+36
lexicons/app/bsky/graph/getListBlocks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getListBlocks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get mod lists that the requesting account (actor) is blocking. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["lists"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"lists": {
28
+
"type": "array",
29
+
"items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+36
lexicons/app/bsky/graph/getListMutes.json
+36
lexicons/app/bsky/graph/getListMutes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getListMutes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["lists"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"lists": {
28
+
"type": "array",
29
+
"items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+50
lexicons/app/bsky/graph/getLists.json
+50
lexicons/app/bsky/graph/getLists.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getLists",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates the lists created by a specified account (actor).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The account (actor) to enumerate lists from."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" },
24
+
"purposes": {
25
+
"type": "array",
26
+
"description": "Optional filter by list purpose. If not specified, all supported types are returned.",
27
+
"items": {
28
+
"type": "string",
29
+
"knownValues": ["modlist", "curatelist"]
30
+
}
31
+
}
32
+
}
33
+
},
34
+
"output": {
35
+
"encoding": "application/json",
36
+
"schema": {
37
+
"type": "object",
38
+
"required": ["lists"],
39
+
"properties": {
40
+
"cursor": { "type": "string" },
41
+
"lists": {
42
+
"type": "array",
43
+
"items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
+65
lexicons/app/bsky/graph/getListsWithMembership.json
+65
lexicons/app/bsky/graph/getListsWithMembership.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getListsWithMembership",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The account (actor) to check for membership."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" },
24
+
"purposes": {
25
+
"type": "array",
26
+
"description": "Optional filter by list purpose. If not specified, all supported types are returned.",
27
+
"items": {
28
+
"type": "string",
29
+
"knownValues": ["modlist", "curatelist"]
30
+
}
31
+
}
32
+
}
33
+
},
34
+
"output": {
35
+
"encoding": "application/json",
36
+
"schema": {
37
+
"type": "object",
38
+
"required": ["listsWithMembership"],
39
+
"properties": {
40
+
"cursor": { "type": "string" },
41
+
"listsWithMembership": {
42
+
"type": "array",
43
+
"items": { "type": "ref", "ref": "#listWithMembership" }
44
+
}
45
+
}
46
+
}
47
+
}
48
+
},
49
+
"listWithMembership": {
50
+
"description": "A list and an optional list item indicating membership of a target user to that list.",
51
+
"type": "object",
52
+
"required": ["list"],
53
+
"properties": {
54
+
"list": {
55
+
"type": "ref",
56
+
"ref": "app.bsky.graph.defs#listView"
57
+
},
58
+
"listItem": {
59
+
"type": "ref",
60
+
"ref": "app.bsky.graph.defs#listItemView"
61
+
}
62
+
}
63
+
}
64
+
}
65
+
}
+39
lexicons/app/bsky/graph/getMutes.json
+39
lexicons/app/bsky/graph/getMutes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getMutes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["mutes"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"mutes": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.actor.defs#profileView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+56
lexicons/app/bsky/graph/getRelationships.json
+56
lexicons/app/bsky/graph/getRelationships.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getRelationships",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates public relationships between one account, and a list of other accounts. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "Primary account requesting relationships for."
16
+
},
17
+
"others": {
18
+
"type": "array",
19
+
"description": "List of 'other' accounts to be related back to the primary.",
20
+
"maxLength": 30,
21
+
"items": {
22
+
"type": "string",
23
+
"format": "at-identifier"
24
+
}
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["relationships"],
33
+
"properties": {
34
+
"actor": { "type": "string", "format": "did" },
35
+
"relationships": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "union",
39
+
"refs": [
40
+
"app.bsky.graph.defs#relationship",
41
+
"app.bsky.graph.defs#notFoundActor"
42
+
]
43
+
}
44
+
}
45
+
}
46
+
}
47
+
},
48
+
"errors": [
49
+
{
50
+
"name": "ActorNotFound",
51
+
"description": "the primary actor at-identifier could not be resolved"
52
+
}
53
+
]
54
+
}
55
+
}
56
+
}
+34
lexicons/app/bsky/graph/getStarterPack.json
+34
lexicons/app/bsky/graph/getStarterPack.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getStarterPack",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Gets a view of a starter pack.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["starterPack"],
11
+
"properties": {
12
+
"starterPack": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) of the starter pack record."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["starterPack"],
24
+
"properties": {
25
+
"starterPack": {
26
+
"type": "ref",
27
+
"ref": "app.bsky.graph.defs#starterPackView"
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
+37
lexicons/app/bsky/graph/getStarterPacks.json
+37
lexicons/app/bsky/graph/getStarterPacks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getStarterPacks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get views for a list of starter packs.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uris"],
11
+
"properties": {
12
+
"uris": {
13
+
"type": "array",
14
+
"items": { "type": "string", "format": "at-uri" },
15
+
"maxLength": 25
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["starterPacks"],
24
+
"properties": {
25
+
"starterPacks": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.graph.defs#starterPackViewBasic"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+57
lexicons/app/bsky/graph/getStarterPacksWithMembership.json
+57
lexicons/app/bsky/graph/getStarterPacksWithMembership.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getStarterPacksWithMembership",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The account (actor) to check for membership."
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
},
23
+
"cursor": { "type": "string" }
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["starterPacksWithMembership"],
31
+
"properties": {
32
+
"cursor": { "type": "string" },
33
+
"starterPacksWithMembership": {
34
+
"type": "array",
35
+
"items": { "type": "ref", "ref": "#starterPackWithMembership" }
36
+
}
37
+
}
38
+
}
39
+
}
40
+
},
41
+
"starterPackWithMembership": {
42
+
"description": "A starter pack and an optional list item indicating membership of a target user to that starter pack.",
43
+
"type": "object",
44
+
"required": ["starterPack"],
45
+
"properties": {
46
+
"starterPack": {
47
+
"type": "ref",
48
+
"ref": "app.bsky.graph.defs#starterPackView"
49
+
},
50
+
"listItem": {
51
+
"type": "ref",
52
+
"ref": "app.bsky.graph.defs#listItemView"
53
+
}
54
+
}
55
+
}
56
+
}
57
+
}
+42
lexicons/app/bsky/graph/getSuggestedFollowsByActor.json
+42
lexicons/app/bsky/graph/getSuggestedFollowsByActor.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.getSuggestedFollowsByActor",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": { "type": "string", "format": "at-identifier" }
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "object",
19
+
"required": ["suggestions"],
20
+
"properties": {
21
+
"suggestions": {
22
+
"type": "array",
23
+
"items": {
24
+
"type": "ref",
25
+
"ref": "app.bsky.actor.defs#profileView"
26
+
}
27
+
},
28
+
"isFallback": {
29
+
"type": "boolean",
30
+
"description": "If true, response has fallen-back to generic results, and is not scoped using relativeToDid",
31
+
"default": false
32
+
},
33
+
"recId": {
34
+
"type": "integer",
35
+
"description": "Snowflake for this recommendation, use when submitting recommendation events."
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+47
lexicons/app/bsky/graph/list.json
+47
lexicons/app/bsky/graph/list.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.list",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["name", "purpose", "createdAt"],
12
+
"properties": {
13
+
"purpose": {
14
+
"type": "ref",
15
+
"description": "Defines the purpose of the list (aka, moderation-oriented or curration-oriented)",
16
+
"ref": "app.bsky.graph.defs#listPurpose"
17
+
},
18
+
"name": {
19
+
"type": "string",
20
+
"maxLength": 64,
21
+
"minLength": 1,
22
+
"description": "Display name for list; can not be empty."
23
+
},
24
+
"description": {
25
+
"type": "string",
26
+
"maxGraphemes": 300,
27
+
"maxLength": 3000
28
+
},
29
+
"descriptionFacets": {
30
+
"type": "array",
31
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
32
+
},
33
+
"avatar": {
34
+
"type": "blob",
35
+
"accept": ["image/png", "image/jpeg"],
36
+
"maxSize": 1000000
37
+
},
38
+
"labels": {
39
+
"type": "union",
40
+
"refs": ["com.atproto.label.defs#selfLabels"]
41
+
},
42
+
"createdAt": { "type": "string", "format": "datetime" }
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
+23
lexicons/app/bsky/graph/listblock.json
+23
lexicons/app/bsky/graph/listblock.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.listblock",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record representing a block relationship against an entire an entire list of accounts (actors).",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "createdAt"],
12
+
"properties": {
13
+
"subject": {
14
+
"type": "string",
15
+
"format": "at-uri",
16
+
"description": "Reference (AT-URI) to the mod list record."
17
+
},
18
+
"createdAt": { "type": "string", "format": "datetime" }
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+28
lexicons/app/bsky/graph/listitem.json
+28
lexicons/app/bsky/graph/listitem.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.listitem",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "list", "createdAt"],
12
+
"properties": {
13
+
"subject": {
14
+
"type": "string",
15
+
"format": "did",
16
+
"description": "The account which is included on the list."
17
+
},
18
+
"list": {
19
+
"type": "string",
20
+
"format": "at-uri",
21
+
"description": "Reference (AT-URI) to the list record (app.bsky.graph.list)."
22
+
},
23
+
"createdAt": { "type": "string", "format": "datetime" }
24
+
}
25
+
}
26
+
}
27
+
}
28
+
}
+20
lexicons/app/bsky/graph/muteActor.json
+20
lexicons/app/bsky/graph/muteActor.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.muteActor",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["actor"],
13
+
"properties": {
14
+
"actor": { "type": "string", "format": "at-identifier" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+20
lexicons/app/bsky/graph/muteActorList.json
+20
lexicons/app/bsky/graph/muteActorList.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.muteActorList",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["list"],
13
+
"properties": {
14
+
"list": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+20
lexicons/app/bsky/graph/muteThread.json
+20
lexicons/app/bsky/graph/muteThread.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.muteThread",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["root"],
13
+
"properties": {
14
+
"root": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+48
lexicons/app/bsky/graph/searchStarterPacks.json
+48
lexicons/app/bsky/graph/searchStarterPacks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.searchStarterPacks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find starter packs matching search criteria. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 100,
20
+
"default": 25
21
+
},
22
+
"cursor": {
23
+
"type": "string"
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["starterPacks"],
32
+
"properties": {
33
+
"cursor": {
34
+
"type": "string"
35
+
},
36
+
"starterPacks": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "ref",
40
+
"ref": "app.bsky.graph.defs#starterPackViewBasic"
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
+51
lexicons/app/bsky/graph/starterpack.json
+51
lexicons/app/bsky/graph/starterpack.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.starterpack",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record defining a starter pack of actors and feeds for new users.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["name", "list", "createdAt"],
12
+
"properties": {
13
+
"name": {
14
+
"type": "string",
15
+
"maxGraphemes": 50,
16
+
"maxLength": 500,
17
+
"minLength": 1,
18
+
"description": "Display name for starter pack; can not be empty."
19
+
},
20
+
"description": {
21
+
"type": "string",
22
+
"maxGraphemes": 300,
23
+
"maxLength": 3000
24
+
},
25
+
"descriptionFacets": {
26
+
"type": "array",
27
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
28
+
},
29
+
"list": {
30
+
"type": "string",
31
+
"format": "at-uri",
32
+
"description": "Reference (AT-URI) to the list record."
33
+
},
34
+
"feeds": {
35
+
"type": "array",
36
+
"maxLength": 3,
37
+
"items": { "type": "ref", "ref": "#feedItem" }
38
+
},
39
+
"createdAt": { "type": "string", "format": "datetime" }
40
+
}
41
+
}
42
+
},
43
+
"feedItem": {
44
+
"type": "object",
45
+
"required": ["uri"],
46
+
"properties": {
47
+
"uri": { "type": "string", "format": "at-uri" }
48
+
}
49
+
}
50
+
}
51
+
}
+20
lexicons/app/bsky/graph/unmuteActor.json
+20
lexicons/app/bsky/graph/unmuteActor.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.unmuteActor",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Unmutes the specified account. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["actor"],
13
+
"properties": {
14
+
"actor": { "type": "string", "format": "at-identifier" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+20
lexicons/app/bsky/graph/unmuteActorList.json
+20
lexicons/app/bsky/graph/unmuteActorList.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.unmuteActorList",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Unmutes the specified list of accounts. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["list"],
13
+
"properties": {
14
+
"list": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+20
lexicons/app/bsky/graph/unmuteThread.json
+20
lexicons/app/bsky/graph/unmuteThread.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.unmuteThread",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Unmutes the specified thread. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["root"],
13
+
"properties": {
14
+
"root": { "type": "string", "format": "at-uri" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+36
lexicons/app/bsky/graph/verification.json
+36
lexicons/app/bsky/graph/verification.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.verification",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["subject", "handle", "displayName", "createdAt"],
12
+
"properties": {
13
+
"subject": {
14
+
"description": "DID of the subject the verification applies to.",
15
+
"type": "string",
16
+
"format": "did"
17
+
},
18
+
"handle": {
19
+
"description": "Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.",
20
+
"type": "string",
21
+
"format": "handle"
22
+
},
23
+
"displayName": {
24
+
"description": "Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.",
25
+
"type": "string"
26
+
},
27
+
"createdAt": {
28
+
"description": "Date of when the verification was created.",
29
+
"type": "string",
30
+
"format": "datetime"
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+91
lexicons/app/bsky/labeler/defs.json
+91
lexicons/app/bsky/labeler/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.labeler.defs",
4
+
"defs": {
5
+
"labelerView": {
6
+
"type": "object",
7
+
"required": ["uri", "cid", "creator", "indexedAt"],
8
+
"properties": {
9
+
"uri": { "type": "string", "format": "at-uri" },
10
+
"cid": { "type": "string", "format": "cid" },
11
+
"creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
12
+
"likeCount": { "type": "integer", "minimum": 0 },
13
+
"viewer": { "type": "ref", "ref": "#labelerViewerState" },
14
+
"indexedAt": { "type": "string", "format": "datetime" },
15
+
"labels": {
16
+
"type": "array",
17
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
18
+
}
19
+
}
20
+
},
21
+
"labelerViewDetailed": {
22
+
"type": "object",
23
+
"required": ["uri", "cid", "creator", "policies", "indexedAt"],
24
+
"properties": {
25
+
"uri": { "type": "string", "format": "at-uri" },
26
+
"cid": { "type": "string", "format": "cid" },
27
+
"creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
28
+
"policies": {
29
+
"type": "ref",
30
+
"ref": "app.bsky.labeler.defs#labelerPolicies"
31
+
},
32
+
"likeCount": { "type": "integer", "minimum": 0 },
33
+
"viewer": { "type": "ref", "ref": "#labelerViewerState" },
34
+
"indexedAt": { "type": "string", "format": "datetime" },
35
+
"labels": {
36
+
"type": "array",
37
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
38
+
},
39
+
"reasonTypes": {
40
+
"description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
41
+
"type": "array",
42
+
"items": {
43
+
"type": "ref",
44
+
"ref": "com.atproto.moderation.defs#reasonType"
45
+
}
46
+
},
47
+
"subjectTypes": {
48
+
"description": "The set of subject types (account, record, etc) this service accepts reports on.",
49
+
"type": "array",
50
+
"items": {
51
+
"type": "ref",
52
+
"ref": "com.atproto.moderation.defs#subjectType"
53
+
}
54
+
},
55
+
"subjectCollections": {
56
+
"type": "array",
57
+
"description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.",
58
+
"items": { "type": "string", "format": "nsid" }
59
+
}
60
+
}
61
+
},
62
+
"labelerViewerState": {
63
+
"type": "object",
64
+
"properties": {
65
+
"like": { "type": "string", "format": "at-uri" }
66
+
}
67
+
},
68
+
"labelerPolicies": {
69
+
"type": "object",
70
+
"required": ["labelValues"],
71
+
"properties": {
72
+
"labelValues": {
73
+
"type": "array",
74
+
"description": "The label values which this labeler publishes. May include global or custom labels.",
75
+
"items": {
76
+
"type": "ref",
77
+
"ref": "com.atproto.label.defs#labelValue"
78
+
}
79
+
},
80
+
"labelValueDefinitions": {
81
+
"type": "array",
82
+
"description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.",
83
+
"items": {
84
+
"type": "ref",
85
+
"ref": "com.atproto.label.defs#labelValueDefinition"
86
+
}
87
+
}
88
+
}
89
+
}
90
+
}
91
+
}
+43
lexicons/app/bsky/labeler/getServices.json
+43
lexicons/app/bsky/labeler/getServices.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.labeler.getServices",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about a list of labeler services.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["dids"],
11
+
"properties": {
12
+
"dids": {
13
+
"type": "array",
14
+
"items": { "type": "string", "format": "did" }
15
+
},
16
+
"detailed": {
17
+
"type": "boolean",
18
+
"default": false
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["views"],
27
+
"properties": {
28
+
"views": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "union",
32
+
"refs": [
33
+
"app.bsky.labeler.defs#labelerView",
34
+
"app.bsky.labeler.defs#labelerViewDetailed"
35
+
]
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+47
lexicons/app/bsky/labeler/service.json
+47
lexicons/app/bsky/labeler/service.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.labeler.service",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of the existence of labeler service.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["policies", "createdAt"],
12
+
"properties": {
13
+
"policies": {
14
+
"type": "ref",
15
+
"ref": "app.bsky.labeler.defs#labelerPolicies"
16
+
},
17
+
"labels": {
18
+
"type": "union",
19
+
"refs": ["com.atproto.label.defs#selfLabels"]
20
+
},
21
+
"createdAt": { "type": "string", "format": "datetime" },
22
+
"reasonTypes": {
23
+
"description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
24
+
"type": "array",
25
+
"items": {
26
+
"type": "ref",
27
+
"ref": "com.atproto.moderation.defs#reasonType"
28
+
}
29
+
},
30
+
"subjectTypes": {
31
+
"description": "The set of subject types (account, record, etc) this service accepts reports on.",
32
+
"type": "array",
33
+
"items": {
34
+
"type": "ref",
35
+
"ref": "com.atproto.moderation.defs#subjectType"
36
+
}
37
+
},
38
+
"subjectCollections": {
39
+
"type": "array",
40
+
"description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.",
41
+
"items": { "type": "string", "format": "nsid" }
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
+22
lexicons/app/bsky/notification/declaration.json
+22
lexicons/app/bsky/notification/declaration.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.declaration",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of the user's choices related to notifications that can be produced by them.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["allowSubscriptions"],
12
+
"properties": {
13
+
"allowSubscriptions": {
14
+
"type": "string",
15
+
"description": "A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.",
16
+
"knownValues": ["followers", "mutuals", "none"]
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
+88
lexicons/app/bsky/notification/defs.json
+88
lexicons/app/bsky/notification/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.defs",
4
+
"defs": {
5
+
"recordDeleted": {
6
+
"type": "object",
7
+
"properties": {}
8
+
},
9
+
"chatPreference": {
10
+
"type": "object",
11
+
"required": ["include", "push"],
12
+
"properties": {
13
+
"include": { "type": "string", "knownValues": ["all", "accepted"] },
14
+
"push": { "type": "boolean" }
15
+
}
16
+
},
17
+
"filterablePreference": {
18
+
"type": "object",
19
+
"required": ["include", "list", "push"],
20
+
"properties": {
21
+
"include": { "type": "string", "knownValues": ["all", "follows"] },
22
+
"list": { "type": "boolean" },
23
+
"push": { "type": "boolean" }
24
+
}
25
+
},
26
+
"preference": {
27
+
"type": "object",
28
+
"required": ["list", "push"],
29
+
"properties": {
30
+
"list": { "type": "boolean" },
31
+
"push": { "type": "boolean" }
32
+
}
33
+
},
34
+
"preferences": {
35
+
"type": "object",
36
+
"required": [
37
+
"chat",
38
+
"follow",
39
+
"like",
40
+
"likeViaRepost",
41
+
"mention",
42
+
"quote",
43
+
"reply",
44
+
"repost",
45
+
"repostViaRepost",
46
+
"starterpackJoined",
47
+
"subscribedPost",
48
+
"unverified",
49
+
"verified"
50
+
],
51
+
"properties": {
52
+
"chat": { "type": "ref", "ref": "#chatPreference" },
53
+
"follow": { "type": "ref", "ref": "#filterablePreference" },
54
+
"like": { "type": "ref", "ref": "#filterablePreference" },
55
+
"likeViaRepost": { "type": "ref", "ref": "#filterablePreference" },
56
+
"mention": { "type": "ref", "ref": "#filterablePreference" },
57
+
"quote": { "type": "ref", "ref": "#filterablePreference" },
58
+
"reply": { "type": "ref", "ref": "#filterablePreference" },
59
+
"repost": { "type": "ref", "ref": "#filterablePreference" },
60
+
"repostViaRepost": { "type": "ref", "ref": "#filterablePreference" },
61
+
"starterpackJoined": { "type": "ref", "ref": "#preference" },
62
+
"subscribedPost": { "type": "ref", "ref": "#preference" },
63
+
"unverified": { "type": "ref", "ref": "#preference" },
64
+
"verified": { "type": "ref", "ref": "#preference" }
65
+
}
66
+
},
67
+
"activitySubscription": {
68
+
"type": "object",
69
+
"required": ["post", "reply"],
70
+
"properties": {
71
+
"post": { "type": "boolean" },
72
+
"reply": { "type": "boolean" }
73
+
}
74
+
},
75
+
"subjectActivitySubscription": {
76
+
"description": "Object used to store activity subscription data in stash.",
77
+
"type": "object",
78
+
"required": ["subject", "activitySubscription"],
79
+
"properties": {
80
+
"subject": { "type": "string", "format": "did" },
81
+
"activitySubscription": {
82
+
"type": "ref",
83
+
"ref": "#activitySubscription"
84
+
}
85
+
}
86
+
}
87
+
}
88
+
}
+27
lexicons/app/bsky/notification/getPreferences.json
+27
lexicons/app/bsky/notification/getPreferences.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.getPreferences",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get notification-related preferences for an account. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["preferences"],
17
+
"properties": {
18
+
"preferences": {
19
+
"type": "ref",
20
+
"ref": "app.bsky.notification.defs#preferences"
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+27
lexicons/app/bsky/notification/getUnreadCount.json
+27
lexicons/app/bsky/notification/getUnreadCount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.getUnreadCount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Count the number of unread notifications for the requesting account. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"priority": { "type": "boolean" },
12
+
"seenAt": { "type": "string", "format": "datetime" }
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "object",
19
+
"required": ["count"],
20
+
"properties": {
21
+
"count": { "type": "integer" }
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+39
lexicons/app/bsky/notification/listActivitySubscriptions.json
+39
lexicons/app/bsky/notification/listActivitySubscriptions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.listActivitySubscriptions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["subscriptions"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"subscriptions": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.actor.defs#profileView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+92
lexicons/app/bsky/notification/listNotifications.json
+92
lexicons/app/bsky/notification/listNotifications.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.listNotifications",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerate notifications for the requesting account. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"reasons": {
12
+
"description": "Notification reasons to include in response.",
13
+
"type": "array",
14
+
"items": {
15
+
"type": "string",
16
+
"description": "A reason that matches the reason property of #notification."
17
+
}
18
+
},
19
+
"limit": {
20
+
"type": "integer",
21
+
"minimum": 1,
22
+
"maximum": 100,
23
+
"default": 50
24
+
},
25
+
"priority": { "type": "boolean" },
26
+
"cursor": { "type": "string" },
27
+
"seenAt": { "type": "string", "format": "datetime" }
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"required": ["notifications"],
35
+
"properties": {
36
+
"cursor": { "type": "string" },
37
+
"notifications": {
38
+
"type": "array",
39
+
"items": { "type": "ref", "ref": "#notification" }
40
+
},
41
+
"priority": { "type": "boolean" },
42
+
"seenAt": { "type": "string", "format": "datetime" }
43
+
}
44
+
}
45
+
}
46
+
},
47
+
"notification": {
48
+
"type": "object",
49
+
"required": [
50
+
"uri",
51
+
"cid",
52
+
"author",
53
+
"reason",
54
+
"record",
55
+
"isRead",
56
+
"indexedAt"
57
+
],
58
+
"properties": {
59
+
"uri": { "type": "string", "format": "at-uri" },
60
+
"cid": { "type": "string", "format": "cid" },
61
+
"author": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
62
+
"reason": {
63
+
"type": "string",
64
+
"description": "The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.",
65
+
"knownValues": [
66
+
"like",
67
+
"repost",
68
+
"follow",
69
+
"mention",
70
+
"reply",
71
+
"quote",
72
+
"starterpack-joined",
73
+
"verified",
74
+
"unverified",
75
+
"like-via-repost",
76
+
"repost-via-repost",
77
+
"subscribed-post",
78
+
"contact-match"
79
+
]
80
+
},
81
+
"reasonSubject": { "type": "string", "format": "at-uri" },
82
+
"record": { "type": "unknown" },
83
+
"isRead": { "type": "boolean" },
84
+
"indexedAt": { "type": "string", "format": "datetime" },
85
+
"labels": {
86
+
"type": "array",
87
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
88
+
}
89
+
}
90
+
}
91
+
}
92
+
}
+38
lexicons/app/bsky/notification/putActivitySubscription.json
+38
lexicons/app/bsky/notification/putActivitySubscription.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.putActivitySubscription",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["subject", "activitySubscription"],
13
+
"properties": {
14
+
"subject": { "type": "string", "format": "did" },
15
+
"activitySubscription": {
16
+
"type": "ref",
17
+
"ref": "app.bsky.notification.defs#activitySubscription"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["subject"],
27
+
"properties": {
28
+
"subject": { "type": "string", "format": "did" },
29
+
"activitySubscription": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.notification.defs#activitySubscription"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
+20
lexicons/app/bsky/notification/putPreferences.json
+20
lexicons/app/bsky/notification/putPreferences.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.putPreferences",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Set notification-related preferences for an account. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["priority"],
13
+
"properties": {
14
+
"priority": { "type": "boolean" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+83
lexicons/app/bsky/notification/putPreferencesV2.json
+83
lexicons/app/bsky/notification/putPreferencesV2.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.putPreferencesV2",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Set notification-related preferences for an account. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"chat": {
14
+
"type": "ref",
15
+
"ref": "app.bsky.notification.defs#chatPreference"
16
+
},
17
+
"follow": {
18
+
"type": "ref",
19
+
"ref": "app.bsky.notification.defs#filterablePreference"
20
+
},
21
+
"like": {
22
+
"type": "ref",
23
+
"ref": "app.bsky.notification.defs#filterablePreference"
24
+
},
25
+
"likeViaRepost": {
26
+
"type": "ref",
27
+
"ref": "app.bsky.notification.defs#filterablePreference"
28
+
},
29
+
"mention": {
30
+
"type": "ref",
31
+
"ref": "app.bsky.notification.defs#filterablePreference"
32
+
},
33
+
"quote": {
34
+
"type": "ref",
35
+
"ref": "app.bsky.notification.defs#filterablePreference"
36
+
},
37
+
"reply": {
38
+
"type": "ref",
39
+
"ref": "app.bsky.notification.defs#filterablePreference"
40
+
},
41
+
"repost": {
42
+
"type": "ref",
43
+
"ref": "app.bsky.notification.defs#filterablePreference"
44
+
},
45
+
"repostViaRepost": {
46
+
"type": "ref",
47
+
"ref": "app.bsky.notification.defs#filterablePreference"
48
+
},
49
+
"starterpackJoined": {
50
+
"type": "ref",
51
+
"ref": "app.bsky.notification.defs#preference"
52
+
},
53
+
"subscribedPost": {
54
+
"type": "ref",
55
+
"ref": "app.bsky.notification.defs#preference"
56
+
},
57
+
"unverified": {
58
+
"type": "ref",
59
+
"ref": "app.bsky.notification.defs#preference"
60
+
},
61
+
"verified": {
62
+
"type": "ref",
63
+
"ref": "app.bsky.notification.defs#preference"
64
+
}
65
+
}
66
+
}
67
+
},
68
+
"output": {
69
+
"encoding": "application/json",
70
+
"schema": {
71
+
"type": "object",
72
+
"required": ["preferences"],
73
+
"properties": {
74
+
"preferences": {
75
+
"type": "ref",
76
+
"ref": "app.bsky.notification.defs#preferences"
77
+
}
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
83
+
}
+30
lexicons/app/bsky/notification/registerPush.json
+30
lexicons/app/bsky/notification/registerPush.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.registerPush",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Register to receive push notifications, via a specified service, for the requesting account. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["serviceDid", "token", "platform", "appId"],
13
+
"properties": {
14
+
"serviceDid": { "type": "string", "format": "did" },
15
+
"token": { "type": "string" },
16
+
"platform": {
17
+
"type": "string",
18
+
"knownValues": ["ios", "android", "web"]
19
+
},
20
+
"appId": { "type": "string" },
21
+
"ageRestricted": {
22
+
"type": "boolean",
23
+
"description": "Set to true when the actor is age restricted"
24
+
}
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
+26
lexicons/app/bsky/notification/unregisterPush.json
+26
lexicons/app/bsky/notification/unregisterPush.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.unregisterPush",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["serviceDid", "token", "platform", "appId"],
13
+
"properties": {
14
+
"serviceDid": { "type": "string", "format": "did" },
15
+
"token": { "type": "string" },
16
+
"platform": {
17
+
"type": "string",
18
+
"knownValues": ["ios", "android", "web"]
19
+
},
20
+
"appId": { "type": "string" }
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
+20
lexicons/app/bsky/notification/updateSeen.json
+20
lexicons/app/bsky/notification/updateSeen.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.notification.updateSeen",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Notify server that the requesting account has seen notifications. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["seenAt"],
13
+
"properties": {
14
+
"seenAt": { "type": "string", "format": "datetime" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+51
lexicons/app/bsky/richtext/facet.json
+51
lexicons/app/bsky/richtext/facet.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.richtext.facet",
4
+
"defs": {
5
+
"main": {
6
+
"type": "object",
7
+
"description": "Annotation of a sub-string within rich text.",
8
+
"required": ["index", "features"],
9
+
"properties": {
10
+
"index": { "type": "ref", "ref": "#byteSlice" },
11
+
"features": {
12
+
"type": "array",
13
+
"items": { "type": "union", "refs": ["#mention", "#link", "#tag"] }
14
+
}
15
+
}
16
+
},
17
+
"mention": {
18
+
"type": "object",
19
+
"description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.",
20
+
"required": ["did"],
21
+
"properties": {
22
+
"did": { "type": "string", "format": "did" }
23
+
}
24
+
},
25
+
"link": {
26
+
"type": "object",
27
+
"description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.",
28
+
"required": ["uri"],
29
+
"properties": {
30
+
"uri": { "type": "string", "format": "uri" }
31
+
}
32
+
},
33
+
"tag": {
34
+
"type": "object",
35
+
"description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').",
36
+
"required": ["tag"],
37
+
"properties": {
38
+
"tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
39
+
}
40
+
},
41
+
"byteSlice": {
42
+
"type": "object",
43
+
"description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.",
44
+
"required": ["byteStart", "byteEnd"],
45
+
"properties": {
46
+
"byteStart": { "type": "integer", "minimum": 0 },
47
+
"byteEnd": { "type": "integer", "minimum": 0 }
48
+
}
49
+
}
50
+
}
51
+
}
+198
lexicons/app/bsky/unspecced/defs.json
+198
lexicons/app/bsky/unspecced/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.defs",
4
+
"defs": {
5
+
"skeletonSearchPost": {
6
+
"type": "object",
7
+
"required": ["uri"],
8
+
"properties": {
9
+
"uri": { "type": "string", "format": "at-uri" }
10
+
}
11
+
},
12
+
"skeletonSearchActor": {
13
+
"type": "object",
14
+
"required": ["did"],
15
+
"properties": {
16
+
"did": { "type": "string", "format": "did" }
17
+
}
18
+
},
19
+
"skeletonSearchStarterPack": {
20
+
"type": "object",
21
+
"required": ["uri"],
22
+
"properties": {
23
+
"uri": { "type": "string", "format": "at-uri" }
24
+
}
25
+
},
26
+
"trendingTopic": {
27
+
"type": "object",
28
+
"required": ["topic", "link"],
29
+
"properties": {
30
+
"topic": { "type": "string" },
31
+
"displayName": { "type": "string" },
32
+
"description": { "type": "string" },
33
+
"link": { "type": "string" }
34
+
}
35
+
},
36
+
"skeletonTrend": {
37
+
"type": "object",
38
+
"required": [
39
+
"topic",
40
+
"displayName",
41
+
"link",
42
+
"startedAt",
43
+
"postCount",
44
+
"dids"
45
+
],
46
+
"properties": {
47
+
"topic": { "type": "string" },
48
+
"displayName": { "type": "string" },
49
+
"link": { "type": "string" },
50
+
"startedAt": { "type": "string", "format": "datetime" },
51
+
"postCount": { "type": "integer" },
52
+
"status": { "type": "string", "knownValues": ["hot"] },
53
+
"category": { "type": "string" },
54
+
"dids": {
55
+
"type": "array",
56
+
"items": {
57
+
"type": "string",
58
+
"format": "did"
59
+
}
60
+
}
61
+
}
62
+
},
63
+
"trendView": {
64
+
"type": "object",
65
+
"required": [
66
+
"topic",
67
+
"displayName",
68
+
"link",
69
+
"startedAt",
70
+
"postCount",
71
+
"actors"
72
+
],
73
+
"properties": {
74
+
"topic": { "type": "string" },
75
+
"displayName": { "type": "string" },
76
+
"link": { "type": "string" },
77
+
"startedAt": { "type": "string", "format": "datetime" },
78
+
"postCount": { "type": "integer" },
79
+
"status": { "type": "string", "knownValues": ["hot"] },
80
+
"category": { "type": "string" },
81
+
"actors": {
82
+
"type": "array",
83
+
"items": {
84
+
"type": "ref",
85
+
"ref": "app.bsky.actor.defs#profileViewBasic"
86
+
}
87
+
}
88
+
}
89
+
},
90
+
"threadItemPost": {
91
+
"type": "object",
92
+
"required": [
93
+
"post",
94
+
"moreParents",
95
+
"moreReplies",
96
+
"opThread",
97
+
"hiddenByThreadgate",
98
+
"mutedByViewer"
99
+
],
100
+
"properties": {
101
+
"post": { "type": "ref", "ref": "app.bsky.feed.defs#postView" },
102
+
"moreParents": {
103
+
"type": "boolean",
104
+
"description": "This post has more parents that were not present in the response. This is just a boolean, without the number of parents."
105
+
},
106
+
"moreReplies": {
107
+
"type": "integer",
108
+
"description": "This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate."
109
+
},
110
+
"opThread": {
111
+
"type": "boolean",
112
+
"description": "This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread."
113
+
},
114
+
"hiddenByThreadgate": {
115
+
"type": "boolean",
116
+
"description": "The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread."
117
+
},
118
+
"mutedByViewer": {
119
+
"type": "boolean",
120
+
"description": "This is by an account muted by the viewer requesting it."
121
+
}
122
+
}
123
+
},
124
+
"threadItemNoUnauthenticated": {
125
+
"type": "object",
126
+
"properties": {}
127
+
},
128
+
"threadItemNotFound": {
129
+
"type": "object",
130
+
"properties": {}
131
+
},
132
+
"threadItemBlocked": {
133
+
"type": "object",
134
+
"required": ["author"],
135
+
"properties": {
136
+
"author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" }
137
+
}
138
+
},
139
+
"ageAssuranceState": {
140
+
"type": "object",
141
+
"description": "The computed state of the age assurance process, returned to the user in question on certain authenticated requests.",
142
+
"required": ["status"],
143
+
"properties": {
144
+
"lastInitiatedAt": {
145
+
"type": "string",
146
+
"format": "datetime",
147
+
"description": "The timestamp when this state was last updated."
148
+
},
149
+
"status": {
150
+
"type": "string",
151
+
"description": "The status of the age assurance process.",
152
+
"knownValues": ["unknown", "pending", "assured", "blocked"]
153
+
}
154
+
}
155
+
},
156
+
"ageAssuranceEvent": {
157
+
"type": "object",
158
+
"description": "Object used to store age assurance data in stash.",
159
+
"required": ["createdAt", "status", "attemptId"],
160
+
"properties": {
161
+
"createdAt": {
162
+
"type": "string",
163
+
"format": "datetime",
164
+
"description": "The date and time of this write operation."
165
+
},
166
+
"status": {
167
+
"type": "string",
168
+
"description": "The status of the age assurance process.",
169
+
"knownValues": ["unknown", "pending", "assured"]
170
+
},
171
+
"attemptId": {
172
+
"type": "string",
173
+
"description": "The unique identifier for this instance of the age assurance flow, in UUID format."
174
+
},
175
+
"email": {
176
+
"type": "string",
177
+
"description": "The email used for AA."
178
+
},
179
+
"initIp": {
180
+
"type": "string",
181
+
"description": "The IP address used when initiating the AA flow."
182
+
},
183
+
"initUa": {
184
+
"type": "string",
185
+
"description": "The user agent used when initiating the AA flow."
186
+
},
187
+
"completeIp": {
188
+
"type": "string",
189
+
"description": "The IP address used when completing the AA flow."
190
+
},
191
+
"completeUa": {
192
+
"type": "string",
193
+
"description": "The user agent used when completing the AA flow."
194
+
}
195
+
}
196
+
}
197
+
}
198
+
}
+17
lexicons/app/bsky/unspecced/getAgeAssuranceState.json
+17
lexicons/app/bsky/unspecced/getAgeAssuranceState.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getAgeAssuranceState",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "ref",
12
+
"ref": "app.bsky.unspecced.defs#ageAssuranceState"
13
+
}
14
+
}
15
+
}
16
+
}
17
+
}
+37
lexicons/app/bsky/unspecced/getConfig.json
+37
lexicons/app/bsky/unspecced/getConfig.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getConfig",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get miscellaneous runtime configuration.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": [],
13
+
"properties": {
14
+
"checkEmailConfirmed": { "type": "boolean" },
15
+
"liveNow": {
16
+
"type": "array",
17
+
"items": { "type": "ref", "ref": "#liveNowConfig" }
18
+
}
19
+
}
20
+
}
21
+
}
22
+
},
23
+
"liveNowConfig": {
24
+
"type": "object",
25
+
"required": ["did", "domains"],
26
+
"properties": {
27
+
"did": { "type": "string", "format": "did" },
28
+
"domains": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "string"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+37
lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json
+37
lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested starterpacks for onboarding",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 25,
15
+
"default": 10
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["starterPacks"],
24
+
"properties": {
25
+
"starterPacks": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.graph.defs#starterPackView"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+42
lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json
+42
lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 25,
20
+
"default": 10
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["starterPacks"],
29
+
"properties": {
30
+
"starterPacks": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "string",
34
+
"format": "at-uri"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+40
lexicons/app/bsky/unspecced/getPopularFeedGenerators.json
+40
lexicons/app/bsky/unspecced/getPopularFeedGenerators.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getPopularFeedGenerators",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "An unspecced view of globally popular feed generators.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": { "type": "string" },
18
+
"query": { "type": "string" }
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["feeds"],
26
+
"properties": {
27
+
"cursor": { "type": "string" },
28
+
"feeds": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "ref",
32
+
"ref": "app.bsky.feed.defs#generatorView"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
+56
lexicons/app/bsky/unspecced/getPostThreadOtherV2.json
+56
lexicons/app/bsky/unspecced/getPostThreadOtherV2.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getPostThreadOtherV2",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["anchor"],
11
+
"properties": {
12
+
"anchor": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) to post record. This is the anchor post."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["thread"],
24
+
"properties": {
25
+
"thread": {
26
+
"type": "array",
27
+
"description": "A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.",
28
+
"items": {
29
+
"type": "ref",
30
+
"ref": "#threadItem"
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
},
37
+
"threadItem": {
38
+
"type": "object",
39
+
"required": ["uri", "depth", "value"],
40
+
"properties": {
41
+
"uri": {
42
+
"type": "string",
43
+
"format": "at-uri"
44
+
},
45
+
"depth": {
46
+
"type": "integer",
47
+
"description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths."
48
+
},
49
+
"value": {
50
+
"type": "union",
51
+
"refs": ["app.bsky.unspecced.defs#threadItemPost"]
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
+94
lexicons/app/bsky/unspecced/getPostThreadV2.json
+94
lexicons/app/bsky/unspecced/getPostThreadV2.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getPostThreadV2",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["anchor"],
11
+
"properties": {
12
+
"anchor": {
13
+
"type": "string",
14
+
"format": "at-uri",
15
+
"description": "Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post."
16
+
},
17
+
"above": {
18
+
"type": "boolean",
19
+
"description": "Whether to include parents above the anchor.",
20
+
"default": true
21
+
},
22
+
"below": {
23
+
"type": "integer",
24
+
"description": "How many levels of replies to include below the anchor.",
25
+
"default": 6,
26
+
"minimum": 0,
27
+
"maximum": 20
28
+
},
29
+
"branchingFactor": {
30
+
"type": "integer",
31
+
"description": "Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).",
32
+
"default": 10,
33
+
"minimum": 0,
34
+
"maximum": 100
35
+
},
36
+
"sort": {
37
+
"type": "string",
38
+
"description": "Sorting for the thread replies.",
39
+
"knownValues": ["newest", "oldest", "top"],
40
+
"default": "oldest"
41
+
}
42
+
}
43
+
},
44
+
"output": {
45
+
"encoding": "application/json",
46
+
"schema": {
47
+
"type": "object",
48
+
"required": ["thread", "hasOtherReplies"],
49
+
"properties": {
50
+
"thread": {
51
+
"type": "array",
52
+
"description": "A flat list of thread items. The depth of each item is indicated by the depth property inside the item.",
53
+
"items": {
54
+
"type": "ref",
55
+
"ref": "#threadItem"
56
+
}
57
+
},
58
+
"threadgate": {
59
+
"type": "ref",
60
+
"ref": "app.bsky.feed.defs#threadgateView"
61
+
},
62
+
"hasOtherReplies": {
63
+
"type": "boolean",
64
+
"description": "Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them."
65
+
}
66
+
}
67
+
}
68
+
}
69
+
},
70
+
"threadItem": {
71
+
"type": "object",
72
+
"required": ["uri", "depth", "value"],
73
+
"properties": {
74
+
"uri": {
75
+
"type": "string",
76
+
"format": "at-uri"
77
+
},
78
+
"depth": {
79
+
"type": "integer",
80
+
"description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths."
81
+
},
82
+
"value": {
83
+
"type": "union",
84
+
"refs": [
85
+
"app.bsky.unspecced.defs#threadItemPost",
86
+
"app.bsky.unspecced.defs#threadItemNoUnauthenticated",
87
+
"app.bsky.unspecced.defs#threadItemNotFound",
88
+
"app.bsky.unspecced.defs#threadItemBlocked"
89
+
]
90
+
}
91
+
}
92
+
}
93
+
}
94
+
}
+37
lexicons/app/bsky/unspecced/getSuggestedFeeds.json
+37
lexicons/app/bsky/unspecced/getSuggestedFeeds.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedFeeds",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested feeds",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 25,
15
+
"default": 10
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["feeds"],
24
+
"properties": {
25
+
"feeds": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.feed.defs#generatorView"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+42
lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json
+42
lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedFeedsSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 25,
20
+
"default": 10
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["feeds"],
29
+
"properties": {
30
+
"feeds": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "string",
34
+
"format": "at-uri"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+37
lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json
+37
lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedStarterPacks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested starterpacks",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 25,
15
+
"default": 10
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["starterPacks"],
24
+
"properties": {
25
+
"starterPacks": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.graph.defs#starterPackView"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+42
lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json
+42
lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 25,
20
+
"default": 10
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["starterPacks"],
29
+
"properties": {
30
+
"starterPacks": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "string",
34
+
"format": "at-uri"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+41
lexicons/app/bsky/unspecced/getSuggestedUsers.json
+41
lexicons/app/bsky/unspecced/getSuggestedUsers.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedUsers",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggested users",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"category": {
12
+
"type": "string",
13
+
"description": "Category of users to get suggestions for."
14
+
},
15
+
"limit": {
16
+
"type": "integer",
17
+
"minimum": 1,
18
+
"maximum": 50,
19
+
"default": 25
20
+
}
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/json",
25
+
"schema": {
26
+
"type": "object",
27
+
"required": ["actors"],
28
+
"properties": {
29
+
"actors": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.actor.defs#profileView"
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
+46
lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json
+46
lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestedUsersSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
15
+
},
16
+
"category": {
17
+
"type": "string",
18
+
"description": "Category of users to get suggestions for."
19
+
},
20
+
"limit": {
21
+
"type": "integer",
22
+
"minimum": 1,
23
+
"maximum": 50,
24
+
"default": 25
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["dids"],
33
+
"properties": {
34
+
"dids": {
35
+
"type": "array",
36
+
"items": {
37
+
"type": "string",
38
+
"format": "did"
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
+58
lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json
+58
lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getSuggestionsSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 100,
20
+
"default": 50
21
+
},
22
+
"cursor": { "type": "string" },
23
+
"relativeToDid": {
24
+
"type": "string",
25
+
"format": "did",
26
+
"description": "DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer."
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"required": ["actors"],
35
+
"properties": {
36
+
"cursor": { "type": "string" },
37
+
"actors": {
38
+
"type": "array",
39
+
"items": {
40
+
"type": "ref",
41
+
"ref": "app.bsky.unspecced.defs#skeletonSearchActor"
42
+
}
43
+
},
44
+
"relativeToDid": {
45
+
"type": "string",
46
+
"format": "did",
47
+
"description": "DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer."
48
+
},
49
+
"recId": {
50
+
"type": "integer",
51
+
"description": "Snowflake for this recommendation, use when submitting recommendation events."
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
57
+
}
58
+
}
+42
lexicons/app/bsky/unspecced/getTaggedSuggestions.json
+42
lexicons/app/bsky/unspecced/getTaggedSuggestions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getTaggedSuggestions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of suggestions (feeds and users) tagged with categories",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["suggestions"],
17
+
"properties": {
18
+
"suggestions": {
19
+
"type": "array",
20
+
"items": {
21
+
"type": "ref",
22
+
"ref": "#suggestion"
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
28
+
},
29
+
"suggestion": {
30
+
"type": "object",
31
+
"required": ["tag", "subjectType", "subject"],
32
+
"properties": {
33
+
"tag": { "type": "string" },
34
+
"subjectType": {
35
+
"type": "string",
36
+
"knownValues": ["actor", "feed"]
37
+
},
38
+
"subject": { "type": "string", "format": "uri" }
39
+
}
40
+
}
41
+
}
42
+
}
+49
lexicons/app/bsky/unspecced/getTrendingTopics.json
+49
lexicons/app/bsky/unspecced/getTrendingTopics.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getTrendingTopics",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a list of trending topics",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 25,
20
+
"default": 10
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["topics", "suggested"],
29
+
"properties": {
30
+
"topics": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "ref",
34
+
"ref": "app.bsky.unspecced.defs#trendingTopic"
35
+
}
36
+
},
37
+
"suggested": {
38
+
"type": "array",
39
+
"items": {
40
+
"type": "ref",
41
+
"ref": "app.bsky.unspecced.defs#trendingTopic"
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
+37
lexicons/app/bsky/unspecced/getTrends.json
+37
lexicons/app/bsky/unspecced/getTrends.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getTrends",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the current trends on the network",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 25,
15
+
"default": 10
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["trends"],
24
+
"properties": {
25
+
"trends": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "app.bsky.unspecced.defs#trendView"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+42
lexicons/app/bsky/unspecced/getTrendsSkeleton.json
+42
lexicons/app/bsky/unspecced/getTrendsSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.getTrendsSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"viewer": {
12
+
"type": "string",
13
+
"format": "did",
14
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 25,
20
+
"default": 10
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["trends"],
29
+
"properties": {
30
+
"trends": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "ref",
34
+
"ref": "app.bsky.unspecced.defs#skeletonTrend"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+43
lexicons/app/bsky/unspecced/initAgeAssurance.json
+43
lexicons/app/bsky/unspecced/initAgeAssurance.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.initAgeAssurance",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["email", "language", "countryCode"],
13
+
"properties": {
14
+
"email": {
15
+
"type": "string",
16
+
"description": "The user's email address to receive assurance instructions."
17
+
},
18
+
"language": {
19
+
"type": "string",
20
+
"description": "The user's preferred language for communication during the assurance process."
21
+
},
22
+
"countryCode": {
23
+
"type": "string",
24
+
"description": "An ISO 3166-1 alpha-2 code of the user's location."
25
+
}
26
+
}
27
+
}
28
+
},
29
+
"output": {
30
+
"encoding": "application/json",
31
+
"schema": {
32
+
"type": "ref",
33
+
"ref": "app.bsky.unspecced.defs#ageAssuranceState"
34
+
}
35
+
},
36
+
"errors": [
37
+
{ "name": "InvalidEmail" },
38
+
{ "name": "DidTooLong" },
39
+
{ "name": "InvalidInitiation" }
40
+
]
41
+
}
42
+
}
43
+
}
+61
lexicons/app/bsky/unspecced/searchActorsSkeleton.json
+61
lexicons/app/bsky/unspecced/searchActorsSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.searchActorsSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Backend Actors (profile) search, returns only skeleton.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax."
15
+
},
16
+
"viewer": {
17
+
"type": "string",
18
+
"format": "did",
19
+
"description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
20
+
},
21
+
"typeahead": {
22
+
"type": "boolean",
23
+
"description": "If true, acts as fast/simple 'typeahead' query."
24
+
},
25
+
"limit": {
26
+
"type": "integer",
27
+
"minimum": 1,
28
+
"maximum": 100,
29
+
"default": 25
30
+
},
31
+
"cursor": {
32
+
"type": "string",
33
+
"description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
34
+
}
35
+
}
36
+
},
37
+
"output": {
38
+
"encoding": "application/json",
39
+
"schema": {
40
+
"type": "object",
41
+
"required": ["actors"],
42
+
"properties": {
43
+
"cursor": { "type": "string" },
44
+
"hitsTotal": {
45
+
"type": "integer",
46
+
"description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
47
+
},
48
+
"actors": {
49
+
"type": "array",
50
+
"items": {
51
+
"type": "ref",
52
+
"ref": "app.bsky.unspecced.defs#skeletonSearchActor"
53
+
}
54
+
}
55
+
}
56
+
}
57
+
},
58
+
"errors": [{ "name": "BadQueryString" }]
59
+
}
60
+
}
61
+
}
+104
lexicons/app/bsky/unspecced/searchPostsSkeleton.json
+104
lexicons/app/bsky/unspecced/searchPostsSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.searchPostsSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Backend Posts search, returns only skeleton",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
15
+
},
16
+
"sort": {
17
+
"type": "string",
18
+
"knownValues": ["top", "latest"],
19
+
"default": "latest",
20
+
"description": "Specifies the ranking order of results."
21
+
},
22
+
"since": {
23
+
"type": "string",
24
+
"description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)."
25
+
},
26
+
"until": {
27
+
"type": "string",
28
+
"description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)."
29
+
},
30
+
"mentions": {
31
+
"type": "string",
32
+
"format": "at-identifier",
33
+
"description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions."
34
+
},
35
+
"author": {
36
+
"type": "string",
37
+
"format": "at-identifier",
38
+
"description": "Filter to posts by the given account. Handles are resolved to DID before query-time."
39
+
},
40
+
"lang": {
41
+
"type": "string",
42
+
"format": "language",
43
+
"description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection."
44
+
},
45
+
"domain": {
46
+
"type": "string",
47
+
"description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization."
48
+
},
49
+
"url": {
50
+
"type": "string",
51
+
"format": "uri",
52
+
"description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching."
53
+
},
54
+
"tag": {
55
+
"type": "array",
56
+
"items": {
57
+
"type": "string",
58
+
"maxLength": 640,
59
+
"maxGraphemes": 64
60
+
},
61
+
"description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching."
62
+
},
63
+
"viewer": {
64
+
"type": "string",
65
+
"format": "did",
66
+
"description": "DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries."
67
+
},
68
+
"limit": {
69
+
"type": "integer",
70
+
"minimum": 1,
71
+
"maximum": 100,
72
+
"default": 25
73
+
},
74
+
"cursor": {
75
+
"type": "string",
76
+
"description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
77
+
}
78
+
}
79
+
},
80
+
"output": {
81
+
"encoding": "application/json",
82
+
"schema": {
83
+
"type": "object",
84
+
"required": ["posts"],
85
+
"properties": {
86
+
"cursor": { "type": "string" },
87
+
"hitsTotal": {
88
+
"type": "integer",
89
+
"description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
90
+
},
91
+
"posts": {
92
+
"type": "array",
93
+
"items": {
94
+
"type": "ref",
95
+
"ref": "app.bsky.unspecced.defs#skeletonSearchPost"
96
+
}
97
+
}
98
+
}
99
+
}
100
+
},
101
+
"errors": [{ "name": "BadQueryString" }]
102
+
}
103
+
}
104
+
}
+63
lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json
+63
lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.unspecced.searchStarterPacksSkeleton",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Backend Starter Pack search, returns only skeleton.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
15
+
},
16
+
"viewer": {
17
+
"type": "string",
18
+
"format": "did",
19
+
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
20
+
},
21
+
"limit": {
22
+
"type": "integer",
23
+
"minimum": 1,
24
+
"maximum": 100,
25
+
"default": 25
26
+
},
27
+
"cursor": {
28
+
"type": "string",
29
+
"description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
30
+
}
31
+
}
32
+
},
33
+
"output": {
34
+
"encoding": "application/json",
35
+
"schema": {
36
+
"type": "object",
37
+
"required": ["starterPacks"],
38
+
"properties": {
39
+
"cursor": {
40
+
"type": "string"
41
+
},
42
+
"hitsTotal": {
43
+
"type": "integer",
44
+
"description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
45
+
},
46
+
"starterPacks": {
47
+
"type": "array",
48
+
"items": {
49
+
"type": "ref",
50
+
"ref": "app.bsky.unspecced.defs#skeletonSearchStarterPack"
51
+
}
52
+
}
53
+
}
54
+
}
55
+
},
56
+
"errors": [
57
+
{
58
+
"name": "BadQueryString"
59
+
}
60
+
]
61
+
}
62
+
}
63
+
}
+28
lexicons/app/bsky/video/defs.json
+28
lexicons/app/bsky/video/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.video.defs",
4
+
"defs": {
5
+
"jobStatus": {
6
+
"type": "object",
7
+
"required": ["jobId", "did", "state"],
8
+
"properties": {
9
+
"jobId": { "type": "string" },
10
+
"did": { "type": "string", "format": "did" },
11
+
"state": {
12
+
"type": "string",
13
+
"description": "The state of the video processing job. All values not listed as a known value indicate that the job is in process.",
14
+
"knownValues": ["JOB_STATE_COMPLETED", "JOB_STATE_FAILED"]
15
+
},
16
+
"progress": {
17
+
"type": "integer",
18
+
"minimum": 0,
19
+
"maximum": 100,
20
+
"description": "Progress within the current processing state."
21
+
},
22
+
"blob": { "type": "blob" },
23
+
"error": { "type": "string" },
24
+
"message": { "type": "string" }
25
+
}
26
+
}
27
+
}
28
+
}
+32
lexicons/app/bsky/video/getJobStatus.json
+32
lexicons/app/bsky/video/getJobStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.video.getJobStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get status details for a video processing job.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["jobId"],
11
+
"properties": {
12
+
"jobId": {
13
+
"type": "string"
14
+
}
15
+
}
16
+
},
17
+
"output": {
18
+
"encoding": "application/json",
19
+
"schema": {
20
+
"type": "object",
21
+
"required": ["jobStatus"],
22
+
"properties": {
23
+
"jobStatus": {
24
+
"type": "ref",
25
+
"ref": "app.bsky.video.defs#jobStatus"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
+24
lexicons/app/bsky/video/getUploadLimits.json
+24
lexicons/app/bsky/video/getUploadLimits.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.video.getUploadLimits",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get video upload limits for the authenticated user.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["canUpload"],
13
+
"properties": {
14
+
"canUpload": { "type": "boolean" },
15
+
"remainingDailyVideos": { "type": "integer" },
16
+
"remainingDailyBytes": { "type": "integer" },
17
+
"message": { "type": "string" },
18
+
"error": { "type": "string" }
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+26
lexicons/app/bsky/video/uploadVideo.json
+26
lexicons/app/bsky/video/uploadVideo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.video.uploadVideo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Upload a video to be processed then stored on the PDS.",
8
+
"input": {
9
+
"encoding": "video/mp4"
10
+
},
11
+
"output": {
12
+
"encoding": "application/json",
13
+
"schema": {
14
+
"type": "object",
15
+
"required": ["jobStatus"],
16
+
"properties": {
17
+
"jobStatus": {
18
+
"type": "ref",
19
+
"ref": "app.bsky.video.defs#jobStatus"
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
+21
lexicons/chat/bsky/actor/declaration.json
+21
lexicons/chat/bsky/actor/declaration.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.actor.declaration",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of a Bluesky chat account.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["allowIncoming"],
12
+
"properties": {
13
+
"allowIncoming": {
14
+
"type": "string",
15
+
"knownValues": ["all", "none", "following"]
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+37
lexicons/chat/bsky/actor/defs.json
+37
lexicons/chat/bsky/actor/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.actor.defs",
4
+
"defs": {
5
+
"profileViewBasic": {
6
+
"type": "object",
7
+
"required": ["did", "handle"],
8
+
"properties": {
9
+
"did": { "type": "string", "format": "did" },
10
+
"handle": { "type": "string", "format": "handle" },
11
+
"displayName": {
12
+
"type": "string",
13
+
"maxGraphemes": 64,
14
+
"maxLength": 640
15
+
},
16
+
"avatar": { "type": "string", "format": "uri" },
17
+
"associated": {
18
+
"type": "ref",
19
+
"ref": "app.bsky.actor.defs#profileAssociated"
20
+
},
21
+
"viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" },
22
+
"labels": {
23
+
"type": "array",
24
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
25
+
},
26
+
"chatDisabled": {
27
+
"type": "boolean",
28
+
"description": "Set to true when the actor cannot actively participate in conversations"
29
+
},
30
+
"verification": {
31
+
"type": "ref",
32
+
"ref": "app.bsky.actor.defs#verificationState"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+16
lexicons/chat/bsky/actor/deleteAccount.json
+16
lexicons/chat/bsky/actor/deleteAccount.json
+12
lexicons/chat/bsky/actor/exportAccountData.json
+12
lexicons/chat/bsky/actor/exportAccountData.json
+49
lexicons/chat/bsky/authFullChatClient.json
+49
lexicons/chat/bsky/authFullChatClient.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.authFullChatClient",
4
+
"defs": {
5
+
"main": {
6
+
"type": "permission-set",
7
+
"title": "Full Chat Client (All Conversations)",
8
+
"title:lang": {},
9
+
"detail": "Control of all chat conversations and configuration management.",
10
+
"detail:lang": {
11
+
"en": "All Chat Conversations"
12
+
},
13
+
"permissions": [
14
+
{
15
+
"type": "permission",
16
+
"resource": "rpc",
17
+
"inheritAud": true,
18
+
"lxm": [
19
+
"chat.bsky.actor.deleteAccount",
20
+
"chat.bsky.convo.acceptConvo",
21
+
"chat.bsky.convo.addReaction",
22
+
"chat.bsky.convo.deleteMessageForSelf",
23
+
"chat.bsky.convo.exportAccountData",
24
+
"chat.bsky.convo.getConvo",
25
+
"chat.bsky.convo.getConvoAvailability",
26
+
"chat.bsky.convo.getConvoForMembers",
27
+
"chat.bsky.convo.getLog",
28
+
"chat.bsky.convo.getMessages",
29
+
"chat.bsky.convo.leaveConvo",
30
+
"chat.bsky.convo.listConvos",
31
+
"chat.bsky.convo.muteConvo",
32
+
"chat.bsky.convo.removeReaction",
33
+
"chat.bsky.convo.sendMessage",
34
+
"chat.bsky.convo.sendMessageBatch",
35
+
"chat.bsky.convo.unmuteConvo",
36
+
"chat.bsky.convo.updateAllRead",
37
+
"chat.bsky.convo.updateRead"
38
+
]
39
+
},
40
+
{
41
+
"type": "permission",
42
+
"resource": "repo",
43
+
"action": ["create", "update", "delete"],
44
+
"collection": ["chat.bsky.actor.declaration"]
45
+
}
46
+
]
47
+
}
48
+
}
49
+
}
+31
lexicons/chat/bsky/convo/acceptConvo.json
+31
lexicons/chat/bsky/convo/acceptConvo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.acceptConvo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" }
14
+
}
15
+
}
16
+
},
17
+
"output": {
18
+
"encoding": "application/json",
19
+
"schema": {
20
+
"type": "object",
21
+
"properties": {
22
+
"rev": {
23
+
"description": "Rev when the convo was accepted. If not present, the convo was already accepted.",
24
+
"type": "string"
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
+55
lexicons/chat/bsky/convo/addReaction.json
+55
lexicons/chat/bsky/convo/addReaction.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.addReaction",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["convoId", "messageId", "value"],
13
+
"properties": {
14
+
"convoId": { "type": "string" },
15
+
"messageId": { "type": "string" },
16
+
"value": {
17
+
"type": "string",
18
+
"minLength": 1,
19
+
"maxLength": 64,
20
+
"minGraphemes": 1,
21
+
"maxGraphemes": 1
22
+
}
23
+
}
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["message"],
31
+
"properties": {
32
+
"message": {
33
+
"type": "ref",
34
+
"ref": "chat.bsky.convo.defs#messageView"
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"errors": [
40
+
{
41
+
"name": "ReactionMessageDeleted",
42
+
"description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
43
+
},
44
+
{
45
+
"name": "ReactionLimitReached",
46
+
"description": "Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent."
47
+
},
48
+
{
49
+
"name": "ReactionInvalidValue",
50
+
"description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji."
51
+
}
52
+
]
53
+
}
54
+
}
55
+
}
+236
lexicons/chat/bsky/convo/defs.json
+236
lexicons/chat/bsky/convo/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.defs",
4
+
"defs": {
5
+
"messageRef": {
6
+
"type": "object",
7
+
"required": ["did", "messageId", "convoId"],
8
+
"properties": {
9
+
"did": { "type": "string", "format": "did" },
10
+
"convoId": { "type": "string" },
11
+
"messageId": { "type": "string" }
12
+
}
13
+
},
14
+
"messageInput": {
15
+
"type": "object",
16
+
"required": ["text"],
17
+
"properties": {
18
+
"text": {
19
+
"type": "string",
20
+
"maxLength": 10000,
21
+
"maxGraphemes": 1000
22
+
},
23
+
"facets": {
24
+
"type": "array",
25
+
"description": "Annotations of text (mentions, URLs, hashtags, etc)",
26
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
27
+
},
28
+
"embed": {
29
+
"type": "union",
30
+
"refs": ["app.bsky.embed.record"]
31
+
}
32
+
}
33
+
},
34
+
"messageView": {
35
+
"type": "object",
36
+
"required": ["id", "rev", "text", "sender", "sentAt"],
37
+
"properties": {
38
+
"id": { "type": "string" },
39
+
"rev": { "type": "string" },
40
+
"text": {
41
+
"type": "string",
42
+
"maxLength": 10000,
43
+
"maxGraphemes": 1000
44
+
},
45
+
"facets": {
46
+
"type": "array",
47
+
"description": "Annotations of text (mentions, URLs, hashtags, etc)",
48
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
49
+
},
50
+
"embed": {
51
+
"type": "union",
52
+
"refs": ["app.bsky.embed.record#view"]
53
+
},
54
+
"reactions": {
55
+
"type": "array",
56
+
"description": "Reactions to this message, in ascending order of creation time.",
57
+
"items": { "type": "ref", "ref": "#reactionView" }
58
+
},
59
+
"sender": { "type": "ref", "ref": "#messageViewSender" },
60
+
"sentAt": { "type": "string", "format": "datetime" }
61
+
}
62
+
},
63
+
"deletedMessageView": {
64
+
"type": "object",
65
+
"required": ["id", "rev", "sender", "sentAt"],
66
+
"properties": {
67
+
"id": { "type": "string" },
68
+
"rev": { "type": "string" },
69
+
"sender": { "type": "ref", "ref": "#messageViewSender" },
70
+
"sentAt": { "type": "string", "format": "datetime" }
71
+
}
72
+
},
73
+
"messageViewSender": {
74
+
"type": "object",
75
+
"required": ["did"],
76
+
"properties": {
77
+
"did": { "type": "string", "format": "did" }
78
+
}
79
+
},
80
+
"reactionView": {
81
+
"type": "object",
82
+
"required": ["value", "sender", "createdAt"],
83
+
"properties": {
84
+
"value": { "type": "string" },
85
+
"sender": { "type": "ref", "ref": "#reactionViewSender" },
86
+
"createdAt": { "type": "string", "format": "datetime" }
87
+
}
88
+
},
89
+
"reactionViewSender": {
90
+
"type": "object",
91
+
"required": ["did"],
92
+
"properties": {
93
+
"did": { "type": "string", "format": "did" }
94
+
}
95
+
},
96
+
"messageAndReactionView": {
97
+
"type": "object",
98
+
"required": ["message", "reaction"],
99
+
"properties": {
100
+
"message": { "type": "ref", "ref": "#messageView" },
101
+
"reaction": { "type": "ref", "ref": "#reactionView" }
102
+
}
103
+
},
104
+
"convoView": {
105
+
"type": "object",
106
+
"required": ["id", "rev", "members", "muted", "unreadCount"],
107
+
"properties": {
108
+
"id": { "type": "string" },
109
+
"rev": { "type": "string" },
110
+
"members": {
111
+
"type": "array",
112
+
"items": {
113
+
"type": "ref",
114
+
"ref": "chat.bsky.actor.defs#profileViewBasic"
115
+
}
116
+
},
117
+
"lastMessage": {
118
+
"type": "union",
119
+
"refs": ["#messageView", "#deletedMessageView"]
120
+
},
121
+
"lastReaction": {
122
+
"type": "union",
123
+
"refs": ["#messageAndReactionView"]
124
+
},
125
+
"muted": { "type": "boolean" },
126
+
"status": {
127
+
"type": "string",
128
+
"knownValues": ["request", "accepted"]
129
+
},
130
+
"unreadCount": { "type": "integer" }
131
+
}
132
+
},
133
+
"logBeginConvo": {
134
+
"type": "object",
135
+
"required": ["rev", "convoId"],
136
+
"properties": {
137
+
"rev": { "type": "string" },
138
+
"convoId": { "type": "string" }
139
+
}
140
+
},
141
+
"logAcceptConvo": {
142
+
"type": "object",
143
+
"required": ["rev", "convoId"],
144
+
"properties": {
145
+
"rev": { "type": "string" },
146
+
"convoId": { "type": "string" }
147
+
}
148
+
},
149
+
"logLeaveConvo": {
150
+
"type": "object",
151
+
"required": ["rev", "convoId"],
152
+
"properties": {
153
+
"rev": { "type": "string" },
154
+
"convoId": { "type": "string" }
155
+
}
156
+
},
157
+
"logMuteConvo": {
158
+
"type": "object",
159
+
"required": ["rev", "convoId"],
160
+
"properties": {
161
+
"rev": { "type": "string" },
162
+
"convoId": { "type": "string" }
163
+
}
164
+
},
165
+
"logUnmuteConvo": {
166
+
"type": "object",
167
+
"required": ["rev", "convoId"],
168
+
"properties": {
169
+
"rev": { "type": "string" },
170
+
"convoId": { "type": "string" }
171
+
}
172
+
},
173
+
"logCreateMessage": {
174
+
"type": "object",
175
+
"required": ["rev", "convoId", "message"],
176
+
"properties": {
177
+
"rev": { "type": "string" },
178
+
"convoId": { "type": "string" },
179
+
"message": {
180
+
"type": "union",
181
+
"refs": ["#messageView", "#deletedMessageView"]
182
+
}
183
+
}
184
+
},
185
+
"logDeleteMessage": {
186
+
"type": "object",
187
+
"required": ["rev", "convoId", "message"],
188
+
"properties": {
189
+
"rev": { "type": "string" },
190
+
"convoId": { "type": "string" },
191
+
"message": {
192
+
"type": "union",
193
+
"refs": ["#messageView", "#deletedMessageView"]
194
+
}
195
+
}
196
+
},
197
+
"logReadMessage": {
198
+
"type": "object",
199
+
"required": ["rev", "convoId", "message"],
200
+
"properties": {
201
+
"rev": { "type": "string" },
202
+
"convoId": { "type": "string" },
203
+
"message": {
204
+
"type": "union",
205
+
"refs": ["#messageView", "#deletedMessageView"]
206
+
}
207
+
}
208
+
},
209
+
"logAddReaction": {
210
+
"type": "object",
211
+
"required": ["rev", "convoId", "message", "reaction"],
212
+
"properties": {
213
+
"rev": { "type": "string" },
214
+
"convoId": { "type": "string" },
215
+
"message": {
216
+
"type": "union",
217
+
"refs": ["#messageView", "#deletedMessageView"]
218
+
},
219
+
"reaction": { "type": "ref", "ref": "#reactionView" }
220
+
}
221
+
},
222
+
"logRemoveReaction": {
223
+
"type": "object",
224
+
"required": ["rev", "convoId", "message", "reaction"],
225
+
"properties": {
226
+
"rev": { "type": "string" },
227
+
"convoId": { "type": "string" },
228
+
"message": {
229
+
"type": "union",
230
+
"refs": ["#messageView", "#deletedMessageView"]
231
+
},
232
+
"reaction": { "type": "ref", "ref": "#reactionView" }
233
+
}
234
+
}
235
+
}
236
+
}
+27
lexicons/chat/bsky/convo/deleteMessageForSelf.json
+27
lexicons/chat/bsky/convo/deleteMessageForSelf.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.deleteMessageForSelf",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId", "messageId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" },
14
+
"messageId": { "type": "string" }
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "ref",
22
+
"ref": "chat.bsky.convo.defs#deletedMessageView"
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+29
lexicons/chat/bsky/convo/getConvo.json
+29
lexicons/chat/bsky/convo/getConvo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.getConvo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": ["convoId"],
10
+
"properties": {
11
+
"convoId": { "type": "string" }
12
+
}
13
+
},
14
+
"output": {
15
+
"encoding": "application/json",
16
+
"schema": {
17
+
"type": "object",
18
+
"required": ["convo"],
19
+
"properties": {
20
+
"convo": {
21
+
"type": "ref",
22
+
"ref": "chat.bsky.convo.defs#convoView"
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
+41
lexicons/chat/bsky/convo/getConvoAvailability.json
+41
lexicons/chat/bsky/convo/getConvoAvailability.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.getConvoAvailability",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["members"],
11
+
"properties": {
12
+
"members": {
13
+
"type": "array",
14
+
"minLength": 1,
15
+
"maxLength": 10,
16
+
"items": {
17
+
"type": "string",
18
+
"format": "did"
19
+
}
20
+
}
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/json",
25
+
"schema": {
26
+
"type": "object",
27
+
"required": ["canChat"],
28
+
"properties": {
29
+
"canChat": {
30
+
"type": "boolean"
31
+
},
32
+
"convo": {
33
+
"type": "ref",
34
+
"ref": "chat.bsky.convo.defs#convoView"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
+37
lexicons/chat/bsky/convo/getConvoForMembers.json
+37
lexicons/chat/bsky/convo/getConvoForMembers.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.getConvoForMembers",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": ["members"],
10
+
"properties": {
11
+
"members": {
12
+
"type": "array",
13
+
"minLength": 1,
14
+
"maxLength": 10,
15
+
"items": {
16
+
"type": "string",
17
+
"format": "did"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["convo"],
27
+
"properties": {
28
+
"convo": {
29
+
"type": "ref",
30
+
"ref": "chat.bsky.convo.defs#convoView"
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+44
lexicons/chat/bsky/convo/getLog.json
+44
lexicons/chat/bsky/convo/getLog.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.getLog",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": [],
10
+
"properties": {
11
+
"cursor": { "type": "string" }
12
+
}
13
+
},
14
+
"output": {
15
+
"encoding": "application/json",
16
+
"schema": {
17
+
"type": "object",
18
+
"required": ["logs"],
19
+
"properties": {
20
+
"cursor": { "type": "string" },
21
+
"logs": {
22
+
"type": "array",
23
+
"items": {
24
+
"type": "union",
25
+
"refs": [
26
+
"chat.bsky.convo.defs#logBeginConvo",
27
+
"chat.bsky.convo.defs#logAcceptConvo",
28
+
"chat.bsky.convo.defs#logLeaveConvo",
29
+
"chat.bsky.convo.defs#logMuteConvo",
30
+
"chat.bsky.convo.defs#logUnmuteConvo",
31
+
"chat.bsky.convo.defs#logCreateMessage",
32
+
"chat.bsky.convo.defs#logDeleteMessage",
33
+
"chat.bsky.convo.defs#logReadMessage",
34
+
"chat.bsky.convo.defs#logAddReaction",
35
+
"chat.bsky.convo.defs#logRemoveReaction"
36
+
]
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
+43
lexicons/chat/bsky/convo/getMessages.json
+43
lexicons/chat/bsky/convo/getMessages.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.getMessages",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": ["convoId"],
10
+
"properties": {
11
+
"convoId": { "type": "string" },
12
+
"limit": {
13
+
"type": "integer",
14
+
"minimum": 1,
15
+
"maximum": 100,
16
+
"default": 50
17
+
},
18
+
"cursor": { "type": "string" }
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["messages"],
26
+
"properties": {
27
+
"cursor": { "type": "string" },
28
+
"messages": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "union",
32
+
"refs": [
33
+
"chat.bsky.convo.defs#messageView",
34
+
"chat.bsky.convo.defs#deletedMessageView"
35
+
]
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+30
lexicons/chat/bsky/convo/leaveConvo.json
+30
lexicons/chat/bsky/convo/leaveConvo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.leaveConvo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" }
14
+
}
15
+
}
16
+
},
17
+
"output": {
18
+
"encoding": "application/json",
19
+
"schema": {
20
+
"type": "object",
21
+
"required": ["convoId", "rev"],
22
+
"properties": {
23
+
"convoId": { "type": "string" },
24
+
"rev": { "type": "string" }
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
+46
lexicons/chat/bsky/convo/listConvos.json
+46
lexicons/chat/bsky/convo/listConvos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.listConvos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"properties": {
10
+
"limit": {
11
+
"type": "integer",
12
+
"minimum": 1,
13
+
"maximum": 100,
14
+
"default": 50
15
+
},
16
+
"cursor": { "type": "string" },
17
+
"readState": {
18
+
"type": "string",
19
+
"knownValues": ["unread"]
20
+
},
21
+
"status": {
22
+
"type": "string",
23
+
"knownValues": ["request", "accepted"]
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["convos"],
32
+
"properties": {
33
+
"cursor": { "type": "string" },
34
+
"convos": {
35
+
"type": "array",
36
+
"items": {
37
+
"type": "ref",
38
+
"ref": "chat.bsky.convo.defs#convoView"
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
+32
lexicons/chat/bsky/convo/muteConvo.json
+32
lexicons/chat/bsky/convo/muteConvo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.muteConvo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" }
14
+
}
15
+
}
16
+
},
17
+
"output": {
18
+
"encoding": "application/json",
19
+
"schema": {
20
+
"type": "object",
21
+
"required": ["convo"],
22
+
"properties": {
23
+
"convo": {
24
+
"type": "ref",
25
+
"ref": "chat.bsky.convo.defs#convoView"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
+51
lexicons/chat/bsky/convo/removeReaction.json
+51
lexicons/chat/bsky/convo/removeReaction.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.removeReaction",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["convoId", "messageId", "value"],
13
+
"properties": {
14
+
"convoId": { "type": "string" },
15
+
"messageId": { "type": "string" },
16
+
"value": {
17
+
"type": "string",
18
+
"minLength": 1,
19
+
"maxLength": 64,
20
+
"minGraphemes": 1,
21
+
"maxGraphemes": 1
22
+
}
23
+
}
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["message"],
31
+
"properties": {
32
+
"message": {
33
+
"type": "ref",
34
+
"ref": "chat.bsky.convo.defs#messageView"
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"errors": [
40
+
{
41
+
"name": "ReactionMessageDeleted",
42
+
"description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
43
+
},
44
+
{
45
+
"name": "ReactionInvalidValue",
46
+
"description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji."
47
+
}
48
+
]
49
+
}
50
+
}
51
+
}
+30
lexicons/chat/bsky/convo/sendMessage.json
+30
lexicons/chat/bsky/convo/sendMessage.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.sendMessage",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId", "message"],
12
+
"properties": {
13
+
"convoId": { "type": "string" },
14
+
"message": {
15
+
"type": "ref",
16
+
"ref": "chat.bsky.convo.defs#messageInput"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "ref",
25
+
"ref": "chat.bsky.convo.defs#messageView"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
+53
lexicons/chat/bsky/convo/sendMessageBatch.json
+53
lexicons/chat/bsky/convo/sendMessageBatch.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.sendMessageBatch",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["items"],
12
+
"properties": {
13
+
"items": {
14
+
"type": "array",
15
+
"maxLength": 100,
16
+
"items": {
17
+
"type": "ref",
18
+
"ref": "#batchItem"
19
+
}
20
+
}
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["items"],
29
+
"properties": {
30
+
"items": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "ref",
34
+
"ref": "chat.bsky.convo.defs#messageView"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
},
41
+
"batchItem": {
42
+
"type": "object",
43
+
"required": ["convoId", "message"],
44
+
"properties": {
45
+
"convoId": { "type": "string" },
46
+
"message": {
47
+
"type": "ref",
48
+
"ref": "chat.bsky.convo.defs#messageInput"
49
+
}
50
+
}
51
+
}
52
+
}
53
+
}
+32
lexicons/chat/bsky/convo/unmuteConvo.json
+32
lexicons/chat/bsky/convo/unmuteConvo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.unmuteConvo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" }
14
+
}
15
+
}
16
+
},
17
+
"output": {
18
+
"encoding": "application/json",
19
+
"schema": {
20
+
"type": "object",
21
+
"required": ["convo"],
22
+
"properties": {
23
+
"convo": {
24
+
"type": "ref",
25
+
"ref": "chat.bsky.convo.defs#convoView"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
+34
lexicons/chat/bsky/convo/updateAllRead.json
+34
lexicons/chat/bsky/convo/updateAllRead.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.updateAllRead",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"properties": {
12
+
"status": {
13
+
"type": "string",
14
+
"knownValues": ["request", "accepted"]
15
+
}
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["updatedCount"],
24
+
"properties": {
25
+
"updatedCount": {
26
+
"description": "The count of updated convos.",
27
+
"type": "integer"
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
+33
lexicons/chat/bsky/convo/updateRead.json
+33
lexicons/chat/bsky/convo/updateRead.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.convo.updateRead",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["convoId"],
12
+
"properties": {
13
+
"convoId": { "type": "string" },
14
+
"messageId": { "type": "string" }
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["convo"],
23
+
"properties": {
24
+
"convo": {
25
+
"type": "ref",
26
+
"ref": "chat.bsky.convo.defs#convoView"
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
+43
lexicons/chat/bsky/moderation/getActorMetadata.json
+43
lexicons/chat/bsky/moderation/getActorMetadata.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.moderation.getActorMetadata",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": ["actor"],
10
+
"properties": {
11
+
"actor": { "type": "string", "format": "did" }
12
+
}
13
+
},
14
+
"output": {
15
+
"encoding": "application/json",
16
+
"schema": {
17
+
"type": "object",
18
+
"required": ["day", "month", "all"],
19
+
"properties": {
20
+
"day": { "type": "ref", "ref": "#metadata" },
21
+
"month": { "type": "ref", "ref": "#metadata" },
22
+
"all": { "type": "ref", "ref": "#metadata" }
23
+
}
24
+
}
25
+
}
26
+
},
27
+
"metadata": {
28
+
"type": "object",
29
+
"required": [
30
+
"messagesSent",
31
+
"messagesReceived",
32
+
"convos",
33
+
"convosStarted"
34
+
],
35
+
"properties": {
36
+
"messagesSent": { "type": "integer" },
37
+
"messagesReceived": { "type": "integer" },
38
+
"convos": { "type": "integer" },
39
+
"convosStarted": { "type": "integer" }
40
+
}
41
+
}
42
+
}
43
+
}
+41
lexicons/chat/bsky/moderation/getMessageContext.json
+41
lexicons/chat/bsky/moderation/getMessageContext.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.moderation.getMessageContext",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"parameters": {
8
+
"type": "params",
9
+
"required": ["messageId"],
10
+
"properties": {
11
+
"convoId": {
12
+
"type": "string",
13
+
"description": "Conversation that the message is from. NOTE: this field will eventually be required."
14
+
},
15
+
"messageId": { "type": "string" },
16
+
"before": { "type": "integer", "default": 5 },
17
+
"after": { "type": "integer", "default": 5 }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["messages"],
25
+
"properties": {
26
+
"messages": {
27
+
"type": "array",
28
+
"items": {
29
+
"type": "union",
30
+
"refs": [
31
+
"chat.bsky.convo.defs#messageView",
32
+
"chat.bsky.convo.defs#deletedMessageView"
33
+
]
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
+21
lexicons/chat/bsky/moderation/updateActorAccess.json
+21
lexicons/chat/bsky/moderation/updateActorAccess.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "chat.bsky.moderation.updateActorAccess",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"input": {
8
+
"encoding": "application/json",
9
+
"schema": {
10
+
"type": "object",
11
+
"required": ["actor", "allowAccess"],
12
+
"properties": {
13
+
"actor": { "type": "string", "format": "did" },
14
+
"allowAccess": { "type": "boolean" },
15
+
"ref": { "type": "string" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+71
lexicons/com/atproto/admin/defs.json
+71
lexicons/com/atproto/admin/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.defs",
4
+
"defs": {
5
+
"statusAttr": {
6
+
"type": "object",
7
+
"required": ["applied"],
8
+
"properties": {
9
+
"applied": { "type": "boolean" },
10
+
"ref": { "type": "string" }
11
+
}
12
+
},
13
+
"accountView": {
14
+
"type": "object",
15
+
"required": ["did", "handle", "indexedAt"],
16
+
"properties": {
17
+
"did": { "type": "string", "format": "did" },
18
+
"handle": { "type": "string", "format": "handle" },
19
+
"email": { "type": "string" },
20
+
"relatedRecords": { "type": "array", "items": { "type": "unknown" } },
21
+
"indexedAt": { "type": "string", "format": "datetime" },
22
+
"invitedBy": {
23
+
"type": "ref",
24
+
"ref": "com.atproto.server.defs#inviteCode"
25
+
},
26
+
"invites": {
27
+
"type": "array",
28
+
"items": {
29
+
"type": "ref",
30
+
"ref": "com.atproto.server.defs#inviteCode"
31
+
}
32
+
},
33
+
"invitesDisabled": { "type": "boolean" },
34
+
"emailConfirmedAt": { "type": "string", "format": "datetime" },
35
+
"inviteNote": { "type": "string" },
36
+
"deactivatedAt": { "type": "string", "format": "datetime" },
37
+
"threatSignatures": {
38
+
"type": "array",
39
+
"items": {
40
+
"type": "ref",
41
+
"ref": "#threatSignature"
42
+
}
43
+
}
44
+
}
45
+
},
46
+
"repoRef": {
47
+
"type": "object",
48
+
"required": ["did"],
49
+
"properties": {
50
+
"did": { "type": "string", "format": "did" }
51
+
}
52
+
},
53
+
"repoBlobRef": {
54
+
"type": "object",
55
+
"required": ["did", "cid"],
56
+
"properties": {
57
+
"did": { "type": "string", "format": "did" },
58
+
"cid": { "type": "string", "format": "cid" },
59
+
"recordUri": { "type": "string", "format": "at-uri" }
60
+
}
61
+
},
62
+
"threatSignature": {
63
+
"type": "object",
64
+
"required": ["property", "value"],
65
+
"properties": {
66
+
"property": { "type": "string" },
67
+
"value": { "type": "string" }
68
+
}
69
+
}
70
+
}
71
+
}
+20
lexicons/com/atproto/admin/deleteAccount.json
+20
lexicons/com/atproto/admin/deleteAccount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.deleteAccount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete a user account as an administrator.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+24
lexicons/com/atproto/admin/disableAccountInvites.json
+24
lexicons/com/atproto/admin/disableAccountInvites.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.disableAccountInvites",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Disable an account from receiving new invite codes, but does not invalidate existing codes.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["account"],
13
+
"properties": {
14
+
"account": { "type": "string", "format": "did" },
15
+
"note": {
16
+
"type": "string",
17
+
"description": "Optional reason for disabled invites."
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+26
lexicons/com/atproto/admin/disableInviteCodes.json
+26
lexicons/com/atproto/admin/disableInviteCodes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.disableInviteCodes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Disable some set of codes and/or all codes associated with a set of users.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"codes": {
14
+
"type": "array",
15
+
"items": { "type": "string" }
16
+
},
17
+
"accounts": {
18
+
"type": "array",
19
+
"items": { "type": "string" }
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
+24
lexicons/com/atproto/admin/enableAccountInvites.json
+24
lexicons/com/atproto/admin/enableAccountInvites.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.enableAccountInvites",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Re-enable an account's ability to receive invite codes.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["account"],
13
+
"properties": {
14
+
"account": { "type": "string", "format": "did" },
15
+
"note": {
16
+
"type": "string",
17
+
"description": "Optional reason for enabled invites."
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+24
lexicons/com/atproto/admin/getAccountInfo.json
+24
lexicons/com/atproto/admin/getAccountInfo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.getAccountInfo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about an account.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": { "type": "string", "format": "did" }
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "ref",
19
+
"ref": "com.atproto.admin.defs#accountView"
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+36
lexicons/com/atproto/admin/getAccountInfos.json
+36
lexicons/com/atproto/admin/getAccountInfos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.getAccountInfos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about some accounts.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["dids"],
11
+
"properties": {
12
+
"dids": {
13
+
"type": "array",
14
+
"items": { "type": "string", "format": "did" }
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["infos"],
23
+
"properties": {
24
+
"infos": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "ref",
28
+
"ref": "com.atproto.admin.defs#accountView"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+44
lexicons/com/atproto/admin/getInviteCodes.json
+44
lexicons/com/atproto/admin/getInviteCodes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.getInviteCodes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get an admin view of invite codes.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"sort": {
12
+
"type": "string",
13
+
"knownValues": ["recent", "usage"],
14
+
"default": "recent"
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 500,
20
+
"default": 100
21
+
},
22
+
"cursor": { "type": "string" }
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["codes"],
30
+
"properties": {
31
+
"cursor": { "type": "string" },
32
+
"codes": {
33
+
"type": "array",
34
+
"items": {
35
+
"type": "ref",
36
+
"ref": "com.atproto.server.defs#inviteCode"
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
+43
lexicons/com/atproto/admin/getSubjectStatus.json
+43
lexicons/com/atproto/admin/getSubjectStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.getSubjectStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the service-specific admin status of a subject (account, record, or blob).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"did": { "type": "string", "format": "did" },
12
+
"uri": { "type": "string", "format": "at-uri" },
13
+
"blob": { "type": "string", "format": "cid" }
14
+
}
15
+
},
16
+
"output": {
17
+
"encoding": "application/json",
18
+
"schema": {
19
+
"type": "object",
20
+
"required": ["subject"],
21
+
"properties": {
22
+
"subject": {
23
+
"type": "union",
24
+
"refs": [
25
+
"com.atproto.admin.defs#repoRef",
26
+
"com.atproto.repo.strongRef",
27
+
"com.atproto.admin.defs#repoBlobRef"
28
+
]
29
+
},
30
+
"takedown": {
31
+
"type": "ref",
32
+
"ref": "com.atproto.admin.defs#statusAttr"
33
+
},
34
+
"deactivated": {
35
+
"type": "ref",
36
+
"ref": "com.atproto.admin.defs#statusAttr"
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+40
lexicons/com/atproto/admin/searchAccounts.json
+40
lexicons/com/atproto/admin/searchAccounts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.searchAccounts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get list of accounts that matches your search query.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"email": { "type": "string" },
12
+
"cursor": { "type": "string" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"minimum": 1,
16
+
"maximum": 100,
17
+
"default": 50
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["accounts"],
26
+
"properties": {
27
+
"cursor": { "type": "string" },
28
+
"accounts": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "ref",
32
+
"ref": "com.atproto.admin.defs#accountView"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
+37
lexicons/com/atproto/admin/sendEmail.json
+37
lexicons/com/atproto/admin/sendEmail.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.sendEmail",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Send email to a user's account email address.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["recipientDid", "content", "senderDid"],
13
+
"properties": {
14
+
"recipientDid": { "type": "string", "format": "did" },
15
+
"content": { "type": "string" },
16
+
"subject": { "type": "string" },
17
+
"senderDid": { "type": "string", "format": "did" },
18
+
"comment": {
19
+
"type": "string",
20
+
"description": "Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers"
21
+
}
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["sent"],
30
+
"properties": {
31
+
"sent": { "type": "boolean" }
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+25
lexicons/com/atproto/admin/updateAccountEmail.json
+25
lexicons/com/atproto/admin/updateAccountEmail.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.updateAccountEmail",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Administrative action to update an account's email.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["account", "email"],
13
+
"properties": {
14
+
"account": {
15
+
"type": "string",
16
+
"format": "at-identifier",
17
+
"description": "The handle or DID of the repo."
18
+
},
19
+
"email": { "type": "string" }
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
+21
lexicons/com/atproto/admin/updateAccountHandle.json
+21
lexicons/com/atproto/admin/updateAccountHandle.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.updateAccountHandle",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Administrative action to update an account's handle.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "handle"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"handle": { "type": "string", "format": "handle" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+21
lexicons/com/atproto/admin/updateAccountPassword.json
+21
lexicons/com/atproto/admin/updateAccountPassword.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.updateAccountPassword",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update the password for a user account as an administrator.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "password"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"password": { "type": "string" }
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+25
lexicons/com/atproto/admin/updateAccountSigningKey.json
+25
lexicons/com/atproto/admin/updateAccountSigningKey.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.updateAccountSigningKey",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Administrative action to update an account's signing key in their Did document.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "signingKey"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"signingKey": {
16
+
"type": "string",
17
+
"format": "did",
18
+
"description": "Did-key formatted public key"
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
+56
lexicons/com/atproto/admin/updateSubjectStatus.json
+56
lexicons/com/atproto/admin/updateSubjectStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.admin.updateSubjectStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update the service-specific admin status of a subject (account, record, or blob).",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["subject"],
13
+
"properties": {
14
+
"subject": {
15
+
"type": "union",
16
+
"refs": [
17
+
"com.atproto.admin.defs#repoRef",
18
+
"com.atproto.repo.strongRef",
19
+
"com.atproto.admin.defs#repoBlobRef"
20
+
]
21
+
},
22
+
"takedown": {
23
+
"type": "ref",
24
+
"ref": "com.atproto.admin.defs#statusAttr"
25
+
},
26
+
"deactivated": {
27
+
"type": "ref",
28
+
"ref": "com.atproto.admin.defs#statusAttr"
29
+
}
30
+
}
31
+
}
32
+
},
33
+
"output": {
34
+
"encoding": "application/json",
35
+
"schema": {
36
+
"type": "object",
37
+
"required": ["subject"],
38
+
"properties": {
39
+
"subject": {
40
+
"type": "union",
41
+
"refs": [
42
+
"com.atproto.admin.defs#repoRef",
43
+
"com.atproto.repo.strongRef",
44
+
"com.atproto.admin.defs#repoBlobRef"
45
+
]
46
+
},
47
+
"takedown": {
48
+
"type": "ref",
49
+
"ref": "com.atproto.admin.defs#statusAttr"
50
+
}
51
+
}
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
+22
lexicons/com/atproto/identity/defs.json
+22
lexicons/com/atproto/identity/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.defs",
4
+
"defs": {
5
+
"identityInfo": {
6
+
"type": "object",
7
+
"required": ["did", "handle", "didDoc"],
8
+
"properties": {
9
+
"did": { "type": "string", "format": "did" },
10
+
"handle": {
11
+
"type": "string",
12
+
"format": "handle",
13
+
"description": "The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document."
14
+
},
15
+
"didDoc": {
16
+
"type": "unknown",
17
+
"description": "The complete DID document for the identity."
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
+29
lexicons/com/atproto/identity/getRecommendedDidCredentials.json
+29
lexicons/com/atproto/identity/getRecommendedDidCredentials.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.getRecommendedDidCredentials",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Describe the credentials that should be included in the DID doc of an account that is migrating to this service.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"rotationKeys": {
14
+
"description": "Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.",
15
+
"type": "array",
16
+
"items": { "type": "string" }
17
+
},
18
+
"alsoKnownAs": {
19
+
"type": "array",
20
+
"items": { "type": "string" }
21
+
},
22
+
"verificationMethods": { "type": "unknown" },
23
+
"services": { "type": "unknown" }
24
+
}
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
+44
lexicons/com/atproto/identity/refreshIdentity.json
+44
lexicons/com/atproto/identity/refreshIdentity.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.refreshIdentity",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["identifier"],
13
+
"properties": {
14
+
"identifier": {
15
+
"type": "string",
16
+
"format": "at-identifier"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "ref",
25
+
"ref": "com.atproto.identity.defs#identityInfo"
26
+
}
27
+
},
28
+
"errors": [
29
+
{
30
+
"name": "HandleNotFound",
31
+
"description": "The resolution process confirmed that the handle does not resolve to any DID."
32
+
},
33
+
{
34
+
"name": "DidNotFound",
35
+
"description": "The DID resolution process confirmed that there is no current DID."
36
+
},
37
+
{
38
+
"name": "DidDeactivated",
39
+
"description": "The DID previously existed, but has been deactivated."
40
+
}
41
+
]
42
+
}
43
+
}
44
+
}
+10
lexicons/com/atproto/identity/requestPlcOperationSignature.json
+10
lexicons/com/atproto/identity/requestPlcOperationSignature.json
+44
lexicons/com/atproto/identity/resolveDid.json
+44
lexicons/com/atproto/identity/resolveDid.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.resolveDid",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Resolves DID to DID document. Does not bi-directionally verify handle.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "DID to resolve."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["didDoc"],
24
+
"properties": {
25
+
"didDoc": {
26
+
"type": "unknown",
27
+
"description": "The complete DID document for the identity."
28
+
}
29
+
}
30
+
}
31
+
},
32
+
"errors": [
33
+
{
34
+
"name": "DidNotFound",
35
+
"description": "The DID resolution process confirmed that there is no current DID."
36
+
},
37
+
{
38
+
"name": "DidDeactivated",
39
+
"description": "The DID previously existed, but has been deactivated."
40
+
}
41
+
]
42
+
}
43
+
}
44
+
}
+37
lexicons/com/atproto/identity/resolveHandle.json
+37
lexicons/com/atproto/identity/resolveHandle.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.resolveHandle",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["handle"],
11
+
"properties": {
12
+
"handle": {
13
+
"type": "string",
14
+
"format": "handle",
15
+
"description": "The handle to resolve."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["did"],
24
+
"properties": {
25
+
"did": { "type": "string", "format": "did" }
26
+
}
27
+
}
28
+
},
29
+
"errors": [
30
+
{
31
+
"name": "HandleNotFound",
32
+
"description": "The resolution process confirmed that the handle does not resolve to any DID."
33
+
}
34
+
]
35
+
}
36
+
}
37
+
}
+42
lexicons/com/atproto/identity/resolveIdentity.json
+42
lexicons/com/atproto/identity/resolveIdentity.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.resolveIdentity",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["identifier"],
11
+
"properties": {
12
+
"identifier": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "Handle or DID to resolve."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "ref",
23
+
"ref": "com.atproto.identity.defs#identityInfo"
24
+
}
25
+
},
26
+
"errors": [
27
+
{
28
+
"name": "HandleNotFound",
29
+
"description": "The resolution process confirmed that the handle does not resolve to any DID."
30
+
},
31
+
{
32
+
"name": "DidNotFound",
33
+
"description": "The DID resolution process confirmed that there is no current DID."
34
+
},
35
+
{
36
+
"name": "DidDeactivated",
37
+
"description": "The DID previously existed, but has been deactivated."
38
+
}
39
+
]
40
+
}
41
+
}
42
+
}
+45
lexicons/com/atproto/identity/signPlcOperation.json
+45
lexicons/com/atproto/identity/signPlcOperation.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.signPlcOperation",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Signs a PLC operation to update some value(s) in the requesting DID's document.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"token": {
14
+
"description": "A token received through com.atproto.identity.requestPlcOperationSignature",
15
+
"type": "string"
16
+
},
17
+
"rotationKeys": {
18
+
"type": "array",
19
+
"items": { "type": "string" }
20
+
},
21
+
"alsoKnownAs": {
22
+
"type": "array",
23
+
"items": { "type": "string" }
24
+
},
25
+
"verificationMethods": { "type": "unknown" },
26
+
"services": { "type": "unknown" }
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"required": ["operation"],
35
+
"properties": {
36
+
"operation": {
37
+
"type": "unknown",
38
+
"description": "A signed DID PLC operation."
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+20
lexicons/com/atproto/identity/submitPlcOperation.json
+20
lexicons/com/atproto/identity/submitPlcOperation.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.submitPlcOperation",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["operation"],
13
+
"properties": {
14
+
"operation": { "type": "unknown" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+24
lexicons/com/atproto/identity/updateHandle.json
+24
lexicons/com/atproto/identity/updateHandle.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.identity.updateHandle",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["handle"],
13
+
"properties": {
14
+
"handle": {
15
+
"type": "string",
16
+
"format": "handle",
17
+
"description": "The new handle."
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+156
lexicons/com/atproto/label/defs.json
+156
lexicons/com/atproto/label/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.defs",
4
+
"defs": {
5
+
"label": {
6
+
"type": "object",
7
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
8
+
"required": ["src", "uri", "val", "cts"],
9
+
"properties": {
10
+
"ver": {
11
+
"type": "integer",
12
+
"description": "The AT Protocol version of the label object."
13
+
},
14
+
"src": {
15
+
"type": "string",
16
+
"format": "did",
17
+
"description": "DID of the actor who created this label."
18
+
},
19
+
"uri": {
20
+
"type": "string",
21
+
"format": "uri",
22
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
23
+
},
24
+
"cid": {
25
+
"type": "string",
26
+
"format": "cid",
27
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
28
+
},
29
+
"val": {
30
+
"type": "string",
31
+
"maxLength": 128,
32
+
"description": "The short string name of the value or type of this label."
33
+
},
34
+
"neg": {
35
+
"type": "boolean",
36
+
"description": "If true, this is a negation label, overwriting a previous label."
37
+
},
38
+
"cts": {
39
+
"type": "string",
40
+
"format": "datetime",
41
+
"description": "Timestamp when this label was created."
42
+
},
43
+
"exp": {
44
+
"type": "string",
45
+
"format": "datetime",
46
+
"description": "Timestamp at which this label expires (no longer applies)."
47
+
},
48
+
"sig": {
49
+
"type": "bytes",
50
+
"description": "Signature of dag-cbor encoded label."
51
+
}
52
+
}
53
+
},
54
+
"selfLabels": {
55
+
"type": "object",
56
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
57
+
"required": ["values"],
58
+
"properties": {
59
+
"values": {
60
+
"type": "array",
61
+
"items": { "type": "ref", "ref": "#selfLabel" },
62
+
"maxLength": 10
63
+
}
64
+
}
65
+
},
66
+
"selfLabel": {
67
+
"type": "object",
68
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
69
+
"required": ["val"],
70
+
"properties": {
71
+
"val": {
72
+
"type": "string",
73
+
"maxLength": 128,
74
+
"description": "The short string name of the value or type of this label."
75
+
}
76
+
}
77
+
},
78
+
"labelValueDefinition": {
79
+
"type": "object",
80
+
"description": "Declares a label value and its expected interpretations and behaviors.",
81
+
"required": ["identifier", "severity", "blurs", "locales"],
82
+
"properties": {
83
+
"identifier": {
84
+
"type": "string",
85
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
86
+
"maxLength": 100,
87
+
"maxGraphemes": 100
88
+
},
89
+
"severity": {
90
+
"type": "string",
91
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
92
+
"knownValues": ["inform", "alert", "none"]
93
+
},
94
+
"blurs": {
95
+
"type": "string",
96
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
97
+
"knownValues": ["content", "media", "none"]
98
+
},
99
+
"defaultSetting": {
100
+
"type": "string",
101
+
"description": "The default setting for this label.",
102
+
"knownValues": ["ignore", "warn", "hide"],
103
+
"default": "warn"
104
+
},
105
+
"adultOnly": {
106
+
"type": "boolean",
107
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
108
+
},
109
+
"locales": {
110
+
"type": "array",
111
+
"items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
112
+
}
113
+
}
114
+
},
115
+
"labelValueDefinitionStrings": {
116
+
"type": "object",
117
+
"description": "Strings which describe the label in the UI, localized into a specific language.",
118
+
"required": ["lang", "name", "description"],
119
+
"properties": {
120
+
"lang": {
121
+
"type": "string",
122
+
"description": "The code of the language these strings are written in.",
123
+
"format": "language"
124
+
},
125
+
"name": {
126
+
"type": "string",
127
+
"description": "A short human-readable name for the label.",
128
+
"maxGraphemes": 64,
129
+
"maxLength": 640
130
+
},
131
+
"description": {
132
+
"type": "string",
133
+
"description": "A longer description of what the label means and why it might be applied.",
134
+
"maxGraphemes": 10000,
135
+
"maxLength": 100000
136
+
}
137
+
}
138
+
},
139
+
"labelValue": {
140
+
"type": "string",
141
+
"knownValues": [
142
+
"!hide",
143
+
"!no-promote",
144
+
"!warn",
145
+
"!no-unauthenticated",
146
+
"dmca-violation",
147
+
"doxxing",
148
+
"porn",
149
+
"sexual",
150
+
"nudity",
151
+
"nsfl",
152
+
"gore"
153
+
]
154
+
}
155
+
}
156
+
}
+47
lexicons/com/atproto/label/queryLabels.json
+47
lexicons/com/atproto/label/queryLabels.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.queryLabels",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uriPatterns"],
11
+
"properties": {
12
+
"uriPatterns": {
13
+
"type": "array",
14
+
"items": { "type": "string" },
15
+
"description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI."
16
+
},
17
+
"sources": {
18
+
"type": "array",
19
+
"items": { "type": "string", "format": "did" },
20
+
"description": "Optional list of label sources (DIDs) to filter on."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 250,
26
+
"default": 50
27
+
},
28
+
"cursor": { "type": "string" }
29
+
}
30
+
},
31
+
"output": {
32
+
"encoding": "application/json",
33
+
"schema": {
34
+
"type": "object",
35
+
"required": ["labels"],
36
+
"properties": {
37
+
"cursor": { "type": "string" },
38
+
"labels": {
39
+
"type": "array",
40
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
47
+
}
+50
lexicons/com/atproto/label/subscribeLabels.json
+50
lexicons/com/atproto/label/subscribeLabels.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.subscribeLabels",
4
+
"defs": {
5
+
"main": {
6
+
"type": "subscription",
7
+
"description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"cursor": {
12
+
"type": "integer",
13
+
"description": "The last known event seq number to backfill from."
14
+
}
15
+
}
16
+
},
17
+
"message": {
18
+
"schema": {
19
+
"type": "union",
20
+
"refs": ["#labels", "#info"]
21
+
}
22
+
},
23
+
"errors": [{ "name": "FutureCursor" }]
24
+
},
25
+
"labels": {
26
+
"type": "object",
27
+
"required": ["seq", "labels"],
28
+
"properties": {
29
+
"seq": { "type": "integer" },
30
+
"labels": {
31
+
"type": "array",
32
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
33
+
}
34
+
}
35
+
},
36
+
"info": {
37
+
"type": "object",
38
+
"required": ["name"],
39
+
"properties": {
40
+
"name": {
41
+
"type": "string",
42
+
"knownValues": ["OutdatedCursor"]
43
+
},
44
+
"message": {
45
+
"type": "string"
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
+51
lexicons/com/atproto/lexicon/resolveLexicon.json
+51
lexicons/com/atproto/lexicon/resolveLexicon.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.lexicon.resolveLexicon",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Resolves an atproto lexicon (NSID) to a schema.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"nsid": {
12
+
"format": "nsid",
13
+
"type": "string",
14
+
"description": "The lexicon NSID to resolve."
15
+
}
16
+
},
17
+
"required": ["nsid"]
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"properties": {
24
+
"cid": {
25
+
"type": "string",
26
+
"format": "cid",
27
+
"description": "The CID of the lexicon schema record."
28
+
},
29
+
"schema": {
30
+
"type": "ref",
31
+
"ref": "com.atproto.lexicon.schema#main",
32
+
"description": "The resolved lexicon schema record."
33
+
},
34
+
"uri": {
35
+
"type": "string",
36
+
"format": "at-uri",
37
+
"description": "The AT-URI of the lexicon schema record."
38
+
}
39
+
},
40
+
"required": ["uri", "cid", "schema"]
41
+
}
42
+
},
43
+
"errors": [
44
+
{
45
+
"description": "No lexicon was resolved for the NSID.",
46
+
"name": "LexiconNotFound"
47
+
}
48
+
]
49
+
}
50
+
}
51
+
}
+21
lexicons/com/atproto/lexicon/schema.json
+21
lexicons/com/atproto/lexicon/schema.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.lexicon.schema",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).",
8
+
"key": "nsid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["lexicon"],
12
+
"properties": {
13
+
"lexicon": {
14
+
"type": "integer",
15
+
"description": "Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system."
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+90
lexicons/com/atproto/moderation/createReport.json
+90
lexicons/com/atproto/moderation/createReport.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.moderation.createReport",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["reasonType", "subject"],
13
+
"properties": {
14
+
"reasonType": {
15
+
"type": "ref",
16
+
"description": "Indicates the broad category of violation the report is for.",
17
+
"ref": "com.atproto.moderation.defs#reasonType"
18
+
},
19
+
"reason": {
20
+
"type": "string",
21
+
"maxGraphemes": 2000,
22
+
"maxLength": 20000,
23
+
"description": "Additional context about the content and violation."
24
+
},
25
+
"subject": {
26
+
"type": "union",
27
+
"refs": [
28
+
"com.atproto.admin.defs#repoRef",
29
+
"com.atproto.repo.strongRef"
30
+
]
31
+
},
32
+
"modTool": {
33
+
"type": "ref",
34
+
"ref": "#modTool"
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"output": {
40
+
"encoding": "application/json",
41
+
"schema": {
42
+
"type": "object",
43
+
"required": [
44
+
"id",
45
+
"reasonType",
46
+
"subject",
47
+
"reportedBy",
48
+
"createdAt"
49
+
],
50
+
"properties": {
51
+
"id": { "type": "integer" },
52
+
"reasonType": {
53
+
"type": "ref",
54
+
"ref": "com.atproto.moderation.defs#reasonType"
55
+
},
56
+
"reason": {
57
+
"type": "string",
58
+
"maxGraphemes": 2000,
59
+
"maxLength": 20000
60
+
},
61
+
"subject": {
62
+
"type": "union",
63
+
"refs": [
64
+
"com.atproto.admin.defs#repoRef",
65
+
"com.atproto.repo.strongRef"
66
+
]
67
+
},
68
+
"reportedBy": { "type": "string", "format": "did" },
69
+
"createdAt": { "type": "string", "format": "datetime" }
70
+
}
71
+
}
72
+
}
73
+
},
74
+
"modTool": {
75
+
"type": "object",
76
+
"description": "Moderation tool information for tracing the source of the action",
77
+
"required": ["name"],
78
+
"properties": {
79
+
"name": {
80
+
"type": "string",
81
+
"description": "Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')"
82
+
},
83
+
"meta": {
84
+
"type": "unknown",
85
+
"description": "Additional arbitrary metadata about the source"
86
+
}
87
+
}
88
+
}
89
+
}
90
+
}
+99
lexicons/com/atproto/moderation/defs.json
+99
lexicons/com/atproto/moderation/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.moderation.defs",
4
+
"defs": {
5
+
"reasonType": {
6
+
"type": "string",
7
+
"knownValues": [
8
+
"com.atproto.moderation.defs#reasonSpam",
9
+
"com.atproto.moderation.defs#reasonViolation",
10
+
"com.atproto.moderation.defs#reasonMisleading",
11
+
"com.atproto.moderation.defs#reasonSexual",
12
+
"com.atproto.moderation.defs#reasonRude",
13
+
"com.atproto.moderation.defs#reasonOther",
14
+
"com.atproto.moderation.defs#reasonAppeal",
15
+
16
+
"tools.ozone.report.defs#reasonAppeal",
17
+
"tools.ozone.report.defs#reasonOther",
18
+
19
+
"tools.ozone.report.defs#reasonViolenceAnimal",
20
+
"tools.ozone.report.defs#reasonViolenceThreats",
21
+
"tools.ozone.report.defs#reasonViolenceGraphicContent",
22
+
"tools.ozone.report.defs#reasonViolenceGlorification",
23
+
"tools.ozone.report.defs#reasonViolenceExtremistContent",
24
+
"tools.ozone.report.defs#reasonViolenceTrafficking",
25
+
"tools.ozone.report.defs#reasonViolenceOther",
26
+
27
+
"tools.ozone.report.defs#reasonSexualAbuseContent",
28
+
"tools.ozone.report.defs#reasonSexualNCII",
29
+
"tools.ozone.report.defs#reasonSexualDeepfake",
30
+
"tools.ozone.report.defs#reasonSexualAnimal",
31
+
"tools.ozone.report.defs#reasonSexualUnlabeled",
32
+
"tools.ozone.report.defs#reasonSexualOther",
33
+
34
+
"tools.ozone.report.defs#reasonChildSafetyCSAM",
35
+
"tools.ozone.report.defs#reasonChildSafetyGroom",
36
+
"tools.ozone.report.defs#reasonChildSafetyPrivacy",
37
+
"tools.ozone.report.defs#reasonChildSafetyHarassment",
38
+
"tools.ozone.report.defs#reasonChildSafetyOther",
39
+
40
+
"tools.ozone.report.defs#reasonHarassmentTroll",
41
+
"tools.ozone.report.defs#reasonHarassmentTargeted",
42
+
"tools.ozone.report.defs#reasonHarassmentHateSpeech",
43
+
"tools.ozone.report.defs#reasonHarassmentDoxxing",
44
+
"tools.ozone.report.defs#reasonHarassmentOther",
45
+
46
+
"tools.ozone.report.defs#reasonMisleadingBot",
47
+
"tools.ozone.report.defs#reasonMisleadingImpersonation",
48
+
"tools.ozone.report.defs#reasonMisleadingSpam",
49
+
"tools.ozone.report.defs#reasonMisleadingScam",
50
+
"tools.ozone.report.defs#reasonMisleadingElections",
51
+
"tools.ozone.report.defs#reasonMisleadingOther",
52
+
53
+
"tools.ozone.report.defs#reasonRuleSiteSecurity",
54
+
"tools.ozone.report.defs#reasonRuleProhibitedSales",
55
+
"tools.ozone.report.defs#reasonRuleBanEvasion",
56
+
"tools.ozone.report.defs#reasonRuleOther",
57
+
58
+
"tools.ozone.report.defs#reasonSelfHarmContent",
59
+
"tools.ozone.report.defs#reasonSelfHarmED",
60
+
"tools.ozone.report.defs#reasonSelfHarmStunts",
61
+
"tools.ozone.report.defs#reasonSelfHarmSubstances",
62
+
"tools.ozone.report.defs#reasonSelfHarmOther"
63
+
]
64
+
},
65
+
"reasonSpam": {
66
+
"type": "token",
67
+
"description": "Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`."
68
+
},
69
+
"reasonViolation": {
70
+
"type": "token",
71
+
"description": "Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`."
72
+
},
73
+
"reasonMisleading": {
74
+
"type": "token",
75
+
"description": "Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`."
76
+
},
77
+
"reasonSexual": {
78
+
"type": "token",
79
+
"description": "Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`."
80
+
},
81
+
"reasonRude": {
82
+
"type": "token",
83
+
"description": "Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`."
84
+
},
85
+
"reasonOther": {
86
+
"type": "token",
87
+
"description": "Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`."
88
+
},
89
+
"reasonAppeal": {
90
+
"type": "token",
91
+
"description": "Appeal a previously taken moderation action"
92
+
},
93
+
"subjectType": {
94
+
"type": "string",
95
+
"description": "Tag describing a type of subject that might be reported.",
96
+
"knownValues": ["account", "record", "chat"]
97
+
}
98
+
}
99
+
}
+131
lexicons/com/atproto/repo/applyWrites.json
+131
lexicons/com/atproto/repo/applyWrites.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.applyWrites",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["repo", "writes"],
13
+
"properties": {
14
+
"repo": {
15
+
"type": "string",
16
+
"format": "at-identifier",
17
+
"description": "The handle or DID of the repo (aka, current account)."
18
+
},
19
+
"validate": {
20
+
"type": "boolean",
21
+
"description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons."
22
+
},
23
+
"writes": {
24
+
"type": "array",
25
+
"items": {
26
+
"type": "union",
27
+
"refs": ["#create", "#update", "#delete"],
28
+
"closed": true
29
+
}
30
+
},
31
+
"swapCommit": {
32
+
"type": "string",
33
+
"description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.",
34
+
"format": "cid"
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"output": {
40
+
"encoding": "application/json",
41
+
"schema": {
42
+
"type": "object",
43
+
"required": [],
44
+
"properties": {
45
+
"commit": {
46
+
"type": "ref",
47
+
"ref": "com.atproto.repo.defs#commitMeta"
48
+
},
49
+
"results": {
50
+
"type": "array",
51
+
"items": {
52
+
"type": "union",
53
+
"refs": ["#createResult", "#updateResult", "#deleteResult"],
54
+
"closed": true
55
+
}
56
+
}
57
+
}
58
+
}
59
+
},
60
+
"errors": [
61
+
{
62
+
"name": "InvalidSwap",
63
+
"description": "Indicates that the 'swapCommit' parameter did not match current commit."
64
+
}
65
+
]
66
+
},
67
+
"create": {
68
+
"type": "object",
69
+
"description": "Operation which creates a new record.",
70
+
"required": ["collection", "value"],
71
+
"properties": {
72
+
"collection": { "type": "string", "format": "nsid" },
73
+
"rkey": {
74
+
"type": "string",
75
+
"maxLength": 512,
76
+
"format": "record-key",
77
+
"description": "NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility."
78
+
},
79
+
"value": { "type": "unknown" }
80
+
}
81
+
},
82
+
"update": {
83
+
"type": "object",
84
+
"description": "Operation which updates an existing record.",
85
+
"required": ["collection", "rkey", "value"],
86
+
"properties": {
87
+
"collection": { "type": "string", "format": "nsid" },
88
+
"rkey": { "type": "string", "format": "record-key" },
89
+
"value": { "type": "unknown" }
90
+
}
91
+
},
92
+
"delete": {
93
+
"type": "object",
94
+
"description": "Operation which deletes an existing record.",
95
+
"required": ["collection", "rkey"],
96
+
"properties": {
97
+
"collection": { "type": "string", "format": "nsid" },
98
+
"rkey": { "type": "string", "format": "record-key" }
99
+
}
100
+
},
101
+
"createResult": {
102
+
"type": "object",
103
+
"required": ["uri", "cid"],
104
+
"properties": {
105
+
"uri": { "type": "string", "format": "at-uri" },
106
+
"cid": { "type": "string", "format": "cid" },
107
+
"validationStatus": {
108
+
"type": "string",
109
+
"knownValues": ["valid", "unknown"]
110
+
}
111
+
}
112
+
},
113
+
"updateResult": {
114
+
"type": "object",
115
+
"required": ["uri", "cid"],
116
+
"properties": {
117
+
"uri": { "type": "string", "format": "at-uri" },
118
+
"cid": { "type": "string", "format": "cid" },
119
+
"validationStatus": {
120
+
"type": "string",
121
+
"knownValues": ["valid", "unknown"]
122
+
}
123
+
}
124
+
},
125
+
"deleteResult": {
126
+
"type": "object",
127
+
"required": [],
128
+
"properties": {}
129
+
}
130
+
}
131
+
}
+73
lexicons/com/atproto/repo/createRecord.json
+73
lexicons/com/atproto/repo/createRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.createRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create a single new repository record. Requires auth, implemented by PDS.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["repo", "collection", "record"],
13
+
"properties": {
14
+
"repo": {
15
+
"type": "string",
16
+
"format": "at-identifier",
17
+
"description": "The handle or DID of the repo (aka, current account)."
18
+
},
19
+
"collection": {
20
+
"type": "string",
21
+
"format": "nsid",
22
+
"description": "The NSID of the record collection."
23
+
},
24
+
"rkey": {
25
+
"type": "string",
26
+
"format": "record-key",
27
+
"description": "The Record Key.",
28
+
"maxLength": 512
29
+
},
30
+
"validate": {
31
+
"type": "boolean",
32
+
"description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
33
+
},
34
+
"record": {
35
+
"type": "unknown",
36
+
"description": "The record itself. Must contain a $type field."
37
+
},
38
+
"swapCommit": {
39
+
"type": "string",
40
+
"format": "cid",
41
+
"description": "Compare and swap with the previous commit by CID."
42
+
}
43
+
}
44
+
}
45
+
},
46
+
"output": {
47
+
"encoding": "application/json",
48
+
"schema": {
49
+
"type": "object",
50
+
"required": ["uri", "cid"],
51
+
"properties": {
52
+
"uri": { "type": "string", "format": "at-uri" },
53
+
"cid": { "type": "string", "format": "cid" },
54
+
"commit": {
55
+
"type": "ref",
56
+
"ref": "com.atproto.repo.defs#commitMeta"
57
+
},
58
+
"validationStatus": {
59
+
"type": "string",
60
+
"knownValues": ["valid", "unknown"]
61
+
}
62
+
}
63
+
}
64
+
},
65
+
"errors": [
66
+
{
67
+
"name": "InvalidSwap",
68
+
"description": "Indicates that 'swapCommit' didn't match current repo commit."
69
+
}
70
+
]
71
+
}
72
+
}
73
+
}
+14
lexicons/com/atproto/repo/defs.json
+14
lexicons/com/atproto/repo/defs.json
+57
lexicons/com/atproto/repo/deleteRecord.json
+57
lexicons/com/atproto/repo/deleteRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.deleteRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["repo", "collection", "rkey"],
13
+
"properties": {
14
+
"repo": {
15
+
"type": "string",
16
+
"format": "at-identifier",
17
+
"description": "The handle or DID of the repo (aka, current account)."
18
+
},
19
+
"collection": {
20
+
"type": "string",
21
+
"format": "nsid",
22
+
"description": "The NSID of the record collection."
23
+
},
24
+
"rkey": {
25
+
"type": "string",
26
+
"format": "record-key",
27
+
"description": "The Record Key."
28
+
},
29
+
"swapRecord": {
30
+
"type": "string",
31
+
"format": "cid",
32
+
"description": "Compare and swap with the previous record by CID."
33
+
},
34
+
"swapCommit": {
35
+
"type": "string",
36
+
"format": "cid",
37
+
"description": "Compare and swap with the previous commit by CID."
38
+
}
39
+
}
40
+
}
41
+
},
42
+
"output": {
43
+
"encoding": "application/json",
44
+
"schema": {
45
+
"type": "object",
46
+
"properties": {
47
+
"commit": {
48
+
"type": "ref",
49
+
"ref": "com.atproto.repo.defs#commitMeta"
50
+
}
51
+
}
52
+
}
53
+
},
54
+
"errors": [{ "name": "InvalidSwap" }]
55
+
}
56
+
}
57
+
}
+51
lexicons/com/atproto/repo/describeRepo.json
+51
lexicons/com/atproto/repo/describeRepo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.describeRepo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about an account and repository, including the list of collections. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["repo"],
11
+
"properties": {
12
+
"repo": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The handle or DID of the repo."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": [
24
+
"handle",
25
+
"did",
26
+
"didDoc",
27
+
"collections",
28
+
"handleIsCorrect"
29
+
],
30
+
"properties": {
31
+
"handle": { "type": "string", "format": "handle" },
32
+
"did": { "type": "string", "format": "did" },
33
+
"didDoc": {
34
+
"type": "unknown",
35
+
"description": "The complete DID document for this account."
36
+
},
37
+
"collections": {
38
+
"type": "array",
39
+
"description": "List of all the collections (NSIDs) for which this repo contains at least one record.",
40
+
"items": { "type": "string", "format": "nsid" }
41
+
},
42
+
"handleIsCorrect": {
43
+
"type": "boolean",
44
+
"description": "Indicates if handle is currently valid (resolves bi-directionally)"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
+49
lexicons/com/atproto/repo/getRecord.json
+49
lexicons/com/atproto/repo/getRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.getRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a single record from a repository. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["repo", "collection", "rkey"],
11
+
"properties": {
12
+
"repo": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The handle or DID of the repo."
16
+
},
17
+
"collection": {
18
+
"type": "string",
19
+
"format": "nsid",
20
+
"description": "The NSID of the record collection."
21
+
},
22
+
"rkey": {
23
+
"type": "string",
24
+
"description": "The Record Key.",
25
+
"format": "record-key"
26
+
},
27
+
"cid": {
28
+
"type": "string",
29
+
"format": "cid",
30
+
"description": "The CID of the version of the record. If not specified, then return the most recent version."
31
+
}
32
+
}
33
+
},
34
+
"output": {
35
+
"encoding": "application/json",
36
+
"schema": {
37
+
"type": "object",
38
+
"required": ["uri", "value"],
39
+
"properties": {
40
+
"uri": { "type": "string", "format": "at-uri" },
41
+
"cid": { "type": "string", "format": "cid" },
42
+
"value": { "type": "unknown" }
43
+
}
44
+
}
45
+
},
46
+
"errors": [{ "name": "RecordNotFound" }]
47
+
}
48
+
}
49
+
}
+13
lexicons/com/atproto/repo/importRepo.json
+13
lexicons/com/atproto/repo/importRepo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.importRepo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.",
8
+
"input": {
9
+
"encoding": "application/vnd.ipld.car"
10
+
}
11
+
}
12
+
}
13
+
}
+44
lexicons/com/atproto/repo/listMissingBlobs.json
+44
lexicons/com/atproto/repo/listMissingBlobs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.listMissingBlobs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 1000,
15
+
"default": 500
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["blobs"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"blobs": {
28
+
"type": "array",
29
+
"items": { "type": "ref", "ref": "#recordBlob" }
30
+
}
31
+
}
32
+
}
33
+
}
34
+
},
35
+
"recordBlob": {
36
+
"type": "object",
37
+
"required": ["cid", "recordUri"],
38
+
"properties": {
39
+
"cid": { "type": "string", "format": "cid" },
40
+
"recordUri": { "type": "string", "format": "at-uri" }
41
+
}
42
+
}
43
+
}
44
+
}
+61
lexicons/com/atproto/repo/listRecords.json
+61
lexicons/com/atproto/repo/listRecords.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.listRecords",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List a range of records in a repository, matching a specific collection. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["repo", "collection"],
11
+
"properties": {
12
+
"repo": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The handle or DID of the repo."
16
+
},
17
+
"collection": {
18
+
"type": "string",
19
+
"format": "nsid",
20
+
"description": "The NSID of the record type."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 100,
26
+
"default": 50,
27
+
"description": "The number of records to return."
28
+
},
29
+
"cursor": { "type": "string" },
30
+
"reverse": {
31
+
"type": "boolean",
32
+
"description": "Flag to reverse the order of the returned records."
33
+
}
34
+
}
35
+
},
36
+
"output": {
37
+
"encoding": "application/json",
38
+
"schema": {
39
+
"type": "object",
40
+
"required": ["records"],
41
+
"properties": {
42
+
"cursor": { "type": "string" },
43
+
"records": {
44
+
"type": "array",
45
+
"items": { "type": "ref", "ref": "#record" }
46
+
}
47
+
}
48
+
}
49
+
}
50
+
},
51
+
"record": {
52
+
"type": "object",
53
+
"required": ["uri", "cid", "value"],
54
+
"properties": {
55
+
"uri": { "type": "string", "format": "at-uri" },
56
+
"cid": { "type": "string", "format": "cid" },
57
+
"value": { "type": "unknown" }
58
+
}
59
+
}
60
+
}
61
+
}
+74
lexicons/com/atproto/repo/putRecord.json
+74
lexicons/com/atproto/repo/putRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.putRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["repo", "collection", "rkey", "record"],
13
+
"nullable": ["swapRecord"],
14
+
"properties": {
15
+
"repo": {
16
+
"type": "string",
17
+
"format": "at-identifier",
18
+
"description": "The handle or DID of the repo (aka, current account)."
19
+
},
20
+
"collection": {
21
+
"type": "string",
22
+
"format": "nsid",
23
+
"description": "The NSID of the record collection."
24
+
},
25
+
"rkey": {
26
+
"type": "string",
27
+
"format": "record-key",
28
+
"description": "The Record Key.",
29
+
"maxLength": 512
30
+
},
31
+
"validate": {
32
+
"type": "boolean",
33
+
"description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
34
+
},
35
+
"record": {
36
+
"type": "unknown",
37
+
"description": "The record to write."
38
+
},
39
+
"swapRecord": {
40
+
"type": "string",
41
+
"format": "cid",
42
+
"description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation"
43
+
},
44
+
"swapCommit": {
45
+
"type": "string",
46
+
"format": "cid",
47
+
"description": "Compare and swap with the previous commit by CID."
48
+
}
49
+
}
50
+
}
51
+
},
52
+
"output": {
53
+
"encoding": "application/json",
54
+
"schema": {
55
+
"type": "object",
56
+
"required": ["uri", "cid"],
57
+
"properties": {
58
+
"uri": { "type": "string", "format": "at-uri" },
59
+
"cid": { "type": "string", "format": "cid" },
60
+
"commit": {
61
+
"type": "ref",
62
+
"ref": "com.atproto.repo.defs#commitMeta"
63
+
},
64
+
"validationStatus": {
65
+
"type": "string",
66
+
"knownValues": ["valid", "unknown"]
67
+
}
68
+
}
69
+
}
70
+
},
71
+
"errors": [{ "name": "InvalidSwap" }]
72
+
}
73
+
}
74
+
}
+15
lexicons/com/atproto/repo/strongRef.json
+15
lexicons/com/atproto/repo/strongRef.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.strongRef",
4
+
"description": "A URI with a content-hash fingerprint.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["uri", "cid"],
9
+
"properties": {
10
+
"uri": { "type": "string", "format": "at-uri" },
11
+
"cid": { "type": "string", "format": "cid" }
12
+
}
13
+
}
14
+
}
15
+
}
+23
lexicons/com/atproto/repo/uploadBlob.json
+23
lexicons/com/atproto/repo/uploadBlob.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.uploadBlob",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.",
8
+
"input": {
9
+
"encoding": "*/*"
10
+
},
11
+
"output": {
12
+
"encoding": "application/json",
13
+
"schema": {
14
+
"type": "object",
15
+
"required": ["blob"],
16
+
"properties": {
17
+
"blob": { "type": "blob" }
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+10
lexicons/com/atproto/server/activateAccount.json
+10
lexicons/com/atproto/server/activateAccount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.activateAccount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup."
8
+
}
9
+
}
10
+
}
+38
lexicons/com/atproto/server/checkAccountStatus.json
+38
lexicons/com/atproto/server/checkAccountStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.checkAccountStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": [
13
+
"activated",
14
+
"validDid",
15
+
"repoCommit",
16
+
"repoRev",
17
+
"repoBlocks",
18
+
"indexedRecords",
19
+
"privateStateValues",
20
+
"expectedBlobs",
21
+
"importedBlobs"
22
+
],
23
+
"properties": {
24
+
"activated": { "type": "boolean" },
25
+
"validDid": { "type": "boolean" },
26
+
"repoCommit": { "type": "string", "format": "cid" },
27
+
"repoRev": { "type": "string" },
28
+
"repoBlocks": { "type": "integer" },
29
+
"indexedRecords": { "type": "integer" },
30
+
"privateStateValues": { "type": "integer" },
31
+
"expectedBlobs": { "type": "integer" },
32
+
"importedBlobs": { "type": "integer" }
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
+27
lexicons/com/atproto/server/confirmEmail.json
+27
lexicons/com/atproto/server/confirmEmail.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.confirmEmail",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Confirm an email using a token from com.atproto.server.requestEmailConfirmation.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["email", "token"],
13
+
"properties": {
14
+
"email": { "type": "string" },
15
+
"token": { "type": "string" }
16
+
}
17
+
}
18
+
},
19
+
"errors": [
20
+
{ "name": "AccountNotFound" },
21
+
{ "name": "ExpiredToken" },
22
+
{ "name": "InvalidToken" },
23
+
{ "name": "InvalidEmail" }
24
+
]
25
+
}
26
+
}
27
+
}
+76
lexicons/com/atproto/server/createAccount.json
+76
lexicons/com/atproto/server/createAccount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.createAccount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create an account. Implemented by PDS.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["handle"],
13
+
"properties": {
14
+
"email": { "type": "string" },
15
+
"handle": {
16
+
"type": "string",
17
+
"format": "handle",
18
+
"description": "Requested handle for the account."
19
+
},
20
+
"did": {
21
+
"type": "string",
22
+
"format": "did",
23
+
"description": "Pre-existing atproto DID, being imported to a new account."
24
+
},
25
+
"inviteCode": { "type": "string" },
26
+
"verificationCode": { "type": "string" },
27
+
"verificationPhone": { "type": "string" },
28
+
"password": {
29
+
"type": "string",
30
+
"description": "Initial account password. May need to meet instance-specific password strength requirements."
31
+
},
32
+
"recoveryKey": {
33
+
"type": "string",
34
+
"description": "DID PLC rotation key (aka, recovery key) to be included in PLC creation operation."
35
+
},
36
+
"plcOp": {
37
+
"type": "unknown",
38
+
"description": "A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented."
39
+
}
40
+
}
41
+
}
42
+
},
43
+
"output": {
44
+
"encoding": "application/json",
45
+
"schema": {
46
+
"type": "object",
47
+
"description": "Account login session returned on successful account creation.",
48
+
"required": ["accessJwt", "refreshJwt", "handle", "did"],
49
+
"properties": {
50
+
"accessJwt": { "type": "string" },
51
+
"refreshJwt": { "type": "string" },
52
+
"handle": { "type": "string", "format": "handle" },
53
+
"did": {
54
+
"type": "string",
55
+
"format": "did",
56
+
"description": "The DID of the new account."
57
+
},
58
+
"didDoc": {
59
+
"type": "unknown",
60
+
"description": "Complete DID document."
61
+
}
62
+
}
63
+
}
64
+
},
65
+
"errors": [
66
+
{ "name": "InvalidHandle" },
67
+
{ "name": "InvalidPassword" },
68
+
{ "name": "InvalidInviteCode" },
69
+
{ "name": "HandleNotAvailable" },
70
+
{ "name": "UnsupportedDomain" },
71
+
{ "name": "UnresolvableDid" },
72
+
{ "name": "IncompatibleDidDoc" }
73
+
]
74
+
}
75
+
}
76
+
}
+45
lexicons/com/atproto/server/createAppPassword.json
+45
lexicons/com/atproto/server/createAppPassword.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.createAppPassword",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create an App Password.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["name"],
13
+
"properties": {
14
+
"name": {
15
+
"type": "string",
16
+
"description": "A short name for the App Password, to help distinguish them."
17
+
},
18
+
"privileged": {
19
+
"type": "boolean",
20
+
"description": "If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients."
21
+
}
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "ref",
29
+
"ref": "#appPassword"
30
+
}
31
+
},
32
+
"errors": [{ "name": "AccountTakedown" }]
33
+
},
34
+
"appPassword": {
35
+
"type": "object",
36
+
"required": ["name", "password", "createdAt"],
37
+
"properties": {
38
+
"name": { "type": "string" },
39
+
"password": { "type": "string" },
40
+
"createdAt": { "type": "string", "format": "datetime" },
41
+
"privileged": { "type": "boolean" }
42
+
}
43
+
}
44
+
}
45
+
}
+31
lexicons/com/atproto/server/createInviteCode.json
+31
lexicons/com/atproto/server/createInviteCode.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.createInviteCode",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create an invite code.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["useCount"],
13
+
"properties": {
14
+
"useCount": { "type": "integer" },
15
+
"forAccount": { "type": "string", "format": "did" }
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["code"],
24
+
"properties": {
25
+
"code": { "type": "string" }
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
+49
lexicons/com/atproto/server/createInviteCodes.json
+49
lexicons/com/atproto/server/createInviteCodes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.createInviteCodes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create invite codes.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["codeCount", "useCount"],
13
+
"properties": {
14
+
"codeCount": { "type": "integer", "default": 1 },
15
+
"useCount": { "type": "integer" },
16
+
"forAccounts": {
17
+
"type": "array",
18
+
"items": { "type": "string", "format": "did" }
19
+
}
20
+
}
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/json",
25
+
"schema": {
26
+
"type": "object",
27
+
"required": ["codes"],
28
+
"properties": {
29
+
"codes": {
30
+
"type": "array",
31
+
"items": { "type": "ref", "ref": "#accountCodes" }
32
+
}
33
+
}
34
+
}
35
+
}
36
+
},
37
+
"accountCodes": {
38
+
"type": "object",
39
+
"required": ["account", "codes"],
40
+
"properties": {
41
+
"account": { "type": "string" },
42
+
"codes": {
43
+
"type": "array",
44
+
"items": { "type": "string" }
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
+56
lexicons/com/atproto/server/createSession.json
+56
lexicons/com/atproto/server/createSession.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.createSession",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create an authentication session.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["identifier", "password"],
13
+
"properties": {
14
+
"identifier": {
15
+
"type": "string",
16
+
"description": "Handle or other identifier supported by the server for the authenticating user."
17
+
},
18
+
"password": { "type": "string" },
19
+
"authFactorToken": { "type": "string" },
20
+
"allowTakendown": {
21
+
"type": "boolean",
22
+
"description": "When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned"
23
+
}
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["accessJwt", "refreshJwt", "handle", "did"],
32
+
"properties": {
33
+
"accessJwt": { "type": "string" },
34
+
"refreshJwt": { "type": "string" },
35
+
"handle": { "type": "string", "format": "handle" },
36
+
"did": { "type": "string", "format": "did" },
37
+
"didDoc": { "type": "unknown" },
38
+
"email": { "type": "string" },
39
+
"emailConfirmed": { "type": "boolean" },
40
+
"emailAuthFactor": { "type": "boolean" },
41
+
"active": { "type": "boolean" },
42
+
"status": {
43
+
"type": "string",
44
+
"description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
45
+
"knownValues": ["takendown", "suspended", "deactivated"]
46
+
}
47
+
}
48
+
}
49
+
},
50
+
"errors": [
51
+
{ "name": "AccountTakedown" },
52
+
{ "name": "AuthFactorTokenRequired" }
53
+
]
54
+
}
55
+
}
56
+
}
+23
lexicons/com/atproto/server/deactivateAccount.json
+23
lexicons/com/atproto/server/deactivateAccount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.deactivateAccount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"deleteAfter": {
14
+
"type": "string",
15
+
"format": "datetime",
16
+
"description": "A recommendation to server as to how long they should hold onto the deactivated account before deleting."
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+38
lexicons/com/atproto/server/defs.json
+38
lexicons/com/atproto/server/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.defs",
4
+
"defs": {
5
+
"inviteCode": {
6
+
"type": "object",
7
+
"required": [
8
+
"code",
9
+
"available",
10
+
"disabled",
11
+
"forAccount",
12
+
"createdBy",
13
+
"createdAt",
14
+
"uses"
15
+
],
16
+
"properties": {
17
+
"code": { "type": "string" },
18
+
"available": { "type": "integer" },
19
+
"disabled": { "type": "boolean" },
20
+
"forAccount": { "type": "string" },
21
+
"createdBy": { "type": "string" },
22
+
"createdAt": { "type": "string", "format": "datetime" },
23
+
"uses": {
24
+
"type": "array",
25
+
"items": { "type": "ref", "ref": "#inviteCodeUse" }
26
+
}
27
+
}
28
+
},
29
+
"inviteCodeUse": {
30
+
"type": "object",
31
+
"required": ["usedBy", "usedAt"],
32
+
"properties": {
33
+
"usedBy": { "type": "string", "format": "did" },
34
+
"usedAt": { "type": "string", "format": "datetime" }
35
+
}
36
+
}
37
+
}
38
+
}
+23
lexicons/com/atproto/server/deleteAccount.json
+23
lexicons/com/atproto/server/deleteAccount.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.deleteAccount",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "password", "token"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"password": { "type": "string" },
16
+
"token": { "type": "string" }
17
+
}
18
+
}
19
+
},
20
+
"errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
21
+
}
22
+
}
23
+
}
+11
lexicons/com/atproto/server/deleteSession.json
+11
lexicons/com/atproto/server/deleteSession.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.deleteSession",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').",
8
+
"errors": [{ "name": "InvalidToken" }, { "name": "ExpiredToken" }]
9
+
}
10
+
}
11
+
}
+59
lexicons/com/atproto/server/describeServer.json
+59
lexicons/com/atproto/server/describeServer.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.describeServer",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Describes the server's account creation requirements and capabilities. Implemented by PDS.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "availableUserDomains"],
13
+
"properties": {
14
+
"inviteCodeRequired": {
15
+
"type": "boolean",
16
+
"description": "If true, an invite code must be supplied to create an account on this instance."
17
+
},
18
+
"phoneVerificationRequired": {
19
+
"type": "boolean",
20
+
"description": "If true, a phone verification token must be supplied to create an account on this instance."
21
+
},
22
+
"availableUserDomains": {
23
+
"type": "array",
24
+
"description": "List of domain suffixes that can be used in account handles.",
25
+
"items": { "type": "string" }
26
+
},
27
+
"links": {
28
+
"type": "ref",
29
+
"description": "URLs of service policy documents.",
30
+
"ref": "#links"
31
+
},
32
+
"contact": {
33
+
"type": "ref",
34
+
"description": "Contact information",
35
+
"ref": "#contact"
36
+
},
37
+
"did": {
38
+
"type": "string",
39
+
"format": "did"
40
+
}
41
+
}
42
+
}
43
+
}
44
+
},
45
+
"links": {
46
+
"type": "object",
47
+
"properties": {
48
+
"privacyPolicy": { "type": "string", "format": "uri" },
49
+
"termsOfService": { "type": "string", "format": "uri" }
50
+
}
51
+
},
52
+
"contact": {
53
+
"type": "object",
54
+
"properties": {
55
+
"email": { "type": "string" }
56
+
}
57
+
}
58
+
}
59
+
}
+38
lexicons/com/atproto/server/getAccountInviteCodes.json
+38
lexicons/com/atproto/server/getAccountInviteCodes.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.getAccountInviteCodes",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get all invite codes for the current account. Requires auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"includeUsed": { "type": "boolean", "default": true },
12
+
"createAvailable": {
13
+
"type": "boolean",
14
+
"default": true,
15
+
"description": "Controls whether any new 'earned' but not 'created' invites should be created."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["codes"],
24
+
"properties": {
25
+
"codes": {
26
+
"type": "array",
27
+
"items": {
28
+
"type": "ref",
29
+
"ref": "com.atproto.server.defs#inviteCode"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
},
35
+
"errors": [{ "name": "DuplicateCreate" }]
36
+
}
37
+
}
38
+
}
+48
lexicons/com/atproto/server/getServiceAuth.json
+48
lexicons/com/atproto/server/getServiceAuth.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.getServiceAuth",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a signed token on behalf of the requesting DID for the requested service.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["aud"],
11
+
"properties": {
12
+
"aud": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the service that the token will be used to authenticate with"
16
+
},
17
+
"exp": {
18
+
"type": "integer",
19
+
"description": "The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope."
20
+
},
21
+
"lxm": {
22
+
"type": "string",
23
+
"format": "nsid",
24
+
"description": "Lexicon (XRPC) method to bind the requested token to"
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["token"],
33
+
"properties": {
34
+
"token": {
35
+
"type": "string"
36
+
}
37
+
}
38
+
}
39
+
},
40
+
"errors": [
41
+
{
42
+
"name": "BadExpiration",
43
+
"description": "Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes."
44
+
}
45
+
]
46
+
}
47
+
}
48
+
}
+31
lexicons/com/atproto/server/getSession.json
+31
lexicons/com/atproto/server/getSession.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.getSession",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get information about the current auth session. Requires auth.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["handle", "did"],
13
+
"properties": {
14
+
"handle": { "type": "string", "format": "handle" },
15
+
"did": { "type": "string", "format": "did" },
16
+
"didDoc": { "type": "unknown" },
17
+
"email": { "type": "string" },
18
+
"emailConfirmed": { "type": "boolean" },
19
+
"emailAuthFactor": { "type": "boolean" },
20
+
"active": { "type": "boolean" },
21
+
"status": {
22
+
"type": "string",
23
+
"description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
24
+
"knownValues": ["takendown", "suspended", "deactivated"]
25
+
}
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
+33
lexicons/com/atproto/server/listAppPasswords.json
+33
lexicons/com/atproto/server/listAppPasswords.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.listAppPasswords",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List all App Passwords.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["passwords"],
13
+
"properties": {
14
+
"passwords": {
15
+
"type": "array",
16
+
"items": { "type": "ref", "ref": "#appPassword" }
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"errors": [{ "name": "AccountTakedown" }]
22
+
},
23
+
"appPassword": {
24
+
"type": "object",
25
+
"required": ["name", "createdAt"],
26
+
"properties": {
27
+
"name": { "type": "string" },
28
+
"createdAt": { "type": "string", "format": "datetime" },
29
+
"privileged": { "type": "boolean" }
30
+
}
31
+
}
32
+
}
33
+
}
+38
lexicons/com/atproto/server/refreshSession.json
+38
lexicons/com/atproto/server/refreshSession.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.refreshSession",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["accessJwt", "refreshJwt", "handle", "did"],
13
+
"properties": {
14
+
"accessJwt": { "type": "string" },
15
+
"refreshJwt": { "type": "string" },
16
+
"handle": { "type": "string", "format": "handle" },
17
+
"did": { "type": "string", "format": "did" },
18
+
"didDoc": { "type": "unknown" },
19
+
"email": { "type": "string" },
20
+
"emailConfirmed": { "type": "boolean" },
21
+
"emailAuthFactor": { "type": "boolean" },
22
+
"active": { "type": "boolean" },
23
+
"status": {
24
+
"type": "string",
25
+
"description": "Hosting status of the account. If not specified, then assume 'active'.",
26
+
"knownValues": ["takendown", "suspended", "deactivated"]
27
+
}
28
+
}
29
+
}
30
+
},
31
+
"errors": [
32
+
{ "name": "AccountTakedown" },
33
+
{ "name": "InvalidToken" },
34
+
{ "name": "ExpiredToken" }
35
+
]
36
+
}
37
+
}
38
+
}
+10
lexicons/com/atproto/server/requestAccountDelete.json
+10
lexicons/com/atproto/server/requestAccountDelete.json
+10
lexicons/com/atproto/server/requestEmailConfirmation.json
+10
lexicons/com/atproto/server/requestEmailConfirmation.json
+20
lexicons/com/atproto/server/requestEmailUpdate.json
+20
lexicons/com/atproto/server/requestEmailUpdate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.requestEmailUpdate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Request a token in order to update email.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["tokenRequired"],
13
+
"properties": {
14
+
"tokenRequired": { "type": "boolean" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+20
lexicons/com/atproto/server/requestPasswordReset.json
+20
lexicons/com/atproto/server/requestPasswordReset.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.requestPasswordReset",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Initiate a user account password reset via email.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["email"],
13
+
"properties": {
14
+
"email": { "type": "string" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+36
lexicons/com/atproto/server/reserveSigningKey.json
+36
lexicons/com/atproto/server/reserveSigningKey.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.reserveSigningKey",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"did": {
14
+
"type": "string",
15
+
"format": "did",
16
+
"description": "The DID to reserve a key for."
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["signingKey"],
26
+
"properties": {
27
+
"signingKey": {
28
+
"type": "string",
29
+
"description": "The public key for the reserved signing key, in did:key serialization."
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+22
lexicons/com/atproto/server/resetPassword.json
+22
lexicons/com/atproto/server/resetPassword.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.resetPassword",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Reset a user account password using a token.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["token", "password"],
13
+
"properties": {
14
+
"token": { "type": "string" },
15
+
"password": { "type": "string" }
16
+
}
17
+
}
18
+
},
19
+
"errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
20
+
}
21
+
}
22
+
}
+20
lexicons/com/atproto/server/revokeAppPassword.json
+20
lexicons/com/atproto/server/revokeAppPassword.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.revokeAppPassword",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Revoke an App Password by name.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["name"],
13
+
"properties": {
14
+
"name": { "type": "string" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+30
lexicons/com/atproto/server/updateEmail.json
+30
lexicons/com/atproto/server/updateEmail.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.server.updateEmail",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update an account's email.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["email"],
13
+
"properties": {
14
+
"email": { "type": "string" },
15
+
"emailAuthFactor": { "type": "boolean" },
16
+
"token": {
17
+
"type": "string",
18
+
"description": "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
19
+
}
20
+
}
21
+
}
22
+
},
23
+
"errors": [
24
+
{ "name": "ExpiredToken" },
25
+
{ "name": "InvalidToken" },
26
+
{ "name": "TokenRequired" }
27
+
]
28
+
}
29
+
}
30
+
}
+10
lexicons/com/atproto/sync/defs.json
+10
lexicons/com/atproto/sync/defs.json
+36
lexicons/com/atproto/sync/getBlob.json
+36
lexicons/com/atproto/sync/getBlob.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getBlob",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did", "cid"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the account."
16
+
},
17
+
"cid": {
18
+
"type": "string",
19
+
"format": "cid",
20
+
"description": "The CID of the blob to fetch"
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "*/*"
26
+
},
27
+
"errors": [
28
+
{ "name": "BlobNotFound" },
29
+
{ "name": "RepoNotFound" },
30
+
{ "name": "RepoTakendown" },
31
+
{ "name": "RepoSuspended" },
32
+
{ "name": "RepoDeactivated" }
33
+
]
34
+
}
35
+
}
36
+
}
+35
lexicons/com/atproto/sync/getBlocks.json
+35
lexicons/com/atproto/sync/getBlocks.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getBlocks",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did", "cids"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
},
17
+
"cids": {
18
+
"type": "array",
19
+
"items": { "type": "string", "format": "cid" }
20
+
}
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/vnd.ipld.car"
25
+
},
26
+
"errors": [
27
+
{ "name": "BlockNotFound" },
28
+
{ "name": "RepoNotFound" },
29
+
{ "name": "RepoTakendown" },
30
+
{ "name": "RepoSuspended" },
31
+
{ "name": "RepoDeactivated" }
32
+
]
33
+
}
34
+
}
35
+
}
+24
lexicons/com/atproto/sync/getCheckout.json
+24
lexicons/com/atproto/sync/getCheckout.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getCheckout",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "DEPRECATED - please use com.atproto.sync.getRepo instead",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/vnd.ipld.car"
21
+
}
22
+
}
23
+
}
24
+
}
+32
lexicons/com/atproto/sync/getHead.json
+32
lexicons/com/atproto/sync/getHead.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getHead",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "DEPRECATED - please use com.atproto.sync.getLatestCommit instead",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["root"],
24
+
"properties": {
25
+
"root": { "type": "string", "format": "cid" }
26
+
}
27
+
}
28
+
},
29
+
"errors": [{ "name": "HeadNotFound" }]
30
+
}
31
+
}
32
+
}
+43
lexicons/com/atproto/sync/getHostStatus.json
+43
lexicons/com/atproto/sync/getHostStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getHostStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Returns information about a specified upstream host, as consumed by the server. Implemented by relays.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["hostname"],
11
+
"properties": {
12
+
"hostname": {
13
+
"type": "string",
14
+
"description": "Hostname of the host (eg, PDS or relay) being queried."
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["hostname"],
23
+
"properties": {
24
+
"hostname": { "type": "string" },
25
+
"seq": {
26
+
"type": "integer",
27
+
"description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)."
28
+
},
29
+
"accountCount": {
30
+
"type": "integer",
31
+
"description": "Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts."
32
+
},
33
+
"status": {
34
+
"type": "ref",
35
+
"ref": "com.atproto.sync.defs#hostStatus"
36
+
}
37
+
}
38
+
}
39
+
},
40
+
"errors": [{ "name": "HostNotFound" }]
41
+
}
42
+
}
43
+
}
+38
lexicons/com/atproto/sync/getLatestCommit.json
+38
lexicons/com/atproto/sync/getLatestCommit.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getLatestCommit",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the current commit CID & revision of the specified repo. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["cid", "rev"],
24
+
"properties": {
25
+
"cid": { "type": "string", "format": "cid" },
26
+
"rev": { "type": "string", "format": "tid" }
27
+
}
28
+
}
29
+
},
30
+
"errors": [
31
+
{ "name": "RepoNotFound" },
32
+
{ "name": "RepoTakendown" },
33
+
{ "name": "RepoSuspended" },
34
+
{ "name": "RepoDeactivated" }
35
+
]
36
+
}
37
+
}
38
+
}
+37
lexicons/com/atproto/sync/getRecord.json
+37
lexicons/com/atproto/sync/getRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did", "collection", "rkey"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
},
17
+
"collection": { "type": "string", "format": "nsid" },
18
+
"rkey": {
19
+
"type": "string",
20
+
"description": "Record Key",
21
+
"format": "record-key"
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/vnd.ipld.car"
27
+
},
28
+
"errors": [
29
+
{ "name": "RecordNotFound" },
30
+
{ "name": "RepoNotFound" },
31
+
{ "name": "RepoTakendown" },
32
+
{ "name": "RepoSuspended" },
33
+
{ "name": "RepoDeactivated" }
34
+
]
35
+
}
36
+
}
37
+
}
+35
lexicons/com/atproto/sync/getRepo.json
+35
lexicons/com/atproto/sync/getRepo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getRepo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
},
17
+
"since": {
18
+
"type": "string",
19
+
"format": "tid",
20
+
"description": "The revision ('rev') of the repo to create a diff from."
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/vnd.ipld.car"
26
+
},
27
+
"errors": [
28
+
{ "name": "RepoNotFound" },
29
+
{ "name": "RepoTakendown" },
30
+
{ "name": "RepoSuspended" },
31
+
{ "name": "RepoDeactivated" }
32
+
]
33
+
}
34
+
}
35
+
}
+50
lexicons/com/atproto/sync/getRepoStatus.json
+50
lexicons/com/atproto/sync/getRepoStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.getRepoStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["did", "active"],
24
+
"properties": {
25
+
"did": { "type": "string", "format": "did" },
26
+
"active": { "type": "boolean" },
27
+
"status": {
28
+
"type": "string",
29
+
"description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
30
+
"knownValues": [
31
+
"takendown",
32
+
"suspended",
33
+
"deleted",
34
+
"deactivated",
35
+
"desynchronized",
36
+
"throttled"
37
+
]
38
+
},
39
+
"rev": {
40
+
"type": "string",
41
+
"format": "tid",
42
+
"description": "Optional field, the current rev of the repo, if active=true"
43
+
}
44
+
}
45
+
}
46
+
},
47
+
"errors": [{ "name": "RepoNotFound" }]
48
+
}
49
+
}
50
+
}
+53
lexicons/com/atproto/sync/listBlobs.json
+53
lexicons/com/atproto/sync/listBlobs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.listBlobs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "The DID of the repo."
16
+
},
17
+
"since": {
18
+
"type": "string",
19
+
"format": "tid",
20
+
"description": "Optional revision of the repo to list blobs since."
21
+
},
22
+
"limit": {
23
+
"type": "integer",
24
+
"minimum": 1,
25
+
"maximum": 1000,
26
+
"default": 500
27
+
},
28
+
"cursor": { "type": "string" }
29
+
}
30
+
},
31
+
"output": {
32
+
"encoding": "application/json",
33
+
"schema": {
34
+
"type": "object",
35
+
"required": ["cids"],
36
+
"properties": {
37
+
"cursor": { "type": "string" },
38
+
"cids": {
39
+
"type": "array",
40
+
"items": { "type": "string", "format": "cid" }
41
+
}
42
+
}
43
+
}
44
+
},
45
+
"errors": [
46
+
{ "name": "RepoNotFound" },
47
+
{ "name": "RepoTakendown" },
48
+
{ "name": "RepoSuspended" },
49
+
{ "name": "RepoDeactivated" }
50
+
]
51
+
}
52
+
}
53
+
}
+56
lexicons/com/atproto/sync/listHosts.json
+56
lexicons/com/atproto/sync/listHosts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.listHosts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 1000,
15
+
"default": 200
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["hosts"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"hosts": {
28
+
"type": "array",
29
+
"items": { "type": "ref", "ref": "#host" },
30
+
"description": "Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first."
31
+
}
32
+
}
33
+
}
34
+
}
35
+
},
36
+
"host": {
37
+
"type": "object",
38
+
"required": ["hostname"],
39
+
"properties": {
40
+
"hostname": {
41
+
"type": "string",
42
+
"description": "hostname of server; not a URL (no scheme)"
43
+
},
44
+
"seq": {
45
+
"type": "integer",
46
+
"description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)."
47
+
},
48
+
"accountCount": { "type": "integer" },
49
+
"status": {
50
+
"type": "ref",
51
+
"ref": "com.atproto.sync.defs#hostStatus"
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
+62
lexicons/com/atproto/sync/listRepos.json
+62
lexicons/com/atproto/sync/listRepos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.listRepos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 1000,
15
+
"default": 500
16
+
},
17
+
"cursor": { "type": "string" }
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["repos"],
25
+
"properties": {
26
+
"cursor": { "type": "string" },
27
+
"repos": {
28
+
"type": "array",
29
+
"items": { "type": "ref", "ref": "#repo" }
30
+
}
31
+
}
32
+
}
33
+
}
34
+
},
35
+
"repo": {
36
+
"type": "object",
37
+
"required": ["did", "head", "rev"],
38
+
"properties": {
39
+
"did": { "type": "string", "format": "did" },
40
+
"head": {
41
+
"type": "string",
42
+
"format": "cid",
43
+
"description": "Current repo commit CID"
44
+
},
45
+
"rev": { "type": "string", "format": "tid" },
46
+
"active": { "type": "boolean" },
47
+
"status": {
48
+
"type": "string",
49
+
"description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
50
+
"knownValues": [
51
+
"takendown",
52
+
"suspended",
53
+
"deleted",
54
+
"deactivated",
55
+
"desynchronized",
56
+
"throttled"
57
+
]
58
+
}
59
+
}
60
+
}
61
+
}
62
+
}
+46
lexicons/com/atproto/sync/listReposByCollection.json
+46
lexicons/com/atproto/sync/listReposByCollection.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.listReposByCollection",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Enumerates all the DIDs which have records with the given collection NSID.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["collection"],
11
+
"properties": {
12
+
"collection": { "type": "string", "format": "nsid" },
13
+
"limit": {
14
+
"type": "integer",
15
+
"description": "Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.",
16
+
"minimum": 1,
17
+
"maximum": 2000,
18
+
"default": 500
19
+
},
20
+
"cursor": { "type": "string" }
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/json",
25
+
"schema": {
26
+
"type": "object",
27
+
"required": ["repos"],
28
+
"properties": {
29
+
"cursor": { "type": "string" },
30
+
"repos": {
31
+
"type": "array",
32
+
"items": { "type": "ref", "ref": "#repo" }
33
+
}
34
+
}
35
+
}
36
+
}
37
+
},
38
+
"repo": {
39
+
"type": "object",
40
+
"required": ["did"],
41
+
"properties": {
42
+
"did": { "type": "string", "format": "did" }
43
+
}
44
+
}
45
+
}
46
+
}
+23
lexicons/com/atproto/sync/notifyOfUpdate.json
+23
lexicons/com/atproto/sync/notifyOfUpdate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.notifyOfUpdate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["hostname"],
13
+
"properties": {
14
+
"hostname": {
15
+
"type": "string",
16
+
"description": "Hostname of the current service (usually a PDS) that is notifying of update."
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+24
lexicons/com/atproto/sync/requestCrawl.json
+24
lexicons/com/atproto/sync/requestCrawl.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.requestCrawl",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["hostname"],
13
+
"properties": {
14
+
"hostname": {
15
+
"type": "string",
16
+
"description": "Hostname of the current service (eg, PDS) that is requesting to be crawled."
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"errors": [{ "name": "HostBanned" }]
22
+
}
23
+
}
24
+
}
+215
lexicons/com/atproto/sync/subscribeRepos.json
+215
lexicons/com/atproto/sync/subscribeRepos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.sync.subscribeRepos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "subscription",
7
+
"description": "Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"cursor": {
12
+
"type": "integer",
13
+
"description": "The last known event seq number to backfill from."
14
+
}
15
+
}
16
+
},
17
+
"message": {
18
+
"schema": {
19
+
"type": "union",
20
+
"refs": ["#commit", "#sync", "#identity", "#account", "#info"]
21
+
}
22
+
},
23
+
"errors": [
24
+
{ "name": "FutureCursor" },
25
+
{
26
+
"name": "ConsumerTooSlow",
27
+
"description": "If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection."
28
+
}
29
+
]
30
+
},
31
+
"commit": {
32
+
"type": "object",
33
+
"description": "Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.",
34
+
"required": [
35
+
"seq",
36
+
"rebase",
37
+
"tooBig",
38
+
"repo",
39
+
"commit",
40
+
"rev",
41
+
"since",
42
+
"blocks",
43
+
"ops",
44
+
"blobs",
45
+
"time"
46
+
],
47
+
"nullable": ["since"],
48
+
"properties": {
49
+
"seq": {
50
+
"type": "integer",
51
+
"description": "The stream sequence number of this message."
52
+
},
53
+
"rebase": { "type": "boolean", "description": "DEPRECATED -- unused" },
54
+
"tooBig": {
55
+
"type": "boolean",
56
+
"description": "DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data."
57
+
},
58
+
"repo": {
59
+
"type": "string",
60
+
"format": "did",
61
+
"description": "The repo this event comes from. Note that all other message types name this field 'did'."
62
+
},
63
+
"commit": {
64
+
"type": "cid-link",
65
+
"description": "Repo commit object CID."
66
+
},
67
+
"rev": {
68
+
"type": "string",
69
+
"format": "tid",
70
+
"description": "The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event."
71
+
},
72
+
"since": {
73
+
"type": "string",
74
+
"format": "tid",
75
+
"description": "The rev of the last emitted commit from this repo (if any)."
76
+
},
77
+
"blocks": {
78
+
"type": "bytes",
79
+
"description": "CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.",
80
+
"maxLength": 2000000
81
+
},
82
+
"ops": {
83
+
"type": "array",
84
+
"items": {
85
+
"type": "ref",
86
+
"ref": "#repoOp",
87
+
"description": "List of repo mutation operations in this commit (eg, records created, updated, or deleted)."
88
+
},
89
+
"maxLength": 200
90
+
},
91
+
"blobs": {
92
+
"type": "array",
93
+
"items": {
94
+
"type": "cid-link",
95
+
"description": "DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit."
96
+
}
97
+
},
98
+
"prevData": {
99
+
"type": "cid-link",
100
+
"description": "The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose."
101
+
},
102
+
"time": {
103
+
"type": "string",
104
+
"format": "datetime",
105
+
"description": "Timestamp of when this message was originally broadcast."
106
+
}
107
+
}
108
+
},
109
+
"sync": {
110
+
"type": "object",
111
+
"description": "Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.",
112
+
"required": ["seq", "did", "blocks", "rev", "time"],
113
+
"properties": {
114
+
"seq": {
115
+
"type": "integer",
116
+
"description": "The stream sequence number of this message."
117
+
},
118
+
"did": {
119
+
"type": "string",
120
+
"format": "did",
121
+
"description": "The account this repo event corresponds to. Must match that in the commit object."
122
+
},
123
+
"blocks": {
124
+
"type": "bytes",
125
+
"description": "CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.",
126
+
"maxLength": 10000
127
+
},
128
+
"rev": {
129
+
"type": "string",
130
+
"description": "The rev of the commit. This value must match that in the commit object."
131
+
},
132
+
"time": {
133
+
"type": "string",
134
+
"format": "datetime",
135
+
"description": "Timestamp of when this message was originally broadcast."
136
+
}
137
+
}
138
+
},
139
+
"identity": {
140
+
"type": "object",
141
+
"description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.",
142
+
"required": ["seq", "did", "time"],
143
+
"properties": {
144
+
"seq": { "type": "integer" },
145
+
"did": { "type": "string", "format": "did" },
146
+
"time": { "type": "string", "format": "datetime" },
147
+
"handle": {
148
+
"type": "string",
149
+
"format": "handle",
150
+
"description": "The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details."
151
+
}
152
+
}
153
+
},
154
+
"account": {
155
+
"type": "object",
156
+
"description": "Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.",
157
+
"required": ["seq", "did", "time", "active"],
158
+
"properties": {
159
+
"seq": { "type": "integer" },
160
+
"did": { "type": "string", "format": "did" },
161
+
"time": { "type": "string", "format": "datetime" },
162
+
"active": {
163
+
"type": "boolean",
164
+
"description": "Indicates that the account has a repository which can be fetched from the host that emitted this event."
165
+
},
166
+
"status": {
167
+
"type": "string",
168
+
"description": "If active=false, this optional field indicates a reason for why the account is not active.",
169
+
"knownValues": [
170
+
"takendown",
171
+
"suspended",
172
+
"deleted",
173
+
"deactivated",
174
+
"desynchronized",
175
+
"throttled"
176
+
]
177
+
}
178
+
}
179
+
},
180
+
"info": {
181
+
"type": "object",
182
+
"required": ["name"],
183
+
"properties": {
184
+
"name": {
185
+
"type": "string",
186
+
"knownValues": ["OutdatedCursor"]
187
+
},
188
+
"message": {
189
+
"type": "string"
190
+
}
191
+
}
192
+
},
193
+
"repoOp": {
194
+
"type": "object",
195
+
"description": "A repo operation, ie a mutation of a single record.",
196
+
"required": ["action", "path", "cid"],
197
+
"nullable": ["cid"],
198
+
"properties": {
199
+
"action": {
200
+
"type": "string",
201
+
"knownValues": ["create", "update", "delete"]
202
+
},
203
+
"path": { "type": "string" },
204
+
"cid": {
205
+
"type": "cid-link",
206
+
"description": "For creates and updates, the new record CID. For deletions, null."
207
+
},
208
+
"prev": {
209
+
"type": "cid-link",
210
+
"description": "For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined."
211
+
}
212
+
}
213
+
}
214
+
}
215
+
}
+27
lexicons/com/atproto/temp/addReservedHandle.json
+27
lexicons/com/atproto/temp/addReservedHandle.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.addReservedHandle",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Add a handle to the set of reserved handles.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["handle"],
13
+
"properties": {
14
+
"handle": { "type": "string" }
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"properties": {}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+88
lexicons/com/atproto/temp/checkHandleAvailability.json
+88
lexicons/com/atproto/temp/checkHandleAvailability.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.checkHandleAvailability",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["handle"],
11
+
"properties": {
12
+
"handle": {
13
+
"type": "string",
14
+
"format": "handle",
15
+
"description": "Tentative handle. Will be checked for availability or used to build handle suggestions."
16
+
},
17
+
"email": {
18
+
"type": "string",
19
+
"description": "User-provided email. Might be used to build handle suggestions."
20
+
},
21
+
"birthDate": {
22
+
"type": "string",
23
+
"format": "datetime",
24
+
"description": "User-provided birth date. Might be used to build handle suggestions."
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["handle", "result"],
33
+
"properties": {
34
+
"handle": {
35
+
"type": "string",
36
+
"format": "handle",
37
+
"description": "Echo of the input handle."
38
+
},
39
+
"result": {
40
+
"type": "union",
41
+
"refs": ["#resultAvailable", "#resultUnavailable"]
42
+
}
43
+
}
44
+
}
45
+
},
46
+
"errors": [
47
+
{
48
+
"name": "InvalidEmail",
49
+
"description": "An invalid email was provided."
50
+
}
51
+
]
52
+
},
53
+
"resultAvailable": {
54
+
"type": "object",
55
+
"description": "Indicates the provided handle is available.",
56
+
"properties": {}
57
+
},
58
+
"resultUnavailable": {
59
+
"type": "object",
60
+
"description": "Indicates the provided handle is unavailable and gives suggestions of available handles.",
61
+
"required": ["suggestions"],
62
+
"properties": {
63
+
"suggestions": {
64
+
"type": "array",
65
+
"description": "List of suggested handles based on the provided inputs.",
66
+
"items": {
67
+
"type": "ref",
68
+
"ref": "#suggestion"
69
+
}
70
+
}
71
+
}
72
+
},
73
+
"suggestion": {
74
+
"type": "object",
75
+
"required": ["handle", "method"],
76
+
"properties": {
77
+
"handle": {
78
+
"type": "string",
79
+
"format": "handle"
80
+
},
81
+
"method": {
82
+
"type": "string",
83
+
"description": "Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics."
84
+
}
85
+
}
86
+
}
87
+
}
88
+
}
+22
lexicons/com/atproto/temp/checkSignupQueue.json
+22
lexicons/com/atproto/temp/checkSignupQueue.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.checkSignupQueue",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Check accounts location in signup queue.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["activated"],
13
+
"properties": {
14
+
"activated": { "type": "boolean" },
15
+
"placeInQueue": { "type": "integer" },
16
+
"estimatedTimeMs": { "type": "integer" }
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
+39
lexicons/com/atproto/temp/dereferenceScope.json
+39
lexicons/com/atproto/temp/dereferenceScope.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.dereferenceScope",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Allows finding the oauth permission scope from a reference",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["scope"],
11
+
"properties": {
12
+
"scope": {
13
+
"type": "string",
14
+
"description": "The scope reference (starts with 'ref:')"
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["scope"],
23
+
"properties": {
24
+
"scope": {
25
+
"type": "string",
26
+
"description": "The full oauth permission scope"
27
+
}
28
+
}
29
+
}
30
+
},
31
+
"errors": [
32
+
{
33
+
"name": "InvalidScopeReference",
34
+
"description": "An invalid scope reference was provided."
35
+
}
36
+
]
37
+
}
38
+
}
39
+
}
+35
lexicons/com/atproto/temp/fetchLabels.json
+35
lexicons/com/atproto/temp/fetchLabels.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.fetchLabels",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"since": { "type": "integer" },
12
+
"limit": {
13
+
"type": "integer",
14
+
"minimum": 1,
15
+
"maximum": 250,
16
+
"default": 50
17
+
}
18
+
}
19
+
},
20
+
"output": {
21
+
"encoding": "application/json",
22
+
"schema": {
23
+
"type": "object",
24
+
"required": ["labels"],
25
+
"properties": {
26
+
"labels": {
27
+
"type": "array",
28
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
+20
lexicons/com/atproto/temp/requestPhoneVerification.json
+20
lexicons/com/atproto/temp/requestPhoneVerification.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.requestPhoneVerification",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Request a verification code to be sent to the supplied phone number",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["phoneNumber"],
13
+
"properties": {
14
+
"phoneNumber": { "type": "string" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+23
lexicons/com/atproto/temp/revokeAccountCredentials.json
+23
lexicons/com/atproto/temp/revokeAccountCredentials.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.temp.revokeAccountCredentials",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["account"],
13
+
"properties": {
14
+
"account": {
15
+
"type": "string",
16
+
"format": "at-identifier"
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
+49
lexicons/tools/ozone/communication/createTemplate.json
+49
lexicons/tools/ozone/communication/createTemplate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.communication.createTemplate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Administrative action to create a new, re-usable communication (email for now) template.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["subject", "contentMarkdown", "name"],
13
+
"properties": {
14
+
"name": {
15
+
"type": "string",
16
+
"description": "Name of the template."
17
+
},
18
+
"contentMarkdown": {
19
+
"type": "string",
20
+
"description": "Content of the template, markdown supported, can contain variable placeholders."
21
+
},
22
+
"subject": {
23
+
"type": "string",
24
+
"description": "Subject of the message, used in emails."
25
+
},
26
+
"lang": {
27
+
"type": "string",
28
+
"format": "language",
29
+
"description": "Message language."
30
+
},
31
+
"createdBy": {
32
+
"type": "string",
33
+
"format": "did",
34
+
"description": "DID of the user who is creating the template."
35
+
}
36
+
}
37
+
}
38
+
},
39
+
"output": {
40
+
"encoding": "application/json",
41
+
"schema": {
42
+
"type": "ref",
43
+
"ref": "tools.ozone.communication.defs#templateView"
44
+
}
45
+
},
46
+
"errors": [{ "name": "DuplicateTemplateName" }]
47
+
}
48
+
}
49
+
}
+43
lexicons/tools/ozone/communication/defs.json
+43
lexicons/tools/ozone/communication/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.communication.defs",
4
+
"defs": {
5
+
"templateView": {
6
+
"type": "object",
7
+
"required": [
8
+
"id",
9
+
"name",
10
+
"contentMarkdown",
11
+
"disabled",
12
+
"lastUpdatedBy",
13
+
"createdAt",
14
+
"updatedAt"
15
+
],
16
+
"properties": {
17
+
"id": { "type": "string" },
18
+
"name": { "type": "string", "description": "Name of the template." },
19
+
"subject": {
20
+
"type": "string",
21
+
"description": "Content of the template, can contain markdown and variable placeholders."
22
+
},
23
+
"contentMarkdown": {
24
+
"type": "string",
25
+
"description": "Subject of the message, used in emails."
26
+
},
27
+
"disabled": { "type": "boolean" },
28
+
"lang": {
29
+
"type": "string",
30
+
"format": "language",
31
+
"description": "Message language."
32
+
},
33
+
"lastUpdatedBy": {
34
+
"type": "string",
35
+
"format": "did",
36
+
"description": "DID of the user who last updated the template."
37
+
},
38
+
"createdAt": { "type": "string", "format": "datetime" },
39
+
"updatedAt": { "type": "string", "format": "datetime" }
40
+
}
41
+
}
42
+
}
43
+
}
+20
lexicons/tools/ozone/communication/deleteTemplate.json
+20
lexicons/tools/ozone/communication/deleteTemplate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.communication.deleteTemplate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete a communication template.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["id"],
13
+
"properties": {
14
+
"id": { "type": "string" }
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
+26
lexicons/tools/ozone/communication/listTemplates.json
+26
lexicons/tools/ozone/communication/listTemplates.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.communication.listTemplates",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get list of all communication templates.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["communicationTemplates"],
13
+
"properties": {
14
+
"communicationTemplates": {
15
+
"type": "array",
16
+
"items": {
17
+
"type": "ref",
18
+
"ref": "tools.ozone.communication.defs#templateView"
19
+
}
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
+56
lexicons/tools/ozone/communication/updateTemplate.json
+56
lexicons/tools/ozone/communication/updateTemplate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.communication.updateTemplate",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["id"],
13
+
"properties": {
14
+
"id": {
15
+
"type": "string",
16
+
"description": "ID of the template to be updated."
17
+
},
18
+
"name": {
19
+
"type": "string",
20
+
"description": "Name of the template."
21
+
},
22
+
"lang": {
23
+
"type": "string",
24
+
"format": "language",
25
+
"description": "Message language."
26
+
},
27
+
"contentMarkdown": {
28
+
"type": "string",
29
+
"description": "Content of the template, markdown supported, can contain variable placeholders."
30
+
},
31
+
"subject": {
32
+
"type": "string",
33
+
"description": "Subject of the message, used in emails."
34
+
},
35
+
"updatedBy": {
36
+
"type": "string",
37
+
"format": "did",
38
+
"description": "DID of the user who is updating the template."
39
+
},
40
+
"disabled": {
41
+
"type": "boolean"
42
+
}
43
+
}
44
+
}
45
+
},
46
+
"output": {
47
+
"encoding": "application/json",
48
+
"schema": {
49
+
"type": "ref",
50
+
"ref": "tools.ozone.communication.defs#templateView"
51
+
}
52
+
},
53
+
"errors": [{ "name": "DuplicateTemplateName" }]
54
+
}
55
+
}
56
+
}
+106
lexicons/tools/ozone/hosting/getAccountHistory.json
+106
lexicons/tools/ozone/hosting/getAccountHistory.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.hosting.getAccountHistory",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get account history, e.g. log of updated email addresses or other identity information.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": { "type": "string", "format": "did" },
13
+
"events": {
14
+
"type": "array",
15
+
"items": {
16
+
"type": "string",
17
+
"knownValues": [
18
+
"accountCreated",
19
+
"emailUpdated",
20
+
"emailConfirmed",
21
+
"passwordUpdated",
22
+
"handleUpdated"
23
+
]
24
+
}
25
+
},
26
+
"cursor": { "type": "string" },
27
+
"limit": {
28
+
"type": "integer",
29
+
"minimum": 1,
30
+
"maximum": 100,
31
+
"default": 50
32
+
}
33
+
}
34
+
},
35
+
"output": {
36
+
"encoding": "application/json",
37
+
"schema": {
38
+
"type": "object",
39
+
"required": ["events"],
40
+
"properties": {
41
+
"cursor": { "type": "string" },
42
+
"events": {
43
+
"type": "array",
44
+
"items": {
45
+
"type": "ref",
46
+
"ref": "#event"
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
52
+
},
53
+
"event": {
54
+
"type": "object",
55
+
"required": ["details", "createdBy", "createdAt"],
56
+
"properties": {
57
+
"details": {
58
+
"type": "union",
59
+
"refs": [
60
+
"#accountCreated",
61
+
"#emailUpdated",
62
+
"#emailConfirmed",
63
+
"#passwordUpdated",
64
+
"#handleUpdated"
65
+
]
66
+
},
67
+
"createdBy": { "type": "string" },
68
+
"createdAt": { "type": "string", "format": "datetime" }
69
+
}
70
+
},
71
+
"accountCreated": {
72
+
"type": "object",
73
+
"required": [],
74
+
"properties": {
75
+
"email": { "type": "string" },
76
+
"handle": { "type": "string", "format": "handle" }
77
+
}
78
+
},
79
+
"emailUpdated": {
80
+
"type": "object",
81
+
"required": ["email"],
82
+
"properties": {
83
+
"email": { "type": "string" }
84
+
}
85
+
},
86
+
"emailConfirmed": {
87
+
"type": "object",
88
+
"required": ["email"],
89
+
"properties": {
90
+
"email": { "type": "string" }
91
+
}
92
+
},
93
+
"passwordUpdated": {
94
+
"type": "object",
95
+
"required": [],
96
+
"properties": {}
97
+
},
98
+
"handleUpdated": {
99
+
"type": "object",
100
+
"required": ["handle"],
101
+
"properties": {
102
+
"handle": { "type": "string", "format": "handle" }
103
+
}
104
+
}
105
+
}
106
+
}
+64
lexicons/tools/ozone/moderation/cancelScheduledActions.json
+64
lexicons/tools/ozone/moderation/cancelScheduledActions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.cancelScheduledActions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Cancel all pending scheduled moderation actions for specified subjects",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["subjects"],
13
+
"properties": {
14
+
"subjects": {
15
+
"type": "array",
16
+
"maxLength": 100,
17
+
"items": {
18
+
"type": "string",
19
+
"format": "did"
20
+
},
21
+
"description": "Array of DID subjects to cancel scheduled actions for"
22
+
},
23
+
"comment": {
24
+
"type": "string",
25
+
"description": "Optional comment describing the reason for cancellation"
26
+
}
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "ref",
34
+
"ref": "#cancellationResults"
35
+
}
36
+
}
37
+
},
38
+
"cancellationResults": {
39
+
"type": "object",
40
+
"required": ["succeeded", "failed"],
41
+
"properties": {
42
+
"succeeded": {
43
+
"type": "array",
44
+
"items": { "type": "string", "format": "did" },
45
+
"description": "DIDs for which all pending scheduled actions were successfully cancelled"
46
+
},
47
+
"failed": {
48
+
"type": "array",
49
+
"items": { "type": "ref", "ref": "#failedCancellation" },
50
+
"description": "DIDs for which cancellation failed with error details"
51
+
}
52
+
}
53
+
},
54
+
"failedCancellation": {
55
+
"type": "object",
56
+
"required": ["did", "error"],
57
+
"properties": {
58
+
"did": { "type": "string", "format": "did" },
59
+
"error": { "type": "string" },
60
+
"errorCode": { "type": "string" }
61
+
}
62
+
}
63
+
}
64
+
}
+1208
lexicons/tools/ozone/moderation/defs.json
+1208
lexicons/tools/ozone/moderation/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.defs",
4
+
"defs": {
5
+
"modEventView": {
6
+
"type": "object",
7
+
"required": [
8
+
"id",
9
+
"event",
10
+
"subject",
11
+
"subjectBlobCids",
12
+
"createdBy",
13
+
"createdAt"
14
+
],
15
+
"properties": {
16
+
"id": { "type": "integer" },
17
+
"event": {
18
+
"type": "union",
19
+
"refs": [
20
+
"#modEventTakedown",
21
+
"#modEventReverseTakedown",
22
+
"#modEventComment",
23
+
"#modEventReport",
24
+
"#modEventLabel",
25
+
"#modEventAcknowledge",
26
+
"#modEventEscalate",
27
+
"#modEventMute",
28
+
"#modEventUnmute",
29
+
"#modEventMuteReporter",
30
+
"#modEventUnmuteReporter",
31
+
"#modEventEmail",
32
+
"#modEventResolveAppeal",
33
+
"#modEventDivert",
34
+
"#modEventTag",
35
+
"#accountEvent",
36
+
"#identityEvent",
37
+
"#recordEvent",
38
+
"#modEventPriorityScore",
39
+
"#ageAssuranceEvent",
40
+
"#ageAssuranceOverrideEvent",
41
+
"#revokeAccountCredentialsEvent",
42
+
"#scheduleTakedownEvent",
43
+
"#cancelScheduledTakedownEvent"
44
+
]
45
+
},
46
+
"subject": {
47
+
"type": "union",
48
+
"refs": [
49
+
"com.atproto.admin.defs#repoRef",
50
+
"com.atproto.repo.strongRef",
51
+
"chat.bsky.convo.defs#messageRef"
52
+
]
53
+
},
54
+
"subjectBlobCids": { "type": "array", "items": { "type": "string" } },
55
+
"createdBy": { "type": "string", "format": "did" },
56
+
"createdAt": { "type": "string", "format": "datetime" },
57
+
"creatorHandle": { "type": "string" },
58
+
"subjectHandle": { "type": "string" },
59
+
"modTool": { "type": "ref", "ref": "#modTool" }
60
+
}
61
+
},
62
+
"modEventViewDetail": {
63
+
"type": "object",
64
+
"required": [
65
+
"id",
66
+
"event",
67
+
"subject",
68
+
"subjectBlobs",
69
+
"createdBy",
70
+
"createdAt"
71
+
],
72
+
"properties": {
73
+
"id": { "type": "integer" },
74
+
"event": {
75
+
"type": "union",
76
+
"refs": [
77
+
"#modEventTakedown",
78
+
"#modEventReverseTakedown",
79
+
"#modEventComment",
80
+
"#modEventReport",
81
+
"#modEventLabel",
82
+
"#modEventAcknowledge",
83
+
"#modEventEscalate",
84
+
"#modEventMute",
85
+
"#modEventUnmute",
86
+
"#modEventMuteReporter",
87
+
"#modEventUnmuteReporter",
88
+
"#modEventEmail",
89
+
"#modEventResolveAppeal",
90
+
"#modEventDivert",
91
+
"#modEventTag",
92
+
"#accountEvent",
93
+
"#identityEvent",
94
+
"#recordEvent",
95
+
"#modEventPriorityScore",
96
+
"#ageAssuranceEvent",
97
+
"#ageAssuranceOverrideEvent",
98
+
"#revokeAccountCredentialsEvent",
99
+
"#scheduleTakedownEvent",
100
+
"#cancelScheduledTakedownEvent"
101
+
]
102
+
},
103
+
"subject": {
104
+
"type": "union",
105
+
"refs": [
106
+
"#repoView",
107
+
"#repoViewNotFound",
108
+
"#recordView",
109
+
"#recordViewNotFound"
110
+
]
111
+
},
112
+
"subjectBlobs": {
113
+
"type": "array",
114
+
"items": { "type": "ref", "ref": "#blobView" }
115
+
},
116
+
"createdBy": { "type": "string", "format": "did" },
117
+
"createdAt": { "type": "string", "format": "datetime" },
118
+
"modTool": { "type": "ref", "ref": "#modTool" }
119
+
}
120
+
},
121
+
"subjectStatusView": {
122
+
"type": "object",
123
+
"required": ["id", "subject", "createdAt", "updatedAt", "reviewState"],
124
+
"properties": {
125
+
"id": { "type": "integer" },
126
+
"subject": {
127
+
"type": "union",
128
+
"refs": [
129
+
"com.atproto.admin.defs#repoRef",
130
+
"com.atproto.repo.strongRef",
131
+
"chat.bsky.convo.defs#messageRef"
132
+
]
133
+
},
134
+
"hosting": {
135
+
"type": "union",
136
+
"refs": ["#accountHosting", "#recordHosting"]
137
+
},
138
+
"subjectBlobCids": {
139
+
"type": "array",
140
+
"items": { "type": "string", "format": "cid" }
141
+
},
142
+
"subjectRepoHandle": { "type": "string" },
143
+
"updatedAt": {
144
+
"type": "string",
145
+
"format": "datetime",
146
+
"description": "Timestamp referencing when the last update was made to the moderation status of the subject"
147
+
},
148
+
"createdAt": {
149
+
"type": "string",
150
+
"format": "datetime",
151
+
"description": "Timestamp referencing the first moderation status impacting event was emitted on the subject"
152
+
},
153
+
"reviewState": {
154
+
"type": "ref",
155
+
"ref": "#subjectReviewState"
156
+
},
157
+
"comment": {
158
+
"type": "string",
159
+
"description": "Sticky comment on the subject."
160
+
},
161
+
"priorityScore": {
162
+
"type": "integer",
163
+
"description": "Numeric value representing the level of priority. Higher score means higher priority.",
164
+
"minimum": 0,
165
+
"maximum": 100
166
+
},
167
+
"muteUntil": {
168
+
"type": "string",
169
+
"format": "datetime"
170
+
},
171
+
"muteReportingUntil": {
172
+
"type": "string",
173
+
"format": "datetime"
174
+
},
175
+
"lastReviewedBy": {
176
+
"type": "string",
177
+
"format": "did"
178
+
},
179
+
"lastReviewedAt": {
180
+
"type": "string",
181
+
"format": "datetime"
182
+
},
183
+
"lastReportedAt": {
184
+
"type": "string",
185
+
"format": "datetime"
186
+
},
187
+
"lastAppealedAt": {
188
+
"type": "string",
189
+
"format": "datetime",
190
+
"description": "Timestamp referencing when the author of the subject appealed a moderation action"
191
+
},
192
+
"takendown": {
193
+
"type": "boolean"
194
+
},
195
+
"appealed": {
196
+
"type": "boolean",
197
+
"description": "True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators."
198
+
},
199
+
"suspendUntil": {
200
+
"type": "string",
201
+
"format": "datetime"
202
+
},
203
+
"tags": {
204
+
"type": "array",
205
+
"items": { "type": "string" }
206
+
},
207
+
"accountStats": {
208
+
"description": "Statistics related to the account subject",
209
+
"type": "ref",
210
+
"ref": "#accountStats"
211
+
},
212
+
"recordsStats": {
213
+
"description": "Statistics related to the record subjects authored by the subject's account",
214
+
"type": "ref",
215
+
"ref": "#recordsStats"
216
+
},
217
+
"accountStrike": {
218
+
"description": "Strike information for the account (account-level only)",
219
+
"type": "ref",
220
+
"ref": "#accountStrike"
221
+
},
222
+
"ageAssuranceState": {
223
+
"type": "string",
224
+
"description": "Current age assurance state of the subject.",
225
+
"knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
226
+
},
227
+
"ageAssuranceUpdatedBy": {
228
+
"type": "string",
229
+
"description": "Whether or not the last successful update to age assurance was made by the user or admin.",
230
+
"knownValues": ["admin", "user"]
231
+
}
232
+
}
233
+
},
234
+
"subjectView": {
235
+
"description": "Detailed view of a subject. For record subjects, the author's repo and profile will be returned.",
236
+
"type": "object",
237
+
"required": ["type", "subject"],
238
+
"properties": {
239
+
"type": {
240
+
"type": "ref",
241
+
"ref": "com.atproto.moderation.defs#subjectType"
242
+
},
243
+
"subject": {
244
+
"type": "string"
245
+
},
246
+
"status": {
247
+
"type": "ref",
248
+
"ref": "#subjectStatusView"
249
+
},
250
+
"repo": {
251
+
"type": "ref",
252
+
"ref": "#repoViewDetail"
253
+
},
254
+
"profile": {
255
+
"type": "union",
256
+
"refs": []
257
+
},
258
+
"record": {
259
+
"type": "ref",
260
+
"ref": "#recordViewDetail"
261
+
}
262
+
}
263
+
},
264
+
"accountStats": {
265
+
"description": "Statistics about a particular account subject",
266
+
"type": "object",
267
+
"properties": {
268
+
"reportCount": {
269
+
"description": "Total number of reports on the account",
270
+
"type": "integer"
271
+
},
272
+
"appealCount": {
273
+
"description": "Total number of appeals against a moderation action on the account",
274
+
"type": "integer"
275
+
},
276
+
"suspendCount": {
277
+
"description": "Number of times the account was suspended",
278
+
"type": "integer"
279
+
},
280
+
"escalateCount": {
281
+
"description": "Number of times the account was escalated",
282
+
"type": "integer"
283
+
},
284
+
"takedownCount": {
285
+
"description": "Number of times the account was taken down",
286
+
"type": "integer"
287
+
}
288
+
}
289
+
},
290
+
"recordsStats": {
291
+
"description": "Statistics about a set of record subject items",
292
+
"type": "object",
293
+
"properties": {
294
+
"totalReports": {
295
+
"description": "Cumulative sum of the number of reports on the items in the set",
296
+
"type": "integer"
297
+
},
298
+
"reportedCount": {
299
+
"description": "Number of items that were reported at least once",
300
+
"type": "integer"
301
+
},
302
+
"escalatedCount": {
303
+
"description": "Number of items that were escalated at least once",
304
+
"type": "integer"
305
+
},
306
+
"appealedCount": {
307
+
"description": "Number of items that were appealed at least once",
308
+
"type": "integer"
309
+
},
310
+
"subjectCount": {
311
+
"description": "Total number of item in the set",
312
+
"type": "integer"
313
+
},
314
+
"pendingCount": {
315
+
"description": "Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state",
316
+
"type": "integer"
317
+
},
318
+
"processedCount": {
319
+
"description": "Number of item currently in \"reviewNone\" or \"reviewClosed\" state",
320
+
"type": "integer"
321
+
},
322
+
"takendownCount": {
323
+
"description": "Number of item currently taken down",
324
+
"type": "integer"
325
+
}
326
+
}
327
+
},
328
+
"accountStrike": {
329
+
"description": "Strike information for an account",
330
+
"type": "object",
331
+
"properties": {
332
+
"activeStrikeCount": {
333
+
"description": "Current number of active strikes (excluding expired strikes)",
334
+
"type": "integer"
335
+
},
336
+
"totalStrikeCount": {
337
+
"description": "Total number of strikes ever received (including expired strikes)",
338
+
"type": "integer"
339
+
},
340
+
"firstStrikeAt": {
341
+
"description": "Timestamp of the first strike received",
342
+
"type": "string",
343
+
"format": "datetime"
344
+
},
345
+
"lastStrikeAt": {
346
+
"description": "Timestamp of the most recent strike received",
347
+
"type": "string",
348
+
"format": "datetime"
349
+
}
350
+
}
351
+
},
352
+
"subjectReviewState": {
353
+
"type": "string",
354
+
"knownValues": [
355
+
"tools.ozone.moderation.defs#reviewOpen",
356
+
"tools.ozone.moderation.defs#reviewEscalated",
357
+
"tools.ozone.moderation.defs#reviewClosed",
358
+
"tools.ozone.moderation.defs#reviewNone"
359
+
]
360
+
},
361
+
"reviewOpen": {
362
+
"type": "token",
363
+
"description": "Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator"
364
+
},
365
+
"reviewEscalated": {
366
+
"type": "token",
367
+
"description": "Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator"
368
+
},
369
+
"reviewClosed": {
370
+
"type": "token",
371
+
"description": "Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator"
372
+
},
373
+
"reviewNone": {
374
+
"type": "token",
375
+
"description": "Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it"
376
+
},
377
+
"modEventTakedown": {
378
+
"type": "object",
379
+
"description": "Take down a subject permanently or temporarily",
380
+
"properties": {
381
+
"comment": {
382
+
"type": "string"
383
+
},
384
+
"durationInHours": {
385
+
"type": "integer",
386
+
"description": "Indicates how long the takedown should be in effect before automatically expiring."
387
+
},
388
+
"acknowledgeAccountSubjects": {
389
+
"type": "boolean",
390
+
"description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
391
+
},
392
+
"policies": {
393
+
"type": "array",
394
+
"maxLength": 5,
395
+
"items": { "type": "string" },
396
+
"description": "Names/Keywords of the policies that drove the decision."
397
+
},
398
+
"severityLevel": {
399
+
"type": "string",
400
+
"description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)."
401
+
},
402
+
"targetServices": {
403
+
"type": "array",
404
+
"items": { "type": "string", "knownValues": ["appview", "pds"] },
405
+
"description": "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services."
406
+
},
407
+
"strikeCount": {
408
+
"type": "integer",
409
+
"description": "Number of strikes to assign to the user for this violation."
410
+
},
411
+
"strikeExpiresAt": {
412
+
"type": "string",
413
+
"format": "datetime",
414
+
"description": "When the strike should expire. If not provided, the strike never expires."
415
+
}
416
+
}
417
+
},
418
+
"modEventReverseTakedown": {
419
+
"type": "object",
420
+
"description": "Revert take down action on a subject",
421
+
"properties": {
422
+
"comment": {
423
+
"type": "string",
424
+
"description": "Describe reasoning behind the reversal."
425
+
},
426
+
"policies": {
427
+
"type": "array",
428
+
"maxLength": 5,
429
+
"items": { "type": "string" },
430
+
"description": "Names/Keywords of the policy infraction for which takedown is being reversed."
431
+
},
432
+
"severityLevel": {
433
+
"type": "string",
434
+
"description": "Severity level of the violation. Usually set from the last policy infraction's severity."
435
+
},
436
+
"strikeCount": {
437
+
"type": "integer",
438
+
"description": "Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity."
439
+
}
440
+
}
441
+
},
442
+
"modEventResolveAppeal": {
443
+
"type": "object",
444
+
"description": "Resolve appeal on a subject",
445
+
"properties": {
446
+
"comment": {
447
+
"type": "string",
448
+
"description": "Describe resolution."
449
+
}
450
+
}
451
+
},
452
+
"modEventComment": {
453
+
"type": "object",
454
+
"description": "Add a comment to a subject. An empty comment will clear any previously set sticky comment.",
455
+
"properties": {
456
+
"comment": {
457
+
"type": "string"
458
+
},
459
+
"sticky": {
460
+
"type": "boolean",
461
+
"description": "Make the comment persistent on the subject"
462
+
}
463
+
}
464
+
},
465
+
"modEventReport": {
466
+
"type": "object",
467
+
"description": "Report a subject",
468
+
"required": ["reportType"],
469
+
"properties": {
470
+
"comment": {
471
+
"type": "string"
472
+
},
473
+
"isReporterMuted": {
474
+
"type": "boolean",
475
+
"description": "Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject."
476
+
},
477
+
"reportType": {
478
+
"type": "ref",
479
+
"ref": "com.atproto.moderation.defs#reasonType"
480
+
}
481
+
}
482
+
},
483
+
"modEventLabel": {
484
+
"type": "object",
485
+
"description": "Apply/Negate labels on a subject",
486
+
"required": ["createLabelVals", "negateLabelVals"],
487
+
"properties": {
488
+
"comment": {
489
+
"type": "string"
490
+
},
491
+
"createLabelVals": {
492
+
"type": "array",
493
+
"items": { "type": "string" }
494
+
},
495
+
"negateLabelVals": {
496
+
"type": "array",
497
+
"items": { "type": "string" }
498
+
},
499
+
"durationInHours": {
500
+
"type": "integer",
501
+
"description": "Indicates how long the label will remain on the subject. Only applies on labels that are being added."
502
+
}
503
+
}
504
+
},
505
+
"modEventPriorityScore": {
506
+
"type": "object",
507
+
"description": "Set priority score of the subject. Higher score means higher priority.",
508
+
"required": ["score"],
509
+
"properties": {
510
+
"comment": {
511
+
"type": "string"
512
+
},
513
+
"score": {
514
+
"type": "integer",
515
+
"minimum": 0,
516
+
"maximum": 100
517
+
}
518
+
}
519
+
},
520
+
"ageAssuranceEvent": {
521
+
"type": "object",
522
+
"description": "Age assurance info coming directly from users. Only works on DID subjects.",
523
+
"required": ["createdAt", "status", "attemptId"],
524
+
"properties": {
525
+
"createdAt": {
526
+
"type": "string",
527
+
"format": "datetime",
528
+
"description": "The date and time of this write operation."
529
+
},
530
+
"attemptId": {
531
+
"type": "string",
532
+
"description": "The unique identifier for this instance of the age assurance flow, in UUID format."
533
+
},
534
+
"status": {
535
+
"type": "string",
536
+
"description": "The status of the Age Assurance process.",
537
+
"knownValues": ["unknown", "pending", "assured"]
538
+
},
539
+
"access": {
540
+
"type": "ref",
541
+
"ref": "app.bsky.ageassurance.defs#access"
542
+
},
543
+
"countryCode": {
544
+
"type": "string",
545
+
"description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow."
546
+
},
547
+
"regionCode": {
548
+
"type": "string",
549
+
"description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow."
550
+
},
551
+
"initIp": {
552
+
"type": "string",
553
+
"description": "The IP address used when initiating the AA flow."
554
+
},
555
+
"initUa": {
556
+
"type": "string",
557
+
"description": "The user agent used when initiating the AA flow."
558
+
},
559
+
"completeIp": {
560
+
"type": "string",
561
+
"description": "The IP address used when completing the AA flow."
562
+
},
563
+
"completeUa": {
564
+
"type": "string",
565
+
"description": "The user agent used when completing the AA flow."
566
+
}
567
+
}
568
+
},
569
+
"ageAssuranceOverrideEvent": {
570
+
"type": "object",
571
+
"description": "Age assurance status override by moderators. Only works on DID subjects.",
572
+
"required": ["comment", "status"],
573
+
"properties": {
574
+
"status": {
575
+
"type": "string",
576
+
"description": "The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.",
577
+
"knownValues": ["assured", "reset", "blocked"]
578
+
},
579
+
"access": {
580
+
"type": "ref",
581
+
"ref": "app.bsky.ageassurance.defs#access"
582
+
},
583
+
"comment": {
584
+
"type": "string",
585
+
"minLength": 1,
586
+
"description": "Comment describing the reason for the override."
587
+
}
588
+
}
589
+
},
590
+
"revokeAccountCredentialsEvent": {
591
+
"type": "object",
592
+
"description": "Account credentials revocation by moderators. Only works on DID subjects.",
593
+
"required": ["comment"],
594
+
"properties": {
595
+
"comment": {
596
+
"minLength": 1,
597
+
"type": "string",
598
+
"description": "Comment describing the reason for the revocation."
599
+
}
600
+
}
601
+
},
602
+
"modEventAcknowledge": {
603
+
"type": "object",
604
+
"properties": {
605
+
"comment": { "type": "string" },
606
+
"acknowledgeAccountSubjects": {
607
+
"type": "boolean",
608
+
"description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
609
+
}
610
+
}
611
+
},
612
+
"modEventEscalate": {
613
+
"type": "object",
614
+
"properties": {
615
+
"comment": { "type": "string" }
616
+
}
617
+
},
618
+
"modEventMute": {
619
+
"type": "object",
620
+
"description": "Mute incoming reports on a subject",
621
+
"required": ["durationInHours"],
622
+
"properties": {
623
+
"comment": { "type": "string" },
624
+
"durationInHours": {
625
+
"type": "integer",
626
+
"description": "Indicates how long the subject should remain muted."
627
+
}
628
+
}
629
+
},
630
+
"modEventUnmute": {
631
+
"type": "object",
632
+
"description": "Unmute action on a subject",
633
+
"properties": {
634
+
"comment": {
635
+
"type": "string",
636
+
"description": "Describe reasoning behind the reversal."
637
+
}
638
+
}
639
+
},
640
+
"modEventMuteReporter": {
641
+
"type": "object",
642
+
"description": "Mute incoming reports from an account",
643
+
"properties": {
644
+
"comment": { "type": "string" },
645
+
"durationInHours": {
646
+
"type": "integer",
647
+
"description": "Indicates how long the account should remain muted. Falsy value here means a permanent mute."
648
+
}
649
+
}
650
+
},
651
+
"modEventUnmuteReporter": {
652
+
"type": "object",
653
+
"description": "Unmute incoming reports from an account",
654
+
"properties": {
655
+
"comment": {
656
+
"type": "string",
657
+
"description": "Describe reasoning behind the reversal."
658
+
}
659
+
}
660
+
},
661
+
"modEventEmail": {
662
+
"type": "object",
663
+
"description": "Keep a log of outgoing email to a user",
664
+
"required": ["subjectLine"],
665
+
"properties": {
666
+
"subjectLine": {
667
+
"type": "string",
668
+
"description": "The subject line of the email sent to the user."
669
+
},
670
+
"content": {
671
+
"type": "string",
672
+
"description": "The content of the email sent to the user."
673
+
},
674
+
"comment": {
675
+
"type": "string",
676
+
"description": "Additional comment about the outgoing comm."
677
+
},
678
+
"policies": {
679
+
"type": "array",
680
+
"maxLength": 5,
681
+
"items": { "type": "string" },
682
+
"description": "Names/Keywords of the policies that necessitated the email."
683
+
},
684
+
"severityLevel": {
685
+
"type": "string",
686
+
"description": "Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense"
687
+
},
688
+
"strikeCount": {
689
+
"type": "integer",
690
+
"description": "Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense."
691
+
},
692
+
"strikeExpiresAt": {
693
+
"type": "string",
694
+
"format": "datetime",
695
+
"description": "When the strike should expire. If not provided, the strike never expires."
696
+
},
697
+
"isDelivered": {
698
+
"type": "boolean",
699
+
"description": "Indicates whether the email was successfully delivered to the user's inbox."
700
+
}
701
+
}
702
+
},
703
+
"modEventDivert": {
704
+
"type": "object",
705
+
"description": "Divert a record's blobs to a 3rd party service for further scanning/tagging",
706
+
"properties": {
707
+
"comment": { "type": "string" }
708
+
}
709
+
},
710
+
"modEventTag": {
711
+
"type": "object",
712
+
"description": "Add/Remove a tag on a subject",
713
+
"required": ["add", "remove"],
714
+
"properties": {
715
+
"add": {
716
+
"type": "array",
717
+
"items": { "type": "string" },
718
+
"description": "Tags to be added to the subject. If already exists, won't be duplicated."
719
+
},
720
+
"remove": {
721
+
"type": "array",
722
+
"items": { "type": "string" },
723
+
"description": "Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated."
724
+
},
725
+
"comment": {
726
+
"type": "string",
727
+
"description": "Additional comment about added/removed tags."
728
+
}
729
+
}
730
+
},
731
+
"accountEvent": {
732
+
"type": "object",
733
+
"description": "Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
734
+
"required": ["timestamp", "active"],
735
+
"properties": {
736
+
"comment": { "type": "string" },
737
+
"active": {
738
+
"type": "boolean",
739
+
"description": "Indicates that the account has a repository which can be fetched from the host that emitted this event."
740
+
},
741
+
"status": {
742
+
"type": "string",
743
+
"knownValues": [
744
+
"unknown",
745
+
"deactivated",
746
+
"deleted",
747
+
"takendown",
748
+
"suspended",
749
+
"tombstoned"
750
+
]
751
+
},
752
+
"timestamp": {
753
+
"type": "string",
754
+
"format": "datetime"
755
+
}
756
+
}
757
+
},
758
+
"identityEvent": {
759
+
"type": "object",
760
+
"description": "Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
761
+
"required": ["timestamp"],
762
+
"properties": {
763
+
"comment": { "type": "string" },
764
+
"handle": { "type": "string", "format": "handle" },
765
+
"pdsHost": { "type": "string", "format": "uri" },
766
+
"tombstone": { "type": "boolean" },
767
+
"timestamp": {
768
+
"type": "string",
769
+
"format": "datetime"
770
+
}
771
+
}
772
+
},
773
+
"recordEvent": {
774
+
"type": "object",
775
+
"description": "Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
776
+
"required": ["timestamp", "op"],
777
+
"properties": {
778
+
"comment": { "type": "string" },
779
+
"op": {
780
+
"type": "string",
781
+
"knownValues": ["create", "update", "delete"]
782
+
},
783
+
"cid": { "type": "string", "format": "cid" },
784
+
"timestamp": { "type": "string", "format": "datetime" }
785
+
}
786
+
},
787
+
"scheduleTakedownEvent": {
788
+
"type": "object",
789
+
"description": "Logs a scheduled takedown action for an account.",
790
+
"properties": {
791
+
"comment": { "type": "string" },
792
+
"executeAt": { "type": "string", "format": "datetime" },
793
+
"executeAfter": { "type": "string", "format": "datetime" },
794
+
"executeUntil": { "type": "string", "format": "datetime" }
795
+
}
796
+
},
797
+
"cancelScheduledTakedownEvent": {
798
+
"type": "object",
799
+
"description": "Logs cancellation of a scheduled takedown action for an account.",
800
+
"properties": {
801
+
"comment": { "type": "string" }
802
+
}
803
+
},
804
+
"repoView": {
805
+
"type": "object",
806
+
"required": [
807
+
"did",
808
+
"handle",
809
+
"relatedRecords",
810
+
"indexedAt",
811
+
"moderation"
812
+
],
813
+
"properties": {
814
+
"did": { "type": "string", "format": "did" },
815
+
"handle": { "type": "string", "format": "handle" },
816
+
"email": { "type": "string" },
817
+
"relatedRecords": { "type": "array", "items": { "type": "unknown" } },
818
+
"indexedAt": { "type": "string", "format": "datetime" },
819
+
"moderation": { "type": "ref", "ref": "#moderation" },
820
+
"invitedBy": {
821
+
"type": "ref",
822
+
"ref": "com.atproto.server.defs#inviteCode"
823
+
},
824
+
"invitesDisabled": { "type": "boolean" },
825
+
"inviteNote": { "type": "string" },
826
+
"deactivatedAt": { "type": "string", "format": "datetime" },
827
+
"threatSignatures": {
828
+
"type": "array",
829
+
"items": {
830
+
"type": "ref",
831
+
"ref": "com.atproto.admin.defs#threatSignature"
832
+
}
833
+
}
834
+
}
835
+
},
836
+
"repoViewDetail": {
837
+
"type": "object",
838
+
"required": [
839
+
"did",
840
+
"handle",
841
+
"relatedRecords",
842
+
"indexedAt",
843
+
"moderation"
844
+
],
845
+
"properties": {
846
+
"did": { "type": "string", "format": "did" },
847
+
"handle": { "type": "string", "format": "handle" },
848
+
"email": { "type": "string" },
849
+
"relatedRecords": { "type": "array", "items": { "type": "unknown" } },
850
+
"indexedAt": { "type": "string", "format": "datetime" },
851
+
"moderation": { "type": "ref", "ref": "#moderationDetail" },
852
+
"labels": {
853
+
"type": "array",
854
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
855
+
},
856
+
"invitedBy": {
857
+
"type": "ref",
858
+
"ref": "com.atproto.server.defs#inviteCode"
859
+
},
860
+
"invites": {
861
+
"type": "array",
862
+
"items": {
863
+
"type": "ref",
864
+
"ref": "com.atproto.server.defs#inviteCode"
865
+
}
866
+
},
867
+
"invitesDisabled": { "type": "boolean" },
868
+
"inviteNote": { "type": "string" },
869
+
"emailConfirmedAt": { "type": "string", "format": "datetime" },
870
+
"deactivatedAt": { "type": "string", "format": "datetime" },
871
+
"threatSignatures": {
872
+
"type": "array",
873
+
"items": {
874
+
"type": "ref",
875
+
"ref": "com.atproto.admin.defs#threatSignature"
876
+
}
877
+
}
878
+
}
879
+
},
880
+
"repoViewNotFound": {
881
+
"type": "object",
882
+
"required": ["did"],
883
+
"properties": {
884
+
"did": { "type": "string", "format": "did" }
885
+
}
886
+
},
887
+
"recordView": {
888
+
"type": "object",
889
+
"required": [
890
+
"uri",
891
+
"cid",
892
+
"value",
893
+
"blobCids",
894
+
"indexedAt",
895
+
"moderation",
896
+
"repo"
897
+
],
898
+
"properties": {
899
+
"uri": { "type": "string", "format": "at-uri" },
900
+
"cid": { "type": "string", "format": "cid" },
901
+
"value": { "type": "unknown" },
902
+
"blobCids": {
903
+
"type": "array",
904
+
"items": { "type": "string", "format": "cid" }
905
+
},
906
+
"indexedAt": { "type": "string", "format": "datetime" },
907
+
"moderation": { "type": "ref", "ref": "#moderation" },
908
+
"repo": { "type": "ref", "ref": "#repoView" }
909
+
}
910
+
},
911
+
"recordViewDetail": {
912
+
"type": "object",
913
+
"required": [
914
+
"uri",
915
+
"cid",
916
+
"value",
917
+
"blobs",
918
+
"indexedAt",
919
+
"moderation",
920
+
"repo"
921
+
],
922
+
"properties": {
923
+
"uri": { "type": "string", "format": "at-uri" },
924
+
"cid": { "type": "string", "format": "cid" },
925
+
"value": { "type": "unknown" },
926
+
"blobs": {
927
+
"type": "array",
928
+
"items": { "type": "ref", "ref": "#blobView" }
929
+
},
930
+
"labels": {
931
+
"type": "array",
932
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
933
+
},
934
+
"indexedAt": { "type": "string", "format": "datetime" },
935
+
"moderation": { "type": "ref", "ref": "#moderationDetail" },
936
+
"repo": { "type": "ref", "ref": "#repoView" }
937
+
}
938
+
},
939
+
"recordViewNotFound": {
940
+
"type": "object",
941
+
"required": ["uri"],
942
+
"properties": {
943
+
"uri": { "type": "string", "format": "at-uri" }
944
+
}
945
+
},
946
+
"moderation": {
947
+
"type": "object",
948
+
"properties": {
949
+
"subjectStatus": { "type": "ref", "ref": "#subjectStatusView" }
950
+
}
951
+
},
952
+
"moderationDetail": {
953
+
"type": "object",
954
+
"properties": {
955
+
"subjectStatus": {
956
+
"type": "ref",
957
+
"ref": "#subjectStatusView"
958
+
}
959
+
}
960
+
},
961
+
"blobView": {
962
+
"type": "object",
963
+
"required": ["cid", "mimeType", "size", "createdAt"],
964
+
"properties": {
965
+
"cid": { "type": "string", "format": "cid" },
966
+
"mimeType": { "type": "string" },
967
+
"size": { "type": "integer" },
968
+
"createdAt": { "type": "string", "format": "datetime" },
969
+
"details": {
970
+
"type": "union",
971
+
"refs": ["#imageDetails", "#videoDetails"]
972
+
},
973
+
"moderation": { "type": "ref", "ref": "#moderation" }
974
+
}
975
+
},
976
+
"imageDetails": {
977
+
"type": "object",
978
+
"required": ["width", "height"],
979
+
"properties": {
980
+
"width": { "type": "integer" },
981
+
"height": { "type": "integer" }
982
+
}
983
+
},
984
+
"videoDetails": {
985
+
"type": "object",
986
+
"required": ["width", "height", "length"],
987
+
"properties": {
988
+
"width": { "type": "integer" },
989
+
"height": { "type": "integer" },
990
+
"length": { "type": "integer" }
991
+
}
992
+
},
993
+
"accountHosting": {
994
+
"type": "object",
995
+
"required": ["status"],
996
+
"properties": {
997
+
"status": {
998
+
"type": "string",
999
+
"knownValues": [
1000
+
"takendown",
1001
+
"suspended",
1002
+
"deleted",
1003
+
"deactivated",
1004
+
"unknown"
1005
+
]
1006
+
},
1007
+
"updatedAt": {
1008
+
"type": "string",
1009
+
"format": "datetime"
1010
+
},
1011
+
"createdAt": {
1012
+
"type": "string",
1013
+
"format": "datetime"
1014
+
},
1015
+
"deletedAt": {
1016
+
"type": "string",
1017
+
"format": "datetime"
1018
+
},
1019
+
"deactivatedAt": {
1020
+
"type": "string",
1021
+
"format": "datetime"
1022
+
},
1023
+
"reactivatedAt": {
1024
+
"type": "string",
1025
+
"format": "datetime"
1026
+
}
1027
+
}
1028
+
},
1029
+
"recordHosting": {
1030
+
"type": "object",
1031
+
"required": ["status"],
1032
+
"properties": {
1033
+
"status": {
1034
+
"type": "string",
1035
+
"knownValues": ["deleted", "unknown"]
1036
+
},
1037
+
"updatedAt": {
1038
+
"type": "string",
1039
+
"format": "datetime"
1040
+
},
1041
+
"createdAt": {
1042
+
"type": "string",
1043
+
"format": "datetime"
1044
+
},
1045
+
"deletedAt": {
1046
+
"type": "string",
1047
+
"format": "datetime"
1048
+
}
1049
+
}
1050
+
},
1051
+
"reporterStats": {
1052
+
"type": "object",
1053
+
"required": [
1054
+
"did",
1055
+
"accountReportCount",
1056
+
"recordReportCount",
1057
+
"reportedAccountCount",
1058
+
"reportedRecordCount",
1059
+
"takendownAccountCount",
1060
+
"takendownRecordCount",
1061
+
"labeledAccountCount",
1062
+
"labeledRecordCount"
1063
+
],
1064
+
"properties": {
1065
+
"did": {
1066
+
"type": "string",
1067
+
"format": "did"
1068
+
},
1069
+
"accountReportCount": {
1070
+
"type": "integer",
1071
+
"description": "The total number of reports made by the user on accounts."
1072
+
},
1073
+
"recordReportCount": {
1074
+
"type": "integer",
1075
+
"description": "The total number of reports made by the user on records."
1076
+
},
1077
+
"reportedAccountCount": {
1078
+
"type": "integer",
1079
+
"description": "The total number of accounts reported by the user."
1080
+
},
1081
+
"reportedRecordCount": {
1082
+
"type": "integer",
1083
+
"description": "The total number of records reported by the user."
1084
+
},
1085
+
"takendownAccountCount": {
1086
+
"type": "integer",
1087
+
"description": "The total number of accounts taken down as a result of the user's reports."
1088
+
},
1089
+
"takendownRecordCount": {
1090
+
"type": "integer",
1091
+
"description": "The total number of records taken down as a result of the user's reports."
1092
+
},
1093
+
"labeledAccountCount": {
1094
+
"type": "integer",
1095
+
"description": "The total number of accounts labeled as a result of the user's reports."
1096
+
},
1097
+
"labeledRecordCount": {
1098
+
"type": "integer",
1099
+
"description": "The total number of records labeled as a result of the user's reports."
1100
+
}
1101
+
}
1102
+
},
1103
+
"modTool": {
1104
+
"type": "object",
1105
+
"description": "Moderation tool information for tracing the source of the action",
1106
+
"required": ["name"],
1107
+
"properties": {
1108
+
"name": {
1109
+
"type": "string",
1110
+
"description": "Name/identifier of the source (e.g., 'automod', 'ozone/workspace')"
1111
+
},
1112
+
"meta": {
1113
+
"type": "unknown",
1114
+
"description": "Additional arbitrary metadata about the source"
1115
+
}
1116
+
}
1117
+
},
1118
+
"timelineEventPlcCreate": {
1119
+
"type": "token",
1120
+
"description": "Moderation event timeline event for a PLC create operation"
1121
+
},
1122
+
"timelineEventPlcOperation": {
1123
+
"type": "token",
1124
+
"description": "Moderation event timeline event for generic PLC operation"
1125
+
},
1126
+
"timelineEventPlcTombstone": {
1127
+
"type": "token",
1128
+
"description": "Moderation event timeline event for a PLC tombstone operation"
1129
+
},
1130
+
"scheduledActionView": {
1131
+
"type": "object",
1132
+
"description": "View of a scheduled moderation action",
1133
+
"required": ["id", "action", "did", "createdBy", "createdAt", "status"],
1134
+
"properties": {
1135
+
"id": {
1136
+
"type": "integer",
1137
+
"description": "Auto-incrementing row ID"
1138
+
},
1139
+
"action": {
1140
+
"type": "string",
1141
+
"knownValues": ["takedown"],
1142
+
"description": "Type of action to be executed"
1143
+
},
1144
+
"eventData": {
1145
+
"type": "unknown",
1146
+
"description": "Serialized event object that will be propagated to the event when performed"
1147
+
},
1148
+
"did": {
1149
+
"type": "string",
1150
+
"format": "did",
1151
+
"description": "Subject DID for the action"
1152
+
},
1153
+
"executeAt": {
1154
+
"type": "string",
1155
+
"format": "datetime",
1156
+
"description": "Exact time to execute the action"
1157
+
},
1158
+
"executeAfter": {
1159
+
"type": "string",
1160
+
"format": "datetime",
1161
+
"description": "Earliest time to execute the action (for randomized scheduling)"
1162
+
},
1163
+
"executeUntil": {
1164
+
"type": "string",
1165
+
"format": "datetime",
1166
+
"description": "Latest time to execute the action (for randomized scheduling)"
1167
+
},
1168
+
"randomizeExecution": {
1169
+
"type": "boolean",
1170
+
"description": "Whether execution time should be randomized within the specified range"
1171
+
},
1172
+
"createdBy": {
1173
+
"type": "string",
1174
+
"format": "did",
1175
+
"description": "DID of the user who created this scheduled action"
1176
+
},
1177
+
"createdAt": {
1178
+
"type": "string",
1179
+
"format": "datetime",
1180
+
"description": "When the scheduled action was created"
1181
+
},
1182
+
"updatedAt": {
1183
+
"type": "string",
1184
+
"format": "datetime",
1185
+
"description": "When the scheduled action was last updated"
1186
+
},
1187
+
"status": {
1188
+
"type": "string",
1189
+
"knownValues": ["pending", "executed", "cancelled", "failed"],
1190
+
"description": "Current status of the scheduled action"
1191
+
},
1192
+
"lastExecutedAt": {
1193
+
"type": "string",
1194
+
"format": "datetime",
1195
+
"description": "When the action was last attempted to be executed"
1196
+
},
1197
+
"lastFailureReason": {
1198
+
"type": "string",
1199
+
"description": "Reason for the last execution failure"
1200
+
},
1201
+
"executionEventId": {
1202
+
"type": "integer",
1203
+
"description": "ID of the moderation event created when action was successfully executed"
1204
+
}
1205
+
}
1206
+
}
1207
+
}
1208
+
}
+90
lexicons/tools/ozone/moderation/emitEvent.json
+90
lexicons/tools/ozone/moderation/emitEvent.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.emitEvent",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Take a moderation action on an actor.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["event", "subject", "createdBy"],
13
+
"properties": {
14
+
"event": {
15
+
"type": "union",
16
+
"refs": [
17
+
"tools.ozone.moderation.defs#modEventTakedown",
18
+
"tools.ozone.moderation.defs#modEventAcknowledge",
19
+
"tools.ozone.moderation.defs#modEventEscalate",
20
+
"tools.ozone.moderation.defs#modEventComment",
21
+
"tools.ozone.moderation.defs#modEventLabel",
22
+
"tools.ozone.moderation.defs#modEventReport",
23
+
"tools.ozone.moderation.defs#modEventMute",
24
+
"tools.ozone.moderation.defs#modEventUnmute",
25
+
"tools.ozone.moderation.defs#modEventMuteReporter",
26
+
"tools.ozone.moderation.defs#modEventUnmuteReporter",
27
+
"tools.ozone.moderation.defs#modEventReverseTakedown",
28
+
"tools.ozone.moderation.defs#modEventResolveAppeal",
29
+
"tools.ozone.moderation.defs#modEventEmail",
30
+
"tools.ozone.moderation.defs#modEventDivert",
31
+
"tools.ozone.moderation.defs#modEventTag",
32
+
"tools.ozone.moderation.defs#accountEvent",
33
+
"tools.ozone.moderation.defs#identityEvent",
34
+
"tools.ozone.moderation.defs#recordEvent",
35
+
"tools.ozone.moderation.defs#modEventPriorityScore",
36
+
"tools.ozone.moderation.defs#ageAssuranceEvent",
37
+
"tools.ozone.moderation.defs#ageAssuranceOverrideEvent",
38
+
"tools.ozone.moderation.defs#revokeAccountCredentialsEvent",
39
+
"tools.ozone.moderation.defs#scheduleTakedownEvent",
40
+
"tools.ozone.moderation.defs#cancelScheduledTakedownEvent"
41
+
]
42
+
},
43
+
"subject": {
44
+
"type": "union",
45
+
"refs": [
46
+
"com.atproto.admin.defs#repoRef",
47
+
"com.atproto.repo.strongRef"
48
+
]
49
+
},
50
+
"subjectBlobCids": {
51
+
"type": "array",
52
+
"items": {
53
+
"type": "string",
54
+
"format": "cid"
55
+
}
56
+
},
57
+
"createdBy": {
58
+
"type": "string",
59
+
"format": "did"
60
+
},
61
+
"modTool": {
62
+
"type": "ref",
63
+
"ref": "tools.ozone.moderation.defs#modTool"
64
+
},
65
+
"externalId": {
66
+
"type": "string",
67
+
"description": "An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject."
68
+
}
69
+
}
70
+
}
71
+
},
72
+
"output": {
73
+
"encoding": "application/json",
74
+
"schema": {
75
+
"type": "ref",
76
+
"ref": "tools.ozone.moderation.defs#modEventView"
77
+
}
78
+
},
79
+
"errors": [
80
+
{
81
+
"name": "SubjectHasAction"
82
+
},
83
+
{
84
+
"name": "DuplicateExternalId",
85
+
"description": "An event with the same external ID already exists for the subject."
86
+
}
87
+
]
88
+
}
89
+
}
90
+
}
+106
lexicons/tools/ozone/moderation/getAccountTimeline.json
+106
lexicons/tools/ozone/moderation/getAccountTimeline.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getAccountTimeline",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get timeline of all available events of an account. This includes moderation events, account history and did history.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did"
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["timeline"],
23
+
"properties": {
24
+
"timeline": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "ref",
28
+
"ref": "#timelineItem"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
},
34
+
"errors": [
35
+
{
36
+
"name": "RepoNotFound"
37
+
}
38
+
]
39
+
},
40
+
"timelineItem": {
41
+
"type": "object",
42
+
"required": ["day", "summary"],
43
+
"properties": {
44
+
"day": {
45
+
"type": "string"
46
+
},
47
+
"summary": {
48
+
"type": "array",
49
+
"items": {
50
+
"type": "ref",
51
+
"ref": "#timelineItemSummary"
52
+
}
53
+
}
54
+
}
55
+
},
56
+
"timelineItemSummary": {
57
+
"type": "object",
58
+
"required": ["eventSubjectType", "eventType", "count"],
59
+
"properties": {
60
+
"eventSubjectType": {
61
+
"type": "string",
62
+
"knownValues": ["account", "record", "chat"]
63
+
},
64
+
"eventType": {
65
+
"type": "string",
66
+
"knownValues": [
67
+
"tools.ozone.moderation.defs#modEventTakedown",
68
+
"tools.ozone.moderation.defs#modEventReverseTakedown",
69
+
"tools.ozone.moderation.defs#modEventComment",
70
+
"tools.ozone.moderation.defs#modEventReport",
71
+
"tools.ozone.moderation.defs#modEventLabel",
72
+
"tools.ozone.moderation.defs#modEventAcknowledge",
73
+
"tools.ozone.moderation.defs#modEventEscalate",
74
+
"tools.ozone.moderation.defs#modEventMute",
75
+
"tools.ozone.moderation.defs#modEventUnmute",
76
+
"tools.ozone.moderation.defs#modEventMuteReporter",
77
+
"tools.ozone.moderation.defs#modEventUnmuteReporter",
78
+
"tools.ozone.moderation.defs#modEventEmail",
79
+
"tools.ozone.moderation.defs#modEventResolveAppeal",
80
+
"tools.ozone.moderation.defs#modEventDivert",
81
+
"tools.ozone.moderation.defs#modEventTag",
82
+
"tools.ozone.moderation.defs#accountEvent",
83
+
"tools.ozone.moderation.defs#identityEvent",
84
+
"tools.ozone.moderation.defs#recordEvent",
85
+
"tools.ozone.moderation.defs#modEventPriorityScore",
86
+
"tools.ozone.moderation.defs#revokeAccountCredentialsEvent",
87
+
"tools.ozone.moderation.defs#ageAssuranceEvent",
88
+
"tools.ozone.moderation.defs#ageAssuranceOverrideEvent",
89
+
"tools.ozone.moderation.defs#timelineEventPlcCreate",
90
+
"tools.ozone.moderation.defs#timelineEventPlcOperation",
91
+
"tools.ozone.moderation.defs#timelineEventPlcTombstone",
92
+
"tools.ozone.hosting.getAccountHistory#accountCreated",
93
+
"tools.ozone.hosting.getAccountHistory#emailConfirmed",
94
+
"tools.ozone.hosting.getAccountHistory#passwordUpdated",
95
+
"tools.ozone.hosting.getAccountHistory#handleUpdated",
96
+
"tools.ozone.moderation.defs#scheduleTakedownEvent",
97
+
"tools.ozone.moderation.defs#cancelScheduledTakedownEvent"
98
+
]
99
+
},
100
+
"count": {
101
+
"type": "integer"
102
+
}
103
+
}
104
+
}
105
+
}
106
+
}
+24
lexicons/tools/ozone/moderation/getEvent.json
+24
lexicons/tools/ozone/moderation/getEvent.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getEvent",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about a moderation event.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["id"],
11
+
"properties": {
12
+
"id": { "type": "integer" }
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "ref",
19
+
"ref": "tools.ozone.moderation.defs#modEventViewDetail"
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+26
lexicons/tools/ozone/moderation/getRecord.json
+26
lexicons/tools/ozone/moderation/getRecord.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getRecord",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about a record.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uri"],
11
+
"properties": {
12
+
"uri": { "type": "string", "format": "at-uri" },
13
+
"cid": { "type": "string", "format": "cid" }
14
+
}
15
+
},
16
+
"output": {
17
+
"encoding": "application/json",
18
+
"schema": {
19
+
"type": "ref",
20
+
"ref": "tools.ozone.moderation.defs#recordViewDetail"
21
+
}
22
+
},
23
+
"errors": [{ "name": "RecordNotFound" }]
24
+
}
25
+
}
26
+
}
+43
lexicons/tools/ozone/moderation/getRecords.json
+43
lexicons/tools/ozone/moderation/getRecords.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getRecords",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about some records.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["uris"],
11
+
"properties": {
12
+
"uris": {
13
+
"type": "array",
14
+
"maxLength": 100,
15
+
"items": {
16
+
"type": "string",
17
+
"format": "at-uri"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["records"],
27
+
"properties": {
28
+
"records": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "union",
32
+
"refs": [
33
+
"tools.ozone.moderation.defs#recordViewDetail",
34
+
"tools.ozone.moderation.defs#recordViewNotFound"
35
+
]
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+25
lexicons/tools/ozone/moderation/getRepo.json
+25
lexicons/tools/ozone/moderation/getRepo.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getRepo",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about a repository.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": { "type": "string", "format": "did" }
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "ref",
19
+
"ref": "tools.ozone.moderation.defs#repoViewDetail"
20
+
}
21
+
},
22
+
"errors": [{ "name": "RepoNotFound" }]
23
+
}
24
+
}
25
+
}
+40
lexicons/tools/ozone/moderation/getReporterStats.json
+40
lexicons/tools/ozone/moderation/getReporterStats.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getReporterStats",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get reporter stats for a list of users.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["dids"],
11
+
"properties": {
12
+
"dids": {
13
+
"type": "array",
14
+
"maxLength": 100,
15
+
"items": {
16
+
"type": "string",
17
+
"format": "did"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["stats"],
27
+
"properties": {
28
+
"stats": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "ref",
32
+
"ref": "tools.ozone.moderation.defs#reporterStats"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
+43
lexicons/tools/ozone/moderation/getRepos.json
+43
lexicons/tools/ozone/moderation/getRepos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getRepos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about some repositories.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["dids"],
11
+
"properties": {
12
+
"dids": {
13
+
"type": "array",
14
+
"maxLength": 100,
15
+
"items": {
16
+
"type": "string",
17
+
"format": "did"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["repos"],
27
+
"properties": {
28
+
"repos": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "union",
32
+
"refs": [
33
+
"tools.ozone.moderation.defs#repoViewDetail",
34
+
"tools.ozone.moderation.defs#repoViewNotFound"
35
+
]
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
+40
lexicons/tools/ozone/moderation/getSubjects.json
+40
lexicons/tools/ozone/moderation/getSubjects.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.getSubjects",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about subjects.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["subjects"],
11
+
"properties": {
12
+
"subjects": {
13
+
"type": "array",
14
+
"maxLength": 100,
15
+
"minLength": 1,
16
+
"items": {
17
+
"type": "string"
18
+
}
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["subjects"],
27
+
"properties": {
28
+
"subjects": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "ref",
32
+
"ref": "tools.ozone.moderation.defs#subjectView"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
+78
lexicons/tools/ozone/moderation/listScheduledActions.json
+78
lexicons/tools/ozone/moderation/listScheduledActions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.listScheduledActions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "List scheduled moderation actions with optional filtering",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["statuses"],
13
+
"properties": {
14
+
"startsAfter": {
15
+
"type": "string",
16
+
"format": "datetime",
17
+
"description": "Filter actions scheduled to execute after this time"
18
+
},
19
+
"endsBefore": {
20
+
"type": "string",
21
+
"format": "datetime",
22
+
"description": "Filter actions scheduled to execute before this time"
23
+
},
24
+
"subjects": {
25
+
"type": "array",
26
+
"maxLength": 100,
27
+
"items": {
28
+
"type": "string",
29
+
"format": "did"
30
+
},
31
+
"description": "Filter actions for specific DID subjects"
32
+
},
33
+
"statuses": {
34
+
"type": "array",
35
+
"minLength": 1,
36
+
"items": {
37
+
"type": "string",
38
+
"knownValues": ["pending", "executed", "cancelled", "failed"]
39
+
},
40
+
"description": "Filter actions by status"
41
+
},
42
+
"limit": {
43
+
"type": "integer",
44
+
"minimum": 1,
45
+
"maximum": 100,
46
+
"default": 50,
47
+
"description": "Maximum number of results to return"
48
+
},
49
+
"cursor": {
50
+
"type": "string",
51
+
"description": "Cursor for pagination"
52
+
}
53
+
}
54
+
}
55
+
},
56
+
"output": {
57
+
"encoding": "application/json",
58
+
"schema": {
59
+
"type": "object",
60
+
"required": ["actions"],
61
+
"properties": {
62
+
"actions": {
63
+
"type": "array",
64
+
"items": {
65
+
"type": "ref",
66
+
"ref": "tools.ozone.moderation.defs#scheduledActionView"
67
+
}
68
+
},
69
+
"cursor": {
70
+
"type": "string",
71
+
"description": "Cursor for next page of results"
72
+
}
73
+
}
74
+
}
75
+
}
76
+
}
77
+
}
78
+
}
+162
lexicons/tools/ozone/moderation/queryEvents.json
+162
lexicons/tools/ozone/moderation/queryEvents.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.queryEvents",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List moderation events related to a subject.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"types": {
12
+
"type": "array",
13
+
"items": {
14
+
"type": "string"
15
+
},
16
+
"description": "The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned."
17
+
},
18
+
"createdBy": {
19
+
"type": "string",
20
+
"format": "did"
21
+
},
22
+
"sortDirection": {
23
+
"type": "string",
24
+
"default": "desc",
25
+
"enum": ["asc", "desc"],
26
+
"description": "Sort direction for the events. Defaults to descending order of created at timestamp."
27
+
},
28
+
"createdAfter": {
29
+
"type": "string",
30
+
"format": "datetime",
31
+
"description": "Retrieve events created after a given timestamp"
32
+
},
33
+
"createdBefore": {
34
+
"type": "string",
35
+
"format": "datetime",
36
+
"description": "Retrieve events created before a given timestamp"
37
+
},
38
+
"subject": {
39
+
"type": "string",
40
+
"format": "uri"
41
+
},
42
+
"collections": {
43
+
"type": "array",
44
+
"maxLength": 20,
45
+
"description": "If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.",
46
+
"items": {
47
+
"type": "string",
48
+
"format": "nsid"
49
+
}
50
+
},
51
+
"subjectType": {
52
+
"type": "string",
53
+
"description": "If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.",
54
+
"knownValues": ["account", "record"]
55
+
},
56
+
"includeAllUserRecords": {
57
+
"type": "boolean",
58
+
"default": false,
59
+
"description": "If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned."
60
+
},
61
+
"limit": {
62
+
"type": "integer",
63
+
"minimum": 1,
64
+
"maximum": 100,
65
+
"default": 50
66
+
},
67
+
"hasComment": {
68
+
"type": "boolean",
69
+
"description": "If true, only events with comments are returned"
70
+
},
71
+
"comment": {
72
+
"type": "string",
73
+
"description": "If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition."
74
+
},
75
+
"addedLabels": {
76
+
"type": "array",
77
+
"items": {
78
+
"type": "string"
79
+
},
80
+
"description": "If specified, only events where all of these labels were added are returned"
81
+
},
82
+
"removedLabels": {
83
+
"type": "array",
84
+
"items": {
85
+
"type": "string"
86
+
},
87
+
"description": "If specified, only events where all of these labels were removed are returned"
88
+
},
89
+
"addedTags": {
90
+
"type": "array",
91
+
"items": {
92
+
"type": "string"
93
+
},
94
+
"description": "If specified, only events where all of these tags were added are returned"
95
+
},
96
+
"removedTags": {
97
+
"type": "array",
98
+
"items": {
99
+
"type": "string"
100
+
},
101
+
"description": "If specified, only events where all of these tags were removed are returned"
102
+
},
103
+
"reportTypes": {
104
+
"type": "array",
105
+
"items": {
106
+
"type": "string"
107
+
}
108
+
},
109
+
"policies": {
110
+
"type": "array",
111
+
"items": {
112
+
"type": "string",
113
+
"description": "If specified, only events where the action policies match any of the given policies are returned"
114
+
}
115
+
},
116
+
"modTool": {
117
+
"type": "array",
118
+
"items": {
119
+
"type": "string"
120
+
},
121
+
"description": "If specified, only events where the modTool name matches any of the given values are returned"
122
+
},
123
+
"batchId": {
124
+
"type": "string",
125
+
"description": "If specified, only events where the batchId matches the given value are returned"
126
+
},
127
+
"ageAssuranceState": {
128
+
"type": "string",
129
+
"description": "If specified, only events where the age assurance state matches the given value are returned",
130
+
"knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
131
+
},
132
+
"withStrike": {
133
+
"type": "boolean",
134
+
"description": "If specified, only events where strikeCount value is set are returned."
135
+
},
136
+
"cursor": {
137
+
"type": "string"
138
+
}
139
+
}
140
+
},
141
+
"output": {
142
+
"encoding": "application/json",
143
+
"schema": {
144
+
"type": "object",
145
+
"required": ["events"],
146
+
"properties": {
147
+
"cursor": {
148
+
"type": "string"
149
+
},
150
+
"events": {
151
+
"type": "array",
152
+
"items": {
153
+
"type": "ref",
154
+
"ref": "tools.ozone.moderation.defs#modEventView"
155
+
}
156
+
}
157
+
}
158
+
}
159
+
}
160
+
}
161
+
}
162
+
}
+225
lexicons/tools/ozone/moderation/queryStatuses.json
+225
lexicons/tools/ozone/moderation/queryStatuses.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.queryStatuses",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "View moderation statuses of subjects (record or repo).",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"queueCount": {
12
+
"type": "integer",
13
+
"description": "Number of queues being used by moderators. Subjects will be split among all queues."
14
+
},
15
+
"queueIndex": {
16
+
"type": "integer",
17
+
"description": "Index of the queue to fetch subjects from. Works only when queueCount value is specified."
18
+
},
19
+
"queueSeed": {
20
+
"type": "string",
21
+
"description": "A seeder to shuffle/balance the queue items."
22
+
},
23
+
"includeAllUserRecords": {
24
+
"type": "boolean",
25
+
"description": "All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned."
26
+
},
27
+
"subject": {
28
+
"type": "string",
29
+
"format": "uri",
30
+
"description": "The subject to get the status for."
31
+
},
32
+
"comment": {
33
+
"type": "string",
34
+
"description": "Search subjects by keyword from comments"
35
+
},
36
+
"reportedAfter": {
37
+
"type": "string",
38
+
"format": "datetime",
39
+
"description": "Search subjects reported after a given timestamp"
40
+
},
41
+
"reportedBefore": {
42
+
"type": "string",
43
+
"format": "datetime",
44
+
"description": "Search subjects reported before a given timestamp"
45
+
},
46
+
"reviewedAfter": {
47
+
"type": "string",
48
+
"format": "datetime",
49
+
"description": "Search subjects reviewed after a given timestamp"
50
+
},
51
+
"hostingDeletedAfter": {
52
+
"type": "string",
53
+
"format": "datetime",
54
+
"description": "Search subjects where the associated record/account was deleted after a given timestamp"
55
+
},
56
+
"hostingDeletedBefore": {
57
+
"type": "string",
58
+
"format": "datetime",
59
+
"description": "Search subjects where the associated record/account was deleted before a given timestamp"
60
+
},
61
+
"hostingUpdatedAfter": {
62
+
"type": "string",
63
+
"format": "datetime",
64
+
"description": "Search subjects where the associated record/account was updated after a given timestamp"
65
+
},
66
+
"hostingUpdatedBefore": {
67
+
"type": "string",
68
+
"format": "datetime",
69
+
"description": "Search subjects where the associated record/account was updated before a given timestamp"
70
+
},
71
+
"hostingStatuses": {
72
+
"type": "array",
73
+
"items": {
74
+
"type": "string"
75
+
},
76
+
"description": "Search subjects by the status of the associated record/account"
77
+
},
78
+
"reviewedBefore": {
79
+
"type": "string",
80
+
"format": "datetime",
81
+
"description": "Search subjects reviewed before a given timestamp"
82
+
},
83
+
"includeMuted": {
84
+
"type": "boolean",
85
+
"description": "By default, we don't include muted subjects in the results. Set this to true to include them."
86
+
},
87
+
"onlyMuted": {
88
+
"type": "boolean",
89
+
"description": "When set to true, only muted subjects and reporters will be returned."
90
+
},
91
+
"reviewState": {
92
+
"type": "string",
93
+
"description": "Specify when fetching subjects in a certain state",
94
+
"knownValues": [
95
+
"tools.ozone.moderation.defs#reviewOpen",
96
+
"tools.ozone.moderation.defs#reviewClosed",
97
+
"tools.ozone.moderation.defs#reviewEscalated",
98
+
"tools.ozone.moderation.defs#reviewNone"
99
+
]
100
+
},
101
+
"ignoreSubjects": {
102
+
"type": "array",
103
+
"items": {
104
+
"type": "string",
105
+
"format": "uri"
106
+
}
107
+
},
108
+
"lastReviewedBy": {
109
+
"type": "string",
110
+
"format": "did",
111
+
"description": "Get all subject statuses that were reviewed by a specific moderator"
112
+
},
113
+
"sortField": {
114
+
"type": "string",
115
+
"default": "lastReportedAt",
116
+
"enum": [
117
+
"lastReviewedAt",
118
+
"lastReportedAt",
119
+
"reportedRecordsCount",
120
+
"takendownRecordsCount",
121
+
"priorityScore"
122
+
]
123
+
},
124
+
"sortDirection": {
125
+
"type": "string",
126
+
"default": "desc",
127
+
"enum": ["asc", "desc"]
128
+
},
129
+
"takendown": {
130
+
"type": "boolean",
131
+
"description": "Get subjects that were taken down"
132
+
},
133
+
"appealed": {
134
+
"type": "boolean",
135
+
"description": "Get subjects in unresolved appealed status"
136
+
},
137
+
"limit": {
138
+
"type": "integer",
139
+
"minimum": 1,
140
+
"maximum": 100,
141
+
"default": 50
142
+
},
143
+
"tags": {
144
+
"type": "array",
145
+
"maxLength": 25,
146
+
"items": {
147
+
"type": "string",
148
+
"description": "Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters"
149
+
}
150
+
},
151
+
"excludeTags": {
152
+
"type": "array",
153
+
"items": {
154
+
"type": "string"
155
+
}
156
+
},
157
+
"cursor": {
158
+
"type": "string"
159
+
},
160
+
"collections": {
161
+
"type": "array",
162
+
"maxLength": 20,
163
+
"description": "If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.",
164
+
"items": {
165
+
"type": "string",
166
+
"format": "nsid"
167
+
}
168
+
},
169
+
"subjectType": {
170
+
"type": "string",
171
+
"description": "If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.",
172
+
"knownValues": ["account", "record"]
173
+
},
174
+
"minAccountSuspendCount": {
175
+
"type": "integer",
176
+
"description": "If specified, only subjects that belong to an account that has at least this many suspensions will be returned."
177
+
},
178
+
"minReportedRecordsCount": {
179
+
"type": "integer",
180
+
"description": "If specified, only subjects that belong to an account that has at least this many reported records will be returned."
181
+
},
182
+
"minTakendownRecordsCount": {
183
+
"type": "integer",
184
+
"description": "If specified, only subjects that belong to an account that has at least this many taken down records will be returned."
185
+
},
186
+
"minPriorityScore": {
187
+
"minimum": 0,
188
+
"maximum": 100,
189
+
"type": "integer",
190
+
"description": "If specified, only subjects that have priority score value above the given value will be returned."
191
+
},
192
+
"minStrikeCount": {
193
+
"type": "integer",
194
+
"minimum": 1,
195
+
"description": "If specified, only subjects that belong to an account that has at least this many active strikes will be returned."
196
+
},
197
+
"ageAssuranceState": {
198
+
"type": "string",
199
+
"description": "If specified, only subjects with the given age assurance state will be returned.",
200
+
"knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
201
+
}
202
+
}
203
+
},
204
+
"output": {
205
+
"encoding": "application/json",
206
+
"schema": {
207
+
"type": "object",
208
+
"required": ["subjectStatuses"],
209
+
"properties": {
210
+
"cursor": {
211
+
"type": "string"
212
+
},
213
+
"subjectStatuses": {
214
+
"type": "array",
215
+
"items": {
216
+
"type": "ref",
217
+
"ref": "tools.ozone.moderation.defs#subjectStatusView"
218
+
}
219
+
}
220
+
}
221
+
}
222
+
}
223
+
}
224
+
}
225
+
}
+155
lexicons/tools/ozone/moderation/scheduleAction.json
+155
lexicons/tools/ozone/moderation/scheduleAction.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.scheduleAction",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Schedule a moderation action to be executed at a future time",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["action", "subjects", "createdBy", "scheduling"],
13
+
"properties": {
14
+
"action": {
15
+
"type": "union",
16
+
"refs": ["#takedown"]
17
+
},
18
+
"subjects": {
19
+
"type": "array",
20
+
"maxLength": 100,
21
+
"items": {
22
+
"type": "string",
23
+
"format": "did"
24
+
},
25
+
"description": "Array of DID subjects to schedule the action for"
26
+
},
27
+
"createdBy": {
28
+
"type": "string",
29
+
"format": "did"
30
+
},
31
+
"scheduling": {
32
+
"type": "ref",
33
+
"ref": "#schedulingConfig"
34
+
},
35
+
"modTool": {
36
+
"type": "ref",
37
+
"ref": "tools.ozone.moderation.defs#modTool",
38
+
"description": "This will be propagated to the moderation event when it is applied"
39
+
}
40
+
}
41
+
}
42
+
},
43
+
"output": {
44
+
"encoding": "application/json",
45
+
"schema": {
46
+
"type": "ref",
47
+
"ref": "#scheduledActionResults"
48
+
}
49
+
}
50
+
},
51
+
"takedown": {
52
+
"type": "object",
53
+
"description": "Schedule a takedown action",
54
+
"properties": {
55
+
"comment": {
56
+
"type": "string"
57
+
},
58
+
"durationInHours": {
59
+
"type": "integer",
60
+
"description": "Indicates how long the takedown should be in effect before automatically expiring."
61
+
},
62
+
"acknowledgeAccountSubjects": {
63
+
"type": "boolean",
64
+
"description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
65
+
},
66
+
"policies": {
67
+
"type": "array",
68
+
"maxLength": 5,
69
+
"items": {
70
+
"type": "string"
71
+
},
72
+
"description": "Names/Keywords of the policies that drove the decision."
73
+
},
74
+
"severityLevel": {
75
+
"type": "string",
76
+
"description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)."
77
+
},
78
+
"strikeCount": {
79
+
"type": "integer",
80
+
"description": "Number of strikes to assign to the user when takedown is applied."
81
+
},
82
+
"strikeExpiresAt": {
83
+
"type": "string",
84
+
"format": "datetime",
85
+
"description": "When the strike should expire. If not provided, the strike never expires."
86
+
},
87
+
"emailContent": {
88
+
"type": "string",
89
+
"description": "Email content to be sent to the user upon takedown."
90
+
},
91
+
"emailSubject": {
92
+
"type": "string",
93
+
"description": "Subject of the email to be sent to the user upon takedown."
94
+
}
95
+
}
96
+
},
97
+
"schedulingConfig": {
98
+
"type": "object",
99
+
"description": "Configuration for when the action should be executed",
100
+
"properties": {
101
+
"executeAt": {
102
+
"type": "string",
103
+
"format": "datetime",
104
+
"description": "Exact time to execute the action"
105
+
},
106
+
"executeAfter": {
107
+
"type": "string",
108
+
"format": "datetime",
109
+
"description": "Earliest time to execute the action (for randomized scheduling)"
110
+
},
111
+
"executeUntil": {
112
+
"type": "string",
113
+
"format": "datetime",
114
+
"description": "Latest time to execute the action (for randomized scheduling)"
115
+
}
116
+
}
117
+
},
118
+
"scheduledActionResults": {
119
+
"type": "object",
120
+
"required": ["succeeded", "failed"],
121
+
"properties": {
122
+
"succeeded": {
123
+
"type": "array",
124
+
"items": {
125
+
"type": "string",
126
+
"format": "did"
127
+
}
128
+
},
129
+
"failed": {
130
+
"type": "array",
131
+
"items": {
132
+
"type": "ref",
133
+
"ref": "#failedScheduling"
134
+
}
135
+
}
136
+
}
137
+
},
138
+
"failedScheduling": {
139
+
"type": "object",
140
+
"required": ["subject", "error"],
141
+
"properties": {
142
+
"subject": {
143
+
"type": "string",
144
+
"format": "did"
145
+
},
146
+
"error": {
147
+
"type": "string"
148
+
},
149
+
"errorCode": {
150
+
"type": "string"
151
+
}
152
+
}
153
+
}
154
+
}
155
+
}
+44
lexicons/tools/ozone/moderation/searchRepos.json
+44
lexicons/tools/ozone/moderation/searchRepos.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.moderation.searchRepos",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find repositories based on a search term.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"term": {
12
+
"type": "string",
13
+
"description": "DEPRECATED: use 'q' instead"
14
+
},
15
+
"q": { "type": "string" },
16
+
"limit": {
17
+
"type": "integer",
18
+
"minimum": 1,
19
+
"maximum": 100,
20
+
"default": 50
21
+
},
22
+
"cursor": { "type": "string" }
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["repos"],
30
+
"properties": {
31
+
"cursor": { "type": "string" },
32
+
"repos": {
33
+
"type": "array",
34
+
"items": {
35
+
"type": "ref",
36
+
"ref": "tools.ozone.moderation.defs#repoView"
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
+223
lexicons/tools/ozone/report/defs.json
+223
lexicons/tools/ozone/report/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.report.defs",
4
+
"defs": {
5
+
"reasonType": {
6
+
"type": "string",
7
+
"knownValues": [
8
+
"tools.ozone.report.defs#reasonAppeal",
9
+
"tools.ozone.report.defs#reasonOther",
10
+
11
+
"tools.ozone.report.defs#reasonViolenceAnimal",
12
+
"tools.ozone.report.defs#reasonViolenceThreats",
13
+
"tools.ozone.report.defs#reasonViolenceGraphicContent",
14
+
"tools.ozone.report.defs#reasonViolenceGlorification",
15
+
"tools.ozone.report.defs#reasonViolenceExtremistContent",
16
+
"tools.ozone.report.defs#reasonViolenceTrafficking",
17
+
"tools.ozone.report.defs#reasonViolenceOther",
18
+
19
+
"tools.ozone.report.defs#reasonSexualAbuseContent",
20
+
"tools.ozone.report.defs#reasonSexualNCII",
21
+
"tools.ozone.report.defs#reasonSexualDeepfake",
22
+
"tools.ozone.report.defs#reasonSexualAnimal",
23
+
"tools.ozone.report.defs#reasonSexualUnlabeled",
24
+
"tools.ozone.report.defs#reasonSexualOther",
25
+
26
+
"tools.ozone.report.defs#reasonChildSafetyCSAM",
27
+
"tools.ozone.report.defs#reasonChildSafetyGroom",
28
+
"tools.ozone.report.defs#reasonChildSafetyPrivacy",
29
+
"tools.ozone.report.defs#reasonChildSafetyHarassment",
30
+
"tools.ozone.report.defs#reasonChildSafetyOther",
31
+
32
+
"tools.ozone.report.defs#reasonHarassmentTroll",
33
+
"tools.ozone.report.defs#reasonHarassmentTargeted",
34
+
"tools.ozone.report.defs#reasonHarassmentHateSpeech",
35
+
"tools.ozone.report.defs#reasonHarassmentDoxxing",
36
+
"tools.ozone.report.defs#reasonHarassmentOther",
37
+
38
+
"tools.ozone.report.defs#reasonMisleadingBot",
39
+
"tools.ozone.report.defs#reasonMisleadingImpersonation",
40
+
"tools.ozone.report.defs#reasonMisleadingSpam",
41
+
"tools.ozone.report.defs#reasonMisleadingScam",
42
+
"tools.ozone.report.defs#reasonMisleadingElections",
43
+
"tools.ozone.report.defs#reasonMisleadingOther",
44
+
45
+
"tools.ozone.report.defs#reasonRuleSiteSecurity",
46
+
"tools.ozone.report.defs#reasonRuleProhibitedSales",
47
+
"tools.ozone.report.defs#reasonRuleBanEvasion",
48
+
"tools.ozone.report.defs#reasonRuleOther",
49
+
50
+
"tools.ozone.report.defs#reasonSelfHarmContent",
51
+
"tools.ozone.report.defs#reasonSelfHarmED",
52
+
"tools.ozone.report.defs#reasonSelfHarmStunts",
53
+
"tools.ozone.report.defs#reasonSelfHarmSubstances",
54
+
"tools.ozone.report.defs#reasonSelfHarmOther"
55
+
]
56
+
},
57
+
"reasonAppeal": {
58
+
"type": "token",
59
+
"description": "Appeal a previously taken moderation action"
60
+
},
61
+
"reasonOther": {
62
+
"type": "token",
63
+
"description": "An issue not included in these options"
64
+
},
65
+
"reasonViolenceAnimal": {
66
+
"type": "token",
67
+
"description": "Animal welfare violations"
68
+
},
69
+
"reasonViolenceThreats": {
70
+
"type": "token",
71
+
"description": "Threats or incitement"
72
+
},
73
+
"reasonViolenceGraphicContent": {
74
+
"type": "token",
75
+
"description": "Graphic violent content"
76
+
},
77
+
"reasonViolenceGlorification": {
78
+
"type": "token",
79
+
"description": "Glorification of violence"
80
+
},
81
+
"reasonViolenceExtremistContent": {
82
+
"type": "token",
83
+
"description": "Extremist content. These reports will be sent only be sent to the application's Moderation Authority."
84
+
},
85
+
"reasonViolenceTrafficking": {
86
+
"type": "token",
87
+
"description": "Human trafficking"
88
+
},
89
+
"reasonViolenceOther": {
90
+
"type": "token",
91
+
"description": "Other violent content"
92
+
},
93
+
94
+
"reasonSexualAbuseContent": {
95
+
"type": "token",
96
+
"description": "Adult sexual abuse content"
97
+
},
98
+
"reasonSexualNCII": {
99
+
"type": "token",
100
+
"description": "Non-consensual intimate imagery"
101
+
},
102
+
"reasonSexualDeepfake": {
103
+
"type": "token",
104
+
"description": "Deepfake adult content"
105
+
},
106
+
"reasonSexualAnimal": {
107
+
"type": "token",
108
+
"description": "Animal sexual abuse"
109
+
},
110
+
"reasonSexualUnlabeled": {
111
+
"type": "token",
112
+
"description": "Unlabelled adult content"
113
+
},
114
+
"reasonSexualOther": {
115
+
"type": "token",
116
+
"description": "Other sexual violence content"
117
+
},
118
+
119
+
"reasonChildSafetyCSAM": {
120
+
"type": "token",
121
+
"description": "Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority."
122
+
},
123
+
"reasonChildSafetyGroom": {
124
+
"type": "token",
125
+
"description": "Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority."
126
+
},
127
+
"reasonChildSafetyPrivacy": {
128
+
"type": "token",
129
+
"description": "Privacy violation involving a minor"
130
+
},
131
+
"reasonChildSafetyHarassment": {
132
+
"type": "token",
133
+
"description": "Harassment or bullying of minors"
134
+
},
135
+
"reasonChildSafetyOther": {
136
+
"type": "token",
137
+
"description": "Other child safety. These reports will be sent only be sent to the application's Moderation Authority."
138
+
},
139
+
140
+
"reasonHarassmentTroll": {
141
+
"type": "token",
142
+
"description": "Trolling"
143
+
},
144
+
"reasonHarassmentTargeted": {
145
+
"type": "token",
146
+
"description": "Targeted harassment"
147
+
},
148
+
"reasonHarassmentHateSpeech": {
149
+
"type": "token",
150
+
"description": "Hate speech"
151
+
},
152
+
"reasonHarassmentDoxxing": {
153
+
"type": "token",
154
+
"description": "Doxxing"
155
+
},
156
+
"reasonHarassmentOther": {
157
+
"type": "token",
158
+
"description": "Other harassing or hateful content"
159
+
},
160
+
161
+
"reasonMisleadingBot": {
162
+
"type": "token",
163
+
"description": "Fake account or bot"
164
+
},
165
+
"reasonMisleadingImpersonation": {
166
+
"type": "token",
167
+
"description": "Impersonation"
168
+
},
169
+
"reasonMisleadingSpam": {
170
+
"type": "token",
171
+
"description": "Spam"
172
+
},
173
+
"reasonMisleadingScam": {
174
+
"type": "token",
175
+
"description": "Scam"
176
+
},
177
+
"reasonMisleadingElections": {
178
+
"type": "token",
179
+
"description": "False information about elections"
180
+
},
181
+
"reasonMisleadingOther": {
182
+
"type": "token",
183
+
"description": "Other misleading content"
184
+
},
185
+
186
+
"reasonRuleSiteSecurity": {
187
+
"type": "token",
188
+
"description": "Hacking or system attacks"
189
+
},
190
+
"reasonRuleProhibitedSales": {
191
+
"type": "token",
192
+
"description": "Promoting or selling prohibited items or services"
193
+
},
194
+
"reasonRuleBanEvasion": {
195
+
"type": "token",
196
+
"description": "Banned user returning"
197
+
},
198
+
"reasonRuleOther": {
199
+
"type": "token",
200
+
"description": "Other"
201
+
},
202
+
"reasonSelfHarmContent": {
203
+
"type": "token",
204
+
"description": "Content promoting or depicting self-harm"
205
+
},
206
+
"reasonSelfHarmED": {
207
+
"type": "token",
208
+
"description": "Eating disorders"
209
+
},
210
+
"reasonSelfHarmStunts": {
211
+
"type": "token",
212
+
"description": "Dangerous challenges or activities"
213
+
},
214
+
"reasonSelfHarmSubstances": {
215
+
"type": "token",
216
+
"description": "Dangerous substances or drug abuse"
217
+
},
218
+
"reasonSelfHarmOther": {
219
+
"type": "token",
220
+
"description": "Other dangerous content"
221
+
}
222
+
}
223
+
}
+61
lexicons/tools/ozone/safelink/addRule.json
+61
lexicons/tools/ozone/safelink/addRule.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.addRule",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Add a new URL safety rule",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["url", "pattern", "action", "reason"],
13
+
"properties": {
14
+
"url": {
15
+
"type": "string",
16
+
"description": "The URL or domain to apply the rule to"
17
+
},
18
+
"pattern": {
19
+
"type": "ref",
20
+
"ref": "tools.ozone.safelink.defs#patternType"
21
+
},
22
+
"action": {
23
+
"type": "ref",
24
+
"ref": "tools.ozone.safelink.defs#actionType"
25
+
},
26
+
"reason": {
27
+
"type": "ref",
28
+
"ref": "tools.ozone.safelink.defs#reasonType"
29
+
},
30
+
"comment": {
31
+
"type": "string",
32
+
"description": "Optional comment about the decision"
33
+
},
34
+
"createdBy": {
35
+
"type": "string",
36
+
"format": "did",
37
+
"description": "Author DID. Only respected when using admin auth"
38
+
}
39
+
}
40
+
}
41
+
},
42
+
"output": {
43
+
"encoding": "application/json",
44
+
"schema": {
45
+
"type": "ref",
46
+
"ref": "tools.ozone.safelink.defs#event"
47
+
}
48
+
},
49
+
"errors": [
50
+
{
51
+
"name": "InvalidUrl",
52
+
"description": "The provided URL is invalid"
53
+
},
54
+
{
55
+
"name": "RuleAlreadyExists",
56
+
"description": "A rule for this URL/domain already exists"
57
+
}
58
+
]
59
+
}
60
+
}
61
+
}
+125
lexicons/tools/ozone/safelink/defs.json
+125
lexicons/tools/ozone/safelink/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.defs",
4
+
"defs": {
5
+
"event": {
6
+
"type": "object",
7
+
"description": "An event for URL safety decisions",
8
+
"required": [
9
+
"id",
10
+
"eventType",
11
+
"url",
12
+
"pattern",
13
+
"action",
14
+
"reason",
15
+
"createdBy",
16
+
"createdAt"
17
+
],
18
+
"properties": {
19
+
"id": {
20
+
"type": "integer",
21
+
"description": "Auto-incrementing row ID"
22
+
},
23
+
"eventType": {
24
+
"type": "ref",
25
+
"ref": "#eventType"
26
+
},
27
+
"url": {
28
+
"type": "string",
29
+
"description": "The URL that this rule applies to"
30
+
},
31
+
"pattern": {
32
+
"type": "ref",
33
+
"ref": "#patternType"
34
+
},
35
+
"action": {
36
+
"type": "ref",
37
+
"ref": "#actionType"
38
+
},
39
+
"reason": {
40
+
"type": "ref",
41
+
"ref": "#reasonType"
42
+
},
43
+
"createdBy": {
44
+
"type": "string",
45
+
"format": "did",
46
+
"description": "DID of the user who created this rule"
47
+
},
48
+
"createdAt": {
49
+
"type": "string",
50
+
"format": "datetime"
51
+
},
52
+
"comment": {
53
+
"type": "string",
54
+
"description": "Optional comment about the decision"
55
+
}
56
+
}
57
+
},
58
+
"eventType": {
59
+
"type": "string",
60
+
"knownValues": ["addRule", "updateRule", "removeRule"]
61
+
},
62
+
"patternType": {
63
+
"type": "string",
64
+
"knownValues": ["domain", "url"]
65
+
},
66
+
"actionType": {
67
+
"type": "string",
68
+
"knownValues": ["block", "warn", "whitelist"]
69
+
},
70
+
"reasonType": {
71
+
"type": "string",
72
+
"knownValues": ["csam", "spam", "phishing", "none"]
73
+
},
74
+
"urlRule": {
75
+
"type": "object",
76
+
"description": "Input for creating a URL safety rule",
77
+
"required": [
78
+
"url",
79
+
"pattern",
80
+
"action",
81
+
"reason",
82
+
"createdBy",
83
+
"createdAt",
84
+
"updatedAt"
85
+
],
86
+
"properties": {
87
+
"url": {
88
+
"type": "string",
89
+
"description": "The URL or domain to apply the rule to"
90
+
},
91
+
"pattern": {
92
+
"type": "ref",
93
+
"ref": "#patternType"
94
+
},
95
+
"action": {
96
+
"type": "ref",
97
+
"ref": "#actionType"
98
+
},
99
+
"reason": {
100
+
"type": "ref",
101
+
"ref": "#reasonType"
102
+
},
103
+
"comment": {
104
+
"type": "string",
105
+
"description": "Optional comment about the decision"
106
+
},
107
+
"createdBy": {
108
+
"type": "string",
109
+
"format": "did",
110
+
"description": "DID of the user added the rule."
111
+
},
112
+
"createdAt": {
113
+
"type": "string",
114
+
"format": "datetime",
115
+
"description": "Timestamp when the rule was created"
116
+
},
117
+
"updatedAt": {
118
+
"type": "string",
119
+
"format": "datetime",
120
+
"description": "Timestamp when the rule was last updated"
121
+
}
122
+
}
123
+
}
124
+
}
125
+
}
+66
lexicons/tools/ozone/safelink/queryEvents.json
+66
lexicons/tools/ozone/safelink/queryEvents.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.queryEvents",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Query URL safety audit events",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"cursor": {
14
+
"type": "string",
15
+
"description": "Cursor for pagination"
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50,
22
+
"description": "Maximum number of results to return"
23
+
},
24
+
"urls": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "string"
28
+
},
29
+
"description": "Filter by specific URLs or domains"
30
+
},
31
+
"patternType": {
32
+
"type": "string",
33
+
"description": "Filter by pattern type"
34
+
},
35
+
"sortDirection": {
36
+
"type": "string",
37
+
"knownValues": ["asc", "desc"],
38
+
"default": "desc",
39
+
"description": "Sort direction"
40
+
}
41
+
}
42
+
}
43
+
},
44
+
"output": {
45
+
"encoding": "application/json",
46
+
"schema": {
47
+
"type": "object",
48
+
"required": ["events"],
49
+
"properties": {
50
+
"cursor": {
51
+
"type": "string",
52
+
"description": "Next cursor for pagination. Only present if there are more results."
53
+
},
54
+
"events": {
55
+
"type": "array",
56
+
"items": {
57
+
"type": "ref",
58
+
"ref": "tools.ozone.safelink.defs#event"
59
+
}
60
+
}
61
+
}
62
+
}
63
+
}
64
+
}
65
+
}
66
+
}
+82
lexicons/tools/ozone/safelink/queryRules.json
+82
lexicons/tools/ozone/safelink/queryRules.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.queryRules",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Query URL safety rules",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"cursor": {
14
+
"type": "string",
15
+
"description": "Cursor for pagination"
16
+
},
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50,
22
+
"description": "Maximum number of results to return"
23
+
},
24
+
"urls": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "string"
28
+
},
29
+
"description": "Filter by specific URLs or domains"
30
+
},
31
+
"patternType": {
32
+
"type": "string",
33
+
"description": "Filter by pattern type"
34
+
},
35
+
"actions": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "string"
39
+
},
40
+
"description": "Filter by action types"
41
+
},
42
+
"reason": {
43
+
"type": "string",
44
+
"description": "Filter by reason type"
45
+
},
46
+
"createdBy": {
47
+
"type": "string",
48
+
"format": "did",
49
+
"description": "Filter by rule creator"
50
+
},
51
+
"sortDirection": {
52
+
"type": "string",
53
+
"knownValues": ["asc", "desc"],
54
+
"default": "desc",
55
+
"description": "Sort direction"
56
+
}
57
+
}
58
+
}
59
+
},
60
+
"output": {
61
+
"encoding": "application/json",
62
+
"schema": {
63
+
"type": "object",
64
+
"required": ["rules"],
65
+
"properties": {
66
+
"cursor": {
67
+
"type": "string",
68
+
"description": "Next cursor for pagination. Only present if there are more results."
69
+
},
70
+
"rules": {
71
+
"type": "array",
72
+
"items": {
73
+
"type": "ref",
74
+
"ref": "tools.ozone.safelink.defs#urlRule"
75
+
}
76
+
}
77
+
}
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
+49
lexicons/tools/ozone/safelink/removeRule.json
+49
lexicons/tools/ozone/safelink/removeRule.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.removeRule",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Remove an existing URL safety rule",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["url", "pattern"],
13
+
"properties": {
14
+
"url": {
15
+
"type": "string",
16
+
"description": "The URL or domain to remove the rule for"
17
+
},
18
+
"pattern": {
19
+
"type": "ref",
20
+
"ref": "tools.ozone.safelink.defs#patternType"
21
+
},
22
+
"comment": {
23
+
"type": "string",
24
+
"description": "Optional comment about why the rule is being removed"
25
+
},
26
+
"createdBy": {
27
+
"type": "string",
28
+
"format": "did",
29
+
"description": "Optional DID of the user. Only respected when using admin auth."
30
+
}
31
+
}
32
+
}
33
+
},
34
+
"output": {
35
+
"encoding": "application/json",
36
+
"schema": {
37
+
"type": "ref",
38
+
"ref": "tools.ozone.safelink.defs#event"
39
+
}
40
+
},
41
+
"errors": [
42
+
{
43
+
"name": "RuleNotFound",
44
+
"description": "No active rule found for this URL/domain"
45
+
}
46
+
]
47
+
}
48
+
}
49
+
}
+57
lexicons/tools/ozone/safelink/updateRule.json
+57
lexicons/tools/ozone/safelink/updateRule.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.safelink.updateRule",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update an existing URL safety rule",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["url", "pattern", "action", "reason"],
13
+
"properties": {
14
+
"url": {
15
+
"type": "string",
16
+
"description": "The URL or domain to update the rule for"
17
+
},
18
+
"pattern": {
19
+
"type": "ref",
20
+
"ref": "tools.ozone.safelink.defs#patternType"
21
+
},
22
+
"action": {
23
+
"type": "ref",
24
+
"ref": "tools.ozone.safelink.defs#actionType"
25
+
},
26
+
"reason": {
27
+
"type": "ref",
28
+
"ref": "tools.ozone.safelink.defs#reasonType"
29
+
},
30
+
"comment": {
31
+
"type": "string",
32
+
"description": "Optional comment about the update"
33
+
},
34
+
"createdBy": {
35
+
"type": "string",
36
+
"format": "did",
37
+
"description": "Optional DID to credit as the creator. Only respected for admin_token authentication."
38
+
}
39
+
}
40
+
}
41
+
},
42
+
"output": {
43
+
"encoding": "application/json",
44
+
"schema": {
45
+
"type": "ref",
46
+
"ref": "tools.ozone.safelink.defs#event"
47
+
}
48
+
},
49
+
"errors": [
50
+
{
51
+
"name": "RuleNotFound",
52
+
"description": "No active rule found for this URL/domain"
53
+
}
54
+
]
55
+
}
56
+
}
57
+
}
+66
lexicons/tools/ozone/server/getConfig.json
+66
lexicons/tools/ozone/server/getConfig.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.server.getConfig",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get details about ozone's server configuration.",
8
+
"output": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"properties": {
13
+
"appview": {
14
+
"type": "ref",
15
+
"ref": "#serviceConfig"
16
+
},
17
+
"pds": {
18
+
"type": "ref",
19
+
"ref": "#serviceConfig"
20
+
},
21
+
"blobDivert": {
22
+
"type": "ref",
23
+
"ref": "#serviceConfig"
24
+
},
25
+
"chat": {
26
+
"type": "ref",
27
+
"ref": "#serviceConfig"
28
+
},
29
+
"viewer": {
30
+
"type": "ref",
31
+
"ref": "#viewerConfig"
32
+
},
33
+
"verifierDid": {
34
+
"type": "string",
35
+
"format": "did",
36
+
"description": "The did of the verifier used for verification."
37
+
}
38
+
}
39
+
}
40
+
}
41
+
},
42
+
"serviceConfig": {
43
+
"type": "object",
44
+
"properties": {
45
+
"url": {
46
+
"type": "string",
47
+
"format": "uri"
48
+
}
49
+
}
50
+
},
51
+
"viewerConfig": {
52
+
"type": "object",
53
+
"properties": {
54
+
"role": {
55
+
"type": "string",
56
+
"knownValues": [
57
+
"tools.ozone.team.defs#roleAdmin",
58
+
"tools.ozone.team.defs#roleModerator",
59
+
"tools.ozone.team.defs#roleTriage",
60
+
"tools.ozone.team.defs#roleVerifier"
61
+
]
62
+
}
63
+
}
64
+
}
65
+
}
66
+
}
+32
lexicons/tools/ozone/set/addValues.json
+32
lexicons/tools/ozone/set/addValues.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.addValues",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["name", "values"],
13
+
"properties": {
14
+
"name": {
15
+
"type": "string",
16
+
"description": "Name of the set to add values to"
17
+
},
18
+
"values": {
19
+
"type": "array",
20
+
"minLength": 1,
21
+
"maxLength": 1000,
22
+
"items": {
23
+
"type": "string"
24
+
},
25
+
"description": "Array of string values to add to the set"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
+49
lexicons/tools/ozone/set/defs.json
+49
lexicons/tools/ozone/set/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.defs",
4
+
"defs": {
5
+
"set": {
6
+
"type": "object",
7
+
"required": ["name"],
8
+
"properties": {
9
+
"name": {
10
+
"type": "string",
11
+
"minLength": 3,
12
+
"maxLength": 128
13
+
},
14
+
"description": {
15
+
"type": "string",
16
+
"maxGraphemes": 1024,
17
+
"maxLength": 10240
18
+
}
19
+
}
20
+
},
21
+
"setView": {
22
+
"type": "object",
23
+
"required": ["name", "setSize", "createdAt", "updatedAt"],
24
+
"properties": {
25
+
"name": {
26
+
"type": "string",
27
+
"minLength": 3,
28
+
"maxLength": 128
29
+
},
30
+
"description": {
31
+
"type": "string",
32
+
"maxGraphemes": 1024,
33
+
"maxLength": 10240
34
+
},
35
+
"setSize": {
36
+
"type": "integer"
37
+
},
38
+
"createdAt": {
39
+
"type": "string",
40
+
"format": "datetime"
41
+
},
42
+
"updatedAt": {
43
+
"type": "string",
44
+
"format": "datetime"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
+36
lexicons/tools/ozone/set/deleteSet.json
+36
lexicons/tools/ozone/set/deleteSet.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.deleteSet",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete an entire set. Attempting to delete a set that does not exist will result in an error.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["name"],
13
+
"properties": {
14
+
"name": {
15
+
"type": "string",
16
+
"description": "Name of the set to delete"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"properties": {}
26
+
}
27
+
},
28
+
"errors": [
29
+
{
30
+
"name": "SetNotFound",
31
+
"description": "set with the given name does not exist"
32
+
}
33
+
]
34
+
}
35
+
}
36
+
}
+37
lexicons/tools/ozone/set/deleteValues.json
+37
lexicons/tools/ozone/set/deleteValues.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.deleteValues",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["name", "values"],
13
+
"properties": {
14
+
"name": {
15
+
"type": "string",
16
+
"description": "Name of the set to delete values from"
17
+
},
18
+
"values": {
19
+
"type": "array",
20
+
"minLength": 1,
21
+
"items": {
22
+
"type": "string"
23
+
},
24
+
"description": "Array of string values to delete from the set"
25
+
}
26
+
}
27
+
}
28
+
},
29
+
"errors": [
30
+
{
31
+
"name": "SetNotFound",
32
+
"description": "set with the given name does not exist"
33
+
}
34
+
]
35
+
}
36
+
}
37
+
}
+56
lexicons/tools/ozone/set/getValues.json
+56
lexicons/tools/ozone/set/getValues.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.getValues",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get a specific set and its values",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["name"],
11
+
"properties": {
12
+
"name": {
13
+
"type": "string"
14
+
},
15
+
"limit": {
16
+
"type": "integer",
17
+
"minimum": 1,
18
+
"maximum": 1000,
19
+
"default": 100
20
+
},
21
+
"cursor": {
22
+
"type": "string"
23
+
}
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["set", "values"],
31
+
"properties": {
32
+
"set": {
33
+
"type": "ref",
34
+
"ref": "tools.ozone.set.defs#setView"
35
+
},
36
+
"values": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "string"
40
+
}
41
+
},
42
+
"cursor": {
43
+
"type": "string"
44
+
}
45
+
}
46
+
}
47
+
},
48
+
"errors": [
49
+
{
50
+
"name": "SetNotFound",
51
+
"description": "set with the given name does not exist"
52
+
}
53
+
]
54
+
}
55
+
}
56
+
}
+57
lexicons/tools/ozone/set/querySets.json
+57
lexicons/tools/ozone/set/querySets.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.querySets",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Query available sets",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": {
18
+
"type": "string"
19
+
},
20
+
"namePrefix": {
21
+
"type": "string"
22
+
},
23
+
"sortBy": {
24
+
"type": "string",
25
+
"enum": ["name", "createdAt", "updatedAt"],
26
+
"default": "name"
27
+
},
28
+
"sortDirection": {
29
+
"type": "string",
30
+
"default": "asc",
31
+
"enum": ["asc", "desc"],
32
+
"description": "Defaults to ascending order of name field."
33
+
}
34
+
}
35
+
},
36
+
"output": {
37
+
"encoding": "application/json",
38
+
"schema": {
39
+
"type": "object",
40
+
"required": ["sets"],
41
+
"properties": {
42
+
"sets": {
43
+
"type": "array",
44
+
"items": {
45
+
"type": "ref",
46
+
"ref": "tools.ozone.set.defs#setView"
47
+
}
48
+
},
49
+
"cursor": {
50
+
"type": "string"
51
+
}
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
57
+
}
+24
lexicons/tools/ozone/set/upsertSet.json
+24
lexicons/tools/ozone/set/upsertSet.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.set.upsertSet",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create or update set metadata",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "ref",
12
+
"ref": "tools.ozone.set.defs#set"
13
+
}
14
+
},
15
+
"output": {
16
+
"encoding": "application/json",
17
+
"schema": {
18
+
"type": "ref",
19
+
"ref": "tools.ozone.set.defs#setView"
20
+
}
21
+
}
22
+
}
23
+
}
24
+
}
+64
lexicons/tools/ozone/setting/defs.json
+64
lexicons/tools/ozone/setting/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.setting.defs",
4
+
"defs": {
5
+
"option": {
6
+
"type": "object",
7
+
"required": [
8
+
"key",
9
+
"value",
10
+
"did",
11
+
"scope",
12
+
"createdBy",
13
+
"lastUpdatedBy"
14
+
],
15
+
"properties": {
16
+
"key": {
17
+
"type": "string",
18
+
"format": "nsid"
19
+
},
20
+
"did": {
21
+
"type": "string",
22
+
"format": "did"
23
+
},
24
+
"value": {
25
+
"type": "unknown"
26
+
},
27
+
"description": {
28
+
"type": "string",
29
+
"maxGraphemes": 1024,
30
+
"maxLength": 10240
31
+
},
32
+
"createdAt": {
33
+
"type": "string",
34
+
"format": "datetime"
35
+
},
36
+
"updatedAt": {
37
+
"type": "string",
38
+
"format": "datetime"
39
+
},
40
+
"managerRole": {
41
+
"type": "string",
42
+
"knownValues": [
43
+
"tools.ozone.team.defs#roleModerator",
44
+
"tools.ozone.team.defs#roleTriage",
45
+
"tools.ozone.team.defs#roleAdmin",
46
+
"tools.ozone.team.defs#roleVerifier"
47
+
]
48
+
},
49
+
"scope": {
50
+
"type": "string",
51
+
"knownValues": ["instance", "personal"]
52
+
},
53
+
"createdBy": {
54
+
"type": "string",
55
+
"format": "did"
56
+
},
57
+
"lastUpdatedBy": {
58
+
"type": "string",
59
+
"format": "did"
60
+
}
61
+
}
62
+
}
63
+
}
64
+
}
+61
lexicons/tools/ozone/setting/listOptions.json
+61
lexicons/tools/ozone/setting/listOptions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.setting.listOptions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List settings with optional filtering",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"limit": {
12
+
"type": "integer",
13
+
"minimum": 1,
14
+
"maximum": 100,
15
+
"default": 50
16
+
},
17
+
"cursor": {
18
+
"type": "string"
19
+
},
20
+
"scope": {
21
+
"type": "string",
22
+
"knownValues": ["instance", "personal"],
23
+
"default": "instance"
24
+
},
25
+
"prefix": {
26
+
"type": "string",
27
+
"description": "Filter keys by prefix"
28
+
},
29
+
"keys": {
30
+
"type": "array",
31
+
"maxLength": 100,
32
+
"items": {
33
+
"type": "string",
34
+
"format": "nsid"
35
+
},
36
+
"description": "Filter for only the specified keys. Ignored if prefix is provided"
37
+
}
38
+
}
39
+
},
40
+
"output": {
41
+
"encoding": "application/json",
42
+
"schema": {
43
+
"type": "object",
44
+
"required": ["options"],
45
+
"properties": {
46
+
"cursor": {
47
+
"type": "string"
48
+
},
49
+
"options": {
50
+
"type": "array",
51
+
"items": {
52
+
"type": "ref",
53
+
"ref": "tools.ozone.setting.defs#option"
54
+
}
55
+
}
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
61
+
}
+39
lexicons/tools/ozone/setting/removeOptions.json
+39
lexicons/tools/ozone/setting/removeOptions.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.setting.removeOptions",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete settings by key",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["keys", "scope"],
13
+
"properties": {
14
+
"keys": {
15
+
"type": "array",
16
+
"minLength": 1,
17
+
"maxLength": 200,
18
+
"items": {
19
+
"type": "string",
20
+
"format": "nsid"
21
+
}
22
+
},
23
+
"scope": {
24
+
"type": "string",
25
+
"knownValues": ["instance", "personal"]
26
+
}
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"properties": {}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+56
lexicons/tools/ozone/setting/upsertOption.json
+56
lexicons/tools/ozone/setting/upsertOption.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.setting.upsertOption",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Create or update setting option",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["key", "scope", "value"],
13
+
"properties": {
14
+
"key": {
15
+
"type": "string",
16
+
"format": "nsid"
17
+
},
18
+
"scope": {
19
+
"type": "string",
20
+
"knownValues": ["instance", "personal"]
21
+
},
22
+
"value": {
23
+
"type": "unknown"
24
+
},
25
+
"description": {
26
+
"type": "string",
27
+
"maxLength": 2000
28
+
},
29
+
"managerRole": {
30
+
"type": "string",
31
+
"knownValues": [
32
+
"tools.ozone.team.defs#roleModerator",
33
+
"tools.ozone.team.defs#roleTriage",
34
+
"tools.ozone.team.defs#roleVerifier",
35
+
"tools.ozone.team.defs#roleAdmin"
36
+
]
37
+
}
38
+
}
39
+
}
40
+
},
41
+
"output": {
42
+
"encoding": "application/json",
43
+
"schema": {
44
+
"type": "object",
45
+
"required": ["option"],
46
+
"properties": {
47
+
"option": {
48
+
"type": "ref",
49
+
"ref": "tools.ozone.setting.defs#option"
50
+
}
51
+
}
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
+14
lexicons/tools/ozone/signature/defs.json
+14
lexicons/tools/ozone/signature/defs.json
+39
lexicons/tools/ozone/signature/findCorrelation.json
+39
lexicons/tools/ozone/signature/findCorrelation.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.signature.findCorrelation",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Find all correlated threat signatures between 2 or more accounts.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["dids"],
11
+
"properties": {
12
+
"dids": {
13
+
"type": "array",
14
+
"items": {
15
+
"type": "string",
16
+
"format": "did"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["details"],
26
+
"properties": {
27
+
"details": {
28
+
"type": "array",
29
+
"items": {
30
+
"type": "ref",
31
+
"ref": "tools.ozone.signature.defs#sigDetail"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
+61
lexicons/tools/ozone/signature/findRelatedAccounts.json
+61
lexicons/tools/ozone/signature/findRelatedAccounts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.signature.findRelatedAccounts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get accounts that share some matching threat signatures with the root account.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["did"],
11
+
"properties": {
12
+
"did": {
13
+
"type": "string",
14
+
"format": "did"
15
+
},
16
+
"cursor": { "type": "string" },
17
+
"limit": {
18
+
"type": "integer",
19
+
"minimum": 1,
20
+
"maximum": 100,
21
+
"default": 50
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["accounts"],
30
+
"properties": {
31
+
"cursor": { "type": "string" },
32
+
"accounts": {
33
+
"type": "array",
34
+
"items": {
35
+
"type": "ref",
36
+
"ref": "#relatedAccount"
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
},
43
+
"relatedAccount": {
44
+
"type": "object",
45
+
"required": ["account"],
46
+
"properties": {
47
+
"account": {
48
+
"type": "ref",
49
+
"ref": "com.atproto.admin.defs#accountView"
50
+
},
51
+
"similarities": {
52
+
"type": "array",
53
+
"items": {
54
+
"type": "ref",
55
+
"ref": "tools.ozone.signature.defs#sigDetail"
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
61
+
}
+46
lexicons/tools/ozone/signature/searchAccounts.json
+46
lexicons/tools/ozone/signature/searchAccounts.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.signature.searchAccounts",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Search for accounts that match one or more threat signature values.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["values"],
11
+
"properties": {
12
+
"values": {
13
+
"type": "array",
14
+
"items": {
15
+
"type": "string"
16
+
}
17
+
},
18
+
"cursor": { "type": "string" },
19
+
"limit": {
20
+
"type": "integer",
21
+
"minimum": 1,
22
+
"maximum": 100,
23
+
"default": 50
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["accounts"],
32
+
"properties": {
33
+
"cursor": { "type": "string" },
34
+
"accounts": {
35
+
"type": "array",
36
+
"items": {
37
+
"type": "ref",
38
+
"ref": "com.atproto.admin.defs#accountView"
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
46
+
}
+42
lexicons/tools/ozone/team/addMember.json
+42
lexicons/tools/ozone/team/addMember.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.team.addMember",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Add a member to the ozone team. Requires admin role.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did", "role"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"role": {
16
+
"type": "string",
17
+
"knownValues": [
18
+
"tools.ozone.team.defs#roleAdmin",
19
+
"tools.ozone.team.defs#roleModerator",
20
+
"tools.ozone.team.defs#roleVerifier",
21
+
"tools.ozone.team.defs#roleTriage"
22
+
]
23
+
}
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "ref",
31
+
"ref": "tools.ozone.team.defs#member"
32
+
}
33
+
},
34
+
"errors": [
35
+
{
36
+
"name": "MemberAlreadyExists",
37
+
"description": "Member already exists in the team."
38
+
}
39
+
]
40
+
}
41
+
}
42
+
}
+46
lexicons/tools/ozone/team/defs.json
+46
lexicons/tools/ozone/team/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.team.defs",
4
+
"defs": {
5
+
"member": {
6
+
"type": "object",
7
+
"required": ["did", "role"],
8
+
"properties": {
9
+
"did": { "type": "string", "format": "did" },
10
+
"disabled": { "type": "boolean" },
11
+
"profile": {
12
+
"type": "ref",
13
+
"ref": "app.bsky.actor.defs#profileViewDetailed"
14
+
},
15
+
"createdAt": { "type": "string", "format": "datetime" },
16
+
"updatedAt": { "type": "string", "format": "datetime" },
17
+
"lastUpdatedBy": { "type": "string" },
18
+
"role": {
19
+
"type": "string",
20
+
"knownValues": [
21
+
"tools.ozone.team.defs#roleAdmin",
22
+
"tools.ozone.team.defs#roleModerator",
23
+
"tools.ozone.team.defs#roleTriage",
24
+
"tools.ozone.team.defs#roleVerifier"
25
+
]
26
+
}
27
+
}
28
+
},
29
+
"roleAdmin": {
30
+
"type": "token",
31
+
"description": "Admin role. Highest level of access, can perform all actions."
32
+
},
33
+
"roleModerator": {
34
+
"type": "token",
35
+
"description": "Moderator role. Can perform most actions."
36
+
},
37
+
"roleTriage": {
38
+
"type": "token",
39
+
"description": "Triage role. Mostly intended for monitoring and escalating issues."
40
+
},
41
+
"roleVerifier": {
42
+
"type": "token",
43
+
"description": "Verifier role. Only allowed to issue verifications."
44
+
}
45
+
}
46
+
}
+30
lexicons/tools/ozone/team/deleteMember.json
+30
lexicons/tools/ozone/team/deleteMember.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.team.deleteMember",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete a member from ozone team. Requires admin role.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" }
15
+
}
16
+
}
17
+
},
18
+
"errors": [
19
+
{
20
+
"name": "MemberNotFound",
21
+
"description": "The member being deleted does not exist"
22
+
},
23
+
{
24
+
"name": "CannotDeleteSelf",
25
+
"description": "You can not delete yourself from the team"
26
+
}
27
+
]
28
+
}
29
+
}
30
+
}
+55
lexicons/tools/ozone/team/listMembers.json
+55
lexicons/tools/ozone/team/listMembers.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.team.listMembers",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List all members with access to the ozone service.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"q": {
12
+
"type": "string"
13
+
},
14
+
"disabled": {
15
+
"type": "boolean"
16
+
},
17
+
"roles": {
18
+
"type": "array",
19
+
"items": {
20
+
"type": "string"
21
+
}
22
+
},
23
+
"limit": {
24
+
"type": "integer",
25
+
"minimum": 1,
26
+
"maximum": 100,
27
+
"default": 50
28
+
},
29
+
"cursor": {
30
+
"type": "string"
31
+
}
32
+
}
33
+
},
34
+
"output": {
35
+
"encoding": "application/json",
36
+
"schema": {
37
+
"type": "object",
38
+
"required": ["members"],
39
+
"properties": {
40
+
"cursor": {
41
+
"type": "string"
42
+
},
43
+
"members": {
44
+
"type": "array",
45
+
"items": {
46
+
"type": "ref",
47
+
"ref": "tools.ozone.team.defs#member"
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}
53
+
}
54
+
}
55
+
}
+43
lexicons/tools/ozone/team/updateMember.json
+43
lexicons/tools/ozone/team/updateMember.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.team.updateMember",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update a member in the ozone service. Requires admin role.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["did"],
13
+
"properties": {
14
+
"did": { "type": "string", "format": "did" },
15
+
"disabled": { "type": "boolean" },
16
+
"role": {
17
+
"type": "string",
18
+
"knownValues": [
19
+
"tools.ozone.team.defs#roleAdmin",
20
+
"tools.ozone.team.defs#roleModerator",
21
+
"tools.ozone.team.defs#roleVerifier",
22
+
"tools.ozone.team.defs#roleTriage"
23
+
]
24
+
}
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "ref",
32
+
"ref": "tools.ozone.team.defs#member"
33
+
}
34
+
},
35
+
"errors": [
36
+
{
37
+
"name": "MemberNotFound",
38
+
"description": "The member being updated does not exist in the team"
39
+
}
40
+
]
41
+
}
42
+
}
43
+
}
+85
lexicons/tools/ozone/verification/defs.json
+85
lexicons/tools/ozone/verification/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.verification.defs",
4
+
"defs": {
5
+
"verificationView": {
6
+
"type": "object",
7
+
"description": "Verification data for the associated subject.",
8
+
"required": [
9
+
"issuer",
10
+
"uri",
11
+
"subject",
12
+
"handle",
13
+
"displayName",
14
+
"createdAt"
15
+
],
16
+
"properties": {
17
+
"issuer": {
18
+
"type": "string",
19
+
"description": "The user who issued this verification.",
20
+
"format": "did"
21
+
},
22
+
"uri": {
23
+
"type": "string",
24
+
"description": "The AT-URI of the verification record.",
25
+
"format": "at-uri"
26
+
},
27
+
"subject": {
28
+
"type": "string",
29
+
"format": "did",
30
+
"description": "The subject of the verification."
31
+
},
32
+
"handle": {
33
+
"type": "string",
34
+
"description": "Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.",
35
+
"format": "handle"
36
+
},
37
+
"displayName": {
38
+
"type": "string",
39
+
"description": "Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying."
40
+
},
41
+
"createdAt": {
42
+
"type": "string",
43
+
"description": "Timestamp when the verification was created.",
44
+
"format": "datetime"
45
+
},
46
+
"revokeReason": {
47
+
"type": "string",
48
+
"description": "Describes the reason for revocation, also indicating that the verification is no longer valid."
49
+
},
50
+
"revokedAt": {
51
+
"type": "string",
52
+
"description": "Timestamp when the verification was revoked.",
53
+
"format": "datetime"
54
+
},
55
+
"revokedBy": {
56
+
"type": "string",
57
+
"description": "The user who revoked this verification.",
58
+
"format": "did"
59
+
},
60
+
"subjectProfile": {
61
+
"type": "union",
62
+
"refs": []
63
+
},
64
+
"issuerProfile": {
65
+
"type": "union",
66
+
"refs": []
67
+
},
68
+
"subjectRepo": {
69
+
"type": "union",
70
+
"refs": [
71
+
"tools.ozone.moderation.defs#repoViewDetail",
72
+
"tools.ozone.moderation.defs#repoViewNotFound"
73
+
]
74
+
},
75
+
"issuerRepo": {
76
+
"type": "union",
77
+
"refs": [
78
+
"tools.ozone.moderation.defs#repoViewDetail",
79
+
"tools.ozone.moderation.defs#repoViewNotFound"
80
+
]
81
+
}
82
+
}
83
+
}
84
+
}
85
+
}
+92
lexicons/tools/ozone/verification/grantVerifications.json
+92
lexicons/tools/ozone/verification/grantVerifications.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.verification.grantVerifications",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["verifications"],
13
+
"properties": {
14
+
"verifications": {
15
+
"type": "array",
16
+
"description": "Array of verification requests to process",
17
+
"maxLength": 100,
18
+
"items": {
19
+
"type": "ref",
20
+
"ref": "#verificationInput"
21
+
}
22
+
}
23
+
}
24
+
}
25
+
},
26
+
"output": {
27
+
"encoding": "application/json",
28
+
"schema": {
29
+
"type": "object",
30
+
"required": ["verifications", "failedVerifications"],
31
+
"properties": {
32
+
"verifications": {
33
+
"type": "array",
34
+
"items": {
35
+
"type": "ref",
36
+
"ref": "tools.ozone.verification.defs#verificationView"
37
+
}
38
+
},
39
+
"failedVerifications": {
40
+
"type": "array",
41
+
"items": {
42
+
"type": "ref",
43
+
"ref": "#grantError"
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
49
+
},
50
+
"verificationInput": {
51
+
"type": "object",
52
+
"required": ["subject", "handle", "displayName"],
53
+
"properties": {
54
+
"subject": {
55
+
"type": "string",
56
+
"description": "The did of the subject being verified",
57
+
"format": "did"
58
+
},
59
+
"handle": {
60
+
"type": "string",
61
+
"description": "Handle of the subject the verification applies to at the moment of verifying.",
62
+
"format": "handle"
63
+
},
64
+
"displayName": {
65
+
"type": "string",
66
+
"description": "Display name of the subject the verification applies to at the moment of verifying."
67
+
},
68
+
"createdAt": {
69
+
"type": "string",
70
+
"format": "datetime",
71
+
"description": "Timestamp for verification record. Defaults to current time when not specified."
72
+
}
73
+
}
74
+
},
75
+
"grantError": {
76
+
"type": "object",
77
+
"description": "Error object for failed verifications.",
78
+
"required": ["error", "subject"],
79
+
"properties": {
80
+
"error": {
81
+
"type": "string",
82
+
"description": "Error message describing the reason for failure."
83
+
},
84
+
"subject": {
85
+
"type": "string",
86
+
"description": "The did of the subject being verified",
87
+
"format": "did"
88
+
}
89
+
}
90
+
}
91
+
}
92
+
}
+83
lexicons/tools/ozone/verification/listVerifications.json
+83
lexicons/tools/ozone/verification/listVerifications.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.verification.listVerifications",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "List verifications",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"cursor": {
12
+
"type": "string",
13
+
"description": "Pagination cursor"
14
+
},
15
+
"limit": {
16
+
"type": "integer",
17
+
"description": "Maximum number of results to return",
18
+
"minimum": 1,
19
+
"maximum": 100,
20
+
"default": 50
21
+
},
22
+
"createdAfter": {
23
+
"type": "string",
24
+
"format": "datetime",
25
+
"description": "Filter to verifications created after this timestamp"
26
+
},
27
+
"createdBefore": {
28
+
"type": "string",
29
+
"format": "datetime",
30
+
"description": "Filter to verifications created before this timestamp"
31
+
},
32
+
"issuers": {
33
+
"type": "array",
34
+
"maxLength": 100,
35
+
"description": "Filter to verifications from specific issuers",
36
+
"items": {
37
+
"type": "string",
38
+
"format": "did"
39
+
}
40
+
},
41
+
"subjects": {
42
+
"type": "array",
43
+
"description": "Filter to specific verified DIDs",
44
+
"maxLength": 100,
45
+
"items": {
46
+
"type": "string",
47
+
"format": "did"
48
+
}
49
+
},
50
+
"sortDirection": {
51
+
"type": "string",
52
+
"description": "Sort direction for creation date",
53
+
"enum": ["asc", "desc"],
54
+
"default": "desc"
55
+
},
56
+
"isRevoked": {
57
+
"type": "boolean",
58
+
"description": "Filter to verifications that are revoked or not. By default, includes both."
59
+
}
60
+
}
61
+
},
62
+
"output": {
63
+
"encoding": "application/json",
64
+
"schema": {
65
+
"type": "object",
66
+
"required": ["verifications"],
67
+
"properties": {
68
+
"cursor": {
69
+
"type": "string"
70
+
},
71
+
"verifications": {
72
+
"type": "array",
73
+
"items": {
74
+
"type": "ref",
75
+
"ref": "tools.ozone.verification.defs#verificationView"
76
+
}
77
+
}
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
83
+
}
+75
lexicons/tools/ozone/verification/revokeVerifications.json
+75
lexicons/tools/ozone/verification/revokeVerifications.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "tools.ozone.verification.revokeVerifications",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Revoke previously granted verifications in batches of up to 100.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["uris"],
13
+
"properties": {
14
+
"uris": {
15
+
"type": "array",
16
+
"description": "Array of verification record uris to revoke",
17
+
"maxLength": 100,
18
+
"items": {
19
+
"type": "string",
20
+
"description": "The AT-URI of the verification record to revoke.",
21
+
"format": "at-uri"
22
+
}
23
+
},
24
+
"revokeReason": {
25
+
"type": "string",
26
+
"description": "Reason for revoking the verification. This is optional and can be omitted if not needed.",
27
+
"maxLength": 1000
28
+
}
29
+
}
30
+
}
31
+
},
32
+
"output": {
33
+
"encoding": "application/json",
34
+
"schema": {
35
+
"type": "object",
36
+
"required": ["revokedVerifications", "failedRevocations"],
37
+
"properties": {
38
+
"revokedVerifications": {
39
+
"type": "array",
40
+
"description": "List of verification uris successfully revoked",
41
+
"items": {
42
+
"type": "string",
43
+
"format": "at-uri"
44
+
}
45
+
},
46
+
"failedRevocations": {
47
+
"type": "array",
48
+
"description": "List of verification uris that couldn't be revoked, including failure reasons",
49
+
"items": {
50
+
"type": "ref",
51
+
"ref": "#revokeError"
52
+
}
53
+
}
54
+
}
55
+
}
56
+
}
57
+
},
58
+
"revokeError": {
59
+
"type": "object",
60
+
"description": "Error object for failed revocations",
61
+
"required": ["uri", "error"],
62
+
"properties": {
63
+
"uri": {
64
+
"type": "string",
65
+
"description": "The AT-URI of the verification record that failed to revoke.",
66
+
"format": "at-uri"
67
+
},
68
+
"error": {
69
+
"type": "string",
70
+
"description": "Description of the error that occurred during revocation."
71
+
}
72
+
}
73
+
}
74
+
}
75
+
}
+191
-260
lib/api/agent.ml
+191
-260
lib/api/agent.ml
···
6
6
open Atproto_syntax
7
7
open Atproto_xrpc
8
8
9
-
(** {1 Types} *)
10
-
11
9
type session = {
12
10
did : string;
13
11
handle : string;
···
15
13
refresh_jwt : string option;
16
14
pds_endpoint : Uri.t;
17
15
}
18
-
(** Authenticated session *)
19
16
20
17
type t = { client : Client.t; session : session option }
21
-
(** API agent *)
22
18
23
19
type error =
24
20
| Not_authenticated
···
32
28
| Parse_error msg -> Printf.sprintf "Parse error: %s" msg
33
29
| Invalid_response msg -> Printf.sprintf "Invalid response: %s" msg
34
30
35
-
(** {1 Agent Creation} *)
36
-
37
-
(** Create an unauthenticated agent *)
38
31
let create ~pds =
39
32
let client = Client.of_uri pds in
40
33
{ client; session = None }
41
34
42
-
(** Create agent from base URL string *)
43
35
let create_from_url ~url =
44
36
let client = Client.create ~base_url:url in
45
37
{ client; session = None }
46
38
47
-
(** Get the underlying client *)
48
39
let client t = t.client
49
-
50
-
(** Check if agent is authenticated *)
51
40
let is_authenticated t = Option.is_some t.session
52
-
53
-
(** Get current session *)
54
41
let session t = t.session
55
-
56
-
(** Get current DID if authenticated *)
57
42
let did t = Option.map (fun s -> s.did) t.session
58
-
59
-
(** Get current handle if authenticated *)
60
43
let handle t = Option.map (fun s -> s.handle) t.session
61
44
62
-
(** {1 Authentication} *)
63
-
64
-
(** Login with identifier (handle or email) and password *)
65
45
let login t ~identifier ~password =
66
46
match Client.create_session t.client ~identifier ~password with
67
47
| Error e -> Error (Xrpc_error e)
68
48
| Ok json -> (
69
-
match json with
70
-
| `Assoc pairs -> (
71
-
let get_string key =
72
-
match List.assoc_opt key pairs with
73
-
| Some (`String s) -> Some s
74
-
| _ -> None
75
-
in
49
+
match Atproto_json.to_object_opt json with
50
+
| Some pairs -> (
76
51
match
77
-
(get_string "did", get_string "handle", get_string "accessJwt")
52
+
( Atproto_json.get_string_opt "did" pairs,
53
+
Atproto_json.get_string_opt "handle" pairs,
54
+
Atproto_json.get_string_opt "accessJwt" pairs )
78
55
with
79
56
| Some did, Some handle, Some access_jwt ->
80
-
let refresh_jwt = get_string "refreshJwt" in
57
+
let refresh_jwt =
58
+
Atproto_json.get_string_opt "refreshJwt" pairs
59
+
in
81
60
let session =
82
61
{
83
62
did;
···
90
69
let client = Client.with_auth ~token:access_jwt t.client in
91
70
Ok { client; session = Some session }
92
71
| _ -> Error (Invalid_response "Missing required session fields"))
93
-
| _ -> Error (Invalid_response "Expected object"))
72
+
| None -> Error (Invalid_response "Expected object"))
94
73
95
-
(** Logout - clears session *)
96
74
let logout t =
97
75
match t.session with
98
76
| None -> Ok { t with client = Client.without_auth t.client }
99
77
| Some _ ->
100
-
(* Call deleteSession if we have a session *)
101
78
let _ = Client.delete_session t.client in
102
79
Ok { client = Client.without_auth t.client; session = None }
103
80
104
-
(** Refresh the access token using refresh token *)
105
81
let refresh_session t =
106
82
match t.session with
107
83
| None -> Error Not_authenticated
···
109
85
match session.refresh_jwt with
110
86
| None -> Error (Invalid_response "No refresh token available")
111
87
| Some refresh_token -> (
112
-
(* Use refresh token for this request *)
113
88
let refresh_client = Client.with_auth ~token:refresh_token t.client in
114
89
match Client.refresh_session refresh_client with
115
90
| Error e -> Error (Xrpc_error e)
116
91
| Ok json -> (
117
-
match json with
118
-
| `Assoc pairs -> (
119
-
let get_string key =
120
-
match List.assoc_opt key pairs with
121
-
| Some (`String s) -> Some s
122
-
| _ -> None
123
-
in
92
+
match Atproto_json.to_object_opt json with
93
+
| Some pairs -> (
124
94
match
125
-
( get_string "did",
126
-
get_string "handle",
127
-
get_string "accessJwt" )
95
+
( Atproto_json.get_string_opt "did" pairs,
96
+
Atproto_json.get_string_opt "handle" pairs,
97
+
Atproto_json.get_string_opt "accessJwt" pairs )
128
98
with
129
99
| Some did, Some handle, Some access_jwt ->
130
-
let refresh_jwt = get_string "refreshJwt" in
100
+
let refresh_jwt =
101
+
Atproto_json.get_string_opt "refreshJwt" pairs
102
+
in
131
103
let new_session =
132
104
{ session with did; handle; access_jwt; refresh_jwt }
133
105
in
···
138
110
| _ ->
139
111
Error (Invalid_response "Missing required session fields")
140
112
)
141
-
| _ -> Error (Invalid_response "Expected object"))))
113
+
| None -> Error (Invalid_response "Expected object"))))
142
114
143
-
(** Get current session info *)
144
115
let get_session t =
145
116
match t.session with
146
117
| None -> Error Not_authenticated
···
148
119
match Client.get_session t.client with
149
120
| Error e -> Error (Xrpc_error e)
150
121
| Ok json -> Ok json)
151
-
152
-
(** {1 Profile Operations} *)
153
122
154
123
type profile = {
155
124
did : string;
···
162
131
follows_count : int;
163
132
posts_count : int;
164
133
}
165
-
(** User profile *)
166
134
167
-
(** Parse profile from JSON *)
168
135
let parse_profile json =
169
-
match json with
170
-
| `Assoc pairs -> (
171
-
let get_string key =
172
-
match List.assoc_opt key pairs with
173
-
| Some (`String s) -> Some s
174
-
| _ -> None
175
-
in
176
-
let get_int key =
177
-
match List.assoc_opt key pairs with Some (`Int i) -> i | _ -> 0
178
-
in
179
-
match (get_string "did", get_string "handle") with
136
+
match Atproto_json.to_object_opt json with
137
+
| Some pairs -> (
138
+
match
139
+
( Atproto_json.get_string_opt "did" pairs,
140
+
Atproto_json.get_string_opt "handle" pairs )
141
+
with
180
142
| Some did, Some handle ->
181
143
Ok
182
144
{
183
145
did;
184
146
handle;
185
-
display_name = get_string "displayName";
186
-
description = get_string "description";
187
-
avatar = get_string "avatar";
188
-
banner = get_string "banner";
189
-
followers_count = get_int "followersCount";
190
-
follows_count = get_int "followsCount";
191
-
posts_count = get_int "postsCount";
147
+
display_name = Atproto_json.get_string_opt "displayName" pairs;
148
+
description = Atproto_json.get_string_opt "description" pairs;
149
+
avatar = Atproto_json.get_string_opt "avatar" pairs;
150
+
banner = Atproto_json.get_string_opt "banner" pairs;
151
+
followers_count =
152
+
(match Atproto_json.get_int_opt "followersCount" pairs with
153
+
| Some i -> i
154
+
| None -> 0);
155
+
follows_count =
156
+
(match Atproto_json.get_int_opt "followsCount" pairs with
157
+
| Some i -> i
158
+
| None -> 0);
159
+
posts_count =
160
+
(match Atproto_json.get_int_opt "postsCount" pairs with
161
+
| Some i -> i
162
+
| None -> 0);
192
163
}
193
164
| _ -> Error (Invalid_response "Missing did or handle"))
194
-
| _ -> Error (Invalid_response "Expected object")
165
+
| None -> Error (Invalid_response "Expected object")
195
166
196
-
(** Get a user's profile *)
197
167
let get_profile t ~actor =
198
168
match Nsid.of_string "app.bsky.actor.getProfile" with
199
169
| Error _ -> Error (Parse_error "invalid nsid")
···
202
172
| Error e -> Error (Xrpc_error e)
203
173
| Ok json -> parse_profile json)
204
174
205
-
(** {1 Post Operations} *)
206
-
207
175
type post_ref = { uri : string; cid : string }
208
-
(** Reference to a post *)
209
-
210
176
type reply_ref = { root : post_ref; parent : post_ref }
211
-
(** Reference for replies *)
212
177
213
-
(** Parse post reference from JSON *)
214
178
let parse_post_ref json =
215
-
match json with
216
-
| `Assoc pairs ->
217
-
let uri =
218
-
match List.assoc_opt "uri" pairs with Some (`String s) -> s | _ -> ""
219
-
in
220
-
let cid =
221
-
match List.assoc_opt "cid" pairs with Some (`String s) -> s | _ -> ""
222
-
in
223
-
if uri <> "" && cid <> "" then Ok { uri; cid }
224
-
else Error (Invalid_response "Missing uri or cid")
225
-
| _ -> Error (Invalid_response "Expected object")
179
+
match Atproto_json.to_object_opt json with
180
+
| Some pairs -> (
181
+
match
182
+
( Atproto_json.get_string_opt "uri" pairs,
183
+
Atproto_json.get_string_opt "cid" pairs )
184
+
with
185
+
| Some uri, Some cid -> Ok { uri; cid }
186
+
| _ -> Error (Invalid_response "Missing uri or cid"))
187
+
| None -> Error (Invalid_response "Expected object")
188
+
189
+
let make_timestamp () =
190
+
let t = Unix.gettimeofday () in
191
+
let tm = Unix.gmtime t in
192
+
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ" (tm.Unix.tm_year + 1900)
193
+
(tm.Unix.tm_mon + 1) tm.Unix.tm_mday tm.Unix.tm_hour tm.Unix.tm_min
194
+
tm.Unix.tm_sec
195
+
(int_of_float ((t -. floor t) *. 1000.))
226
196
227
-
(** Create a new post *)
228
197
let create_post t ~text ?reply ?langs () =
229
198
match t.session with
230
199
| None -> Error Not_authenticated
···
232
201
match Nsid.of_string "com.atproto.repo.createRecord" with
233
202
| Error _ -> Error (Parse_error "invalid nsid")
234
203
| Ok nsid -> (
235
-
(* Build record *)
236
-
let now =
237
-
let t = Unix.gettimeofday () in
238
-
let tm = Unix.gmtime t in
239
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ"
240
-
(tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
241
-
tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec
242
-
(int_of_float ((t -. floor t) *. 1000.))
243
-
in
204
+
let now = make_timestamp () in
244
205
let record =
245
206
[
246
-
("$type", `String "app.bsky.feed.post");
247
-
("text", `String text);
248
-
("createdAt", `String now);
207
+
("$type", Atproto_json.string "app.bsky.feed.post");
208
+
("text", Atproto_json.string text);
209
+
("createdAt", Atproto_json.string now);
249
210
]
250
211
in
251
212
let record =
252
213
match reply with
253
214
| Some r ->
254
215
( "reply",
255
-
`Assoc
216
+
Atproto_json.object_
256
217
[
257
218
( "root",
258
-
`Assoc
219
+
Atproto_json.object_
259
220
[
260
-
("uri", `String r.root.uri);
261
-
("cid", `String r.root.cid);
221
+
("uri", Atproto_json.string r.root.uri);
222
+
("cid", Atproto_json.string r.root.cid);
262
223
] );
263
224
( "parent",
264
-
`Assoc
225
+
Atproto_json.object_
265
226
[
266
-
("uri", `String r.parent.uri);
267
-
("cid", `String r.parent.cid);
227
+
("uri", Atproto_json.string r.parent.uri);
228
+
("cid", Atproto_json.string r.parent.cid);
268
229
] );
269
230
] )
270
231
:: record
···
273
234
let record =
274
235
match langs with
275
236
| Some ls ->
276
-
("langs", `List (List.map (fun l -> `String l) ls)) :: record
237
+
("langs", Atproto_json.array (List.map Atproto_json.string ls))
238
+
:: record
277
239
| None -> record
278
240
in
279
241
let input =
280
-
`Assoc
242
+
Atproto_json.object_
281
243
[
282
-
("repo", `String session.did);
283
-
("collection", `String "app.bsky.feed.post");
284
-
("record", `Assoc record);
244
+
("repo", Atproto_json.string session.did);
245
+
("collection", Atproto_json.string "app.bsky.feed.post");
246
+
("record", Atproto_json.object_ record);
285
247
]
286
248
in
287
249
match Client.procedure t.client ~nsid ~input () with
288
250
| Error e -> Error (Xrpc_error e)
289
251
| Ok json -> parse_post_ref json))
290
252
291
-
(** Create a post with rich text *)
292
253
let create_post_richtext t ~richtext ?reply ?langs () =
293
254
match t.session with
294
255
| None -> Error Not_authenticated
···
296
257
match Nsid.of_string "com.atproto.repo.createRecord" with
297
258
| Error _ -> Error (Parse_error "invalid nsid")
298
259
| Ok nsid -> (
299
-
let now =
300
-
let t = Unix.gettimeofday () in
301
-
let tm = Unix.gmtime t in
302
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ"
303
-
(tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
304
-
tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec
305
-
(int_of_float ((t -. floor t) *. 1000.))
306
-
in
260
+
let now = make_timestamp () in
307
261
let rt_json = Richtext.to_json richtext in
308
262
let base_record =
309
-
match rt_json with `Assoc pairs -> pairs | _ -> []
263
+
match Atproto_json.to_object_opt rt_json with
264
+
| Some pairs -> pairs
265
+
| None -> []
310
266
in
311
267
let record =
312
-
("$type", `String "app.bsky.feed.post")
313
-
:: ("createdAt", `String now)
268
+
("$type", Atproto_json.string "app.bsky.feed.post")
269
+
:: ("createdAt", Atproto_json.string now)
314
270
:: base_record
315
271
in
316
272
let record =
317
273
match reply with
318
274
| Some r ->
319
275
( "reply",
320
-
`Assoc
276
+
Atproto_json.object_
321
277
[
322
278
( "root",
323
-
`Assoc
279
+
Atproto_json.object_
324
280
[
325
-
("uri", `String r.root.uri);
326
-
("cid", `String r.root.cid);
281
+
("uri", Atproto_json.string r.root.uri);
282
+
("cid", Atproto_json.string r.root.cid);
327
283
] );
328
284
( "parent",
329
-
`Assoc
285
+
Atproto_json.object_
330
286
[
331
-
("uri", `String r.parent.uri);
332
-
("cid", `String r.parent.cid);
287
+
("uri", Atproto_json.string r.parent.uri);
288
+
("cid", Atproto_json.string r.parent.cid);
333
289
] );
334
290
] )
335
291
:: record
···
338
294
let record =
339
295
match langs with
340
296
| Some ls ->
341
-
("langs", `List (List.map (fun l -> `String l) ls)) :: record
297
+
("langs", Atproto_json.array (List.map Atproto_json.string ls))
298
+
:: record
342
299
| None -> record
343
300
in
344
301
let input =
345
-
`Assoc
302
+
Atproto_json.object_
346
303
[
347
-
("repo", `String session.did);
348
-
("collection", `String "app.bsky.feed.post");
349
-
("record", `Assoc record);
304
+
("repo", Atproto_json.string session.did);
305
+
("collection", Atproto_json.string "app.bsky.feed.post");
306
+
("record", Atproto_json.object_ record);
350
307
]
351
308
in
352
309
match Client.procedure t.client ~nsid ~input () with
353
310
| Error e -> Error (Xrpc_error e)
354
311
| Ok json -> parse_post_ref json))
355
312
356
-
(** Delete a post *)
357
313
let delete_post t ~uri =
358
314
match t.session with
359
315
| None -> Error Not_authenticated
···
361
317
match Nsid.of_string "com.atproto.repo.deleteRecord" with
362
318
| Error _ -> Error (Parse_error "invalid nsid")
363
319
| Ok nsid -> (
364
-
(* Parse AT-URI to extract rkey *)
365
320
match At_uri.of_string uri with
366
321
| Error _ -> Error (Parse_error "invalid AT-URI")
367
322
| Ok at_uri -> (
···
369
324
match At_uri.rkey at_uri with Some r -> r | None -> ""
370
325
in
371
326
let input =
372
-
`Assoc
327
+
Atproto_json.object_
373
328
[
374
-
("repo", `String session.did);
375
-
("collection", `String "app.bsky.feed.post");
376
-
("rkey", `String rkey);
329
+
("repo", Atproto_json.string session.did);
330
+
("collection", Atproto_json.string "app.bsky.feed.post");
331
+
("rkey", Atproto_json.string rkey);
377
332
]
378
333
in
379
334
match Client.procedure t.client ~nsid ~input () with
380
335
| Error e -> Error (Xrpc_error e)
381
336
| Ok _ -> Ok ())))
382
337
383
-
(** {1 Social Operations} *)
384
-
385
-
(** Like a post *)
386
338
let like t ~uri ~cid =
387
339
match t.session with
388
340
| None -> Error Not_authenticated
···
390
342
match Nsid.of_string "com.atproto.repo.createRecord" with
391
343
| Error _ -> Error (Parse_error "invalid nsid")
392
344
| Ok nsid -> (
393
-
let now =
394
-
let t = Unix.gettimeofday () in
395
-
let tm = Unix.gmtime t in
396
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ"
397
-
(tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
398
-
tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec
399
-
(int_of_float ((t -. floor t) *. 1000.))
400
-
in
345
+
let now = make_timestamp () in
401
346
let input =
402
-
`Assoc
347
+
Atproto_json.object_
403
348
[
404
-
("repo", `String session.did);
405
-
("collection", `String "app.bsky.feed.like");
349
+
("repo", Atproto_json.string session.did);
350
+
("collection", Atproto_json.string "app.bsky.feed.like");
406
351
( "record",
407
-
`Assoc
352
+
Atproto_json.object_
408
353
[
409
-
("$type", `String "app.bsky.feed.like");
354
+
("$type", Atproto_json.string "app.bsky.feed.like");
410
355
( "subject",
411
-
`Assoc [ ("uri", `String uri); ("cid", `String cid) ] );
412
-
("createdAt", `String now);
356
+
Atproto_json.object_
357
+
[
358
+
("uri", Atproto_json.string uri);
359
+
("cid", Atproto_json.string cid);
360
+
] );
361
+
("createdAt", Atproto_json.string now);
413
362
] );
414
363
]
415
364
in
···
417
366
| Error e -> Error (Xrpc_error e)
418
367
| Ok json -> parse_post_ref json))
419
368
420
-
(** Unlike (delete like) *)
421
369
let unlike t ~uri =
422
370
match t.session with
423
371
| None -> Error Not_authenticated
···
432
380
match At_uri.rkey at_uri with Some r -> r | None -> ""
433
381
in
434
382
let input =
435
-
`Assoc
383
+
Atproto_json.object_
436
384
[
437
-
("repo", `String session.did);
438
-
("collection", `String "app.bsky.feed.like");
439
-
("rkey", `String rkey);
385
+
("repo", Atproto_json.string session.did);
386
+
("collection", Atproto_json.string "app.bsky.feed.like");
387
+
("rkey", Atproto_json.string rkey);
440
388
]
441
389
in
442
390
match Client.procedure t.client ~nsid ~input () with
443
391
| Error e -> Error (Xrpc_error e)
444
392
| Ok _ -> Ok ())))
445
393
446
-
(** Follow a user *)
447
394
let follow t ~did =
448
395
match t.session with
449
396
| None -> Error Not_authenticated
···
451
398
match Nsid.of_string "com.atproto.repo.createRecord" with
452
399
| Error _ -> Error (Parse_error "invalid nsid")
453
400
| Ok nsid -> (
454
-
let now =
455
-
let t = Unix.gettimeofday () in
456
-
let tm = Unix.gmtime t in
457
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ"
458
-
(tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
459
-
tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec
460
-
(int_of_float ((t -. floor t) *. 1000.))
461
-
in
401
+
let now = make_timestamp () in
462
402
let input =
463
-
`Assoc
403
+
Atproto_json.object_
464
404
[
465
-
("repo", `String session.did);
466
-
("collection", `String "app.bsky.graph.follow");
405
+
("repo", Atproto_json.string session.did);
406
+
("collection", Atproto_json.string "app.bsky.graph.follow");
467
407
( "record",
468
-
`Assoc
408
+
Atproto_json.object_
469
409
[
470
-
("$type", `String "app.bsky.graph.follow");
471
-
("subject", `String did);
472
-
("createdAt", `String now);
410
+
("$type", Atproto_json.string "app.bsky.graph.follow");
411
+
("subject", Atproto_json.string did);
412
+
("createdAt", Atproto_json.string now);
473
413
] );
474
414
]
475
415
in
···
477
417
| Error e -> Error (Xrpc_error e)
478
418
| Ok json -> parse_post_ref json))
479
419
480
-
(** Unfollow (delete follow) *)
481
420
let unfollow t ~uri =
482
421
match t.session with
483
422
| None -> Error Not_authenticated
···
492
431
match At_uri.rkey at_uri with Some r -> r | None -> ""
493
432
in
494
433
let input =
495
-
`Assoc
434
+
Atproto_json.object_
496
435
[
497
-
("repo", `String session.did);
498
-
("collection", `String "app.bsky.graph.follow");
499
-
("rkey", `String rkey);
436
+
("repo", Atproto_json.string session.did);
437
+
("collection", Atproto_json.string "app.bsky.graph.follow");
438
+
("rkey", Atproto_json.string rkey);
500
439
]
501
440
in
502
441
match Client.procedure t.client ~nsid ~input () with
503
442
| Error e -> Error (Xrpc_error e)
504
443
| Ok _ -> Ok ())))
505
444
506
-
(** Repost a post *)
507
445
let repost t ~uri ~cid =
508
446
match t.session with
509
447
| None -> Error Not_authenticated
···
511
449
match Nsid.of_string "com.atproto.repo.createRecord" with
512
450
| Error _ -> Error (Parse_error "invalid nsid")
513
451
| Ok nsid -> (
514
-
let now =
515
-
let t = Unix.gettimeofday () in
516
-
let tm = Unix.gmtime t in
517
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ"
518
-
(tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
519
-
tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec
520
-
(int_of_float ((t -. floor t) *. 1000.))
521
-
in
452
+
let now = make_timestamp () in
522
453
let input =
523
-
`Assoc
454
+
Atproto_json.object_
524
455
[
525
-
("repo", `String session.did);
526
-
("collection", `String "app.bsky.feed.repost");
456
+
("repo", Atproto_json.string session.did);
457
+
("collection", Atproto_json.string "app.bsky.feed.repost");
527
458
( "record",
528
-
`Assoc
459
+
Atproto_json.object_
529
460
[
530
-
("$type", `String "app.bsky.feed.repost");
461
+
("$type", Atproto_json.string "app.bsky.feed.repost");
531
462
( "subject",
532
-
`Assoc [ ("uri", `String uri); ("cid", `String cid) ] );
533
-
("createdAt", `String now);
463
+
Atproto_json.object_
464
+
[
465
+
("uri", Atproto_json.string uri);
466
+
("cid", Atproto_json.string cid);
467
+
] );
468
+
("createdAt", Atproto_json.string now);
534
469
] );
535
470
]
536
471
in
537
472
match Client.procedure t.client ~nsid ~input () with
538
473
| Error e -> Error (Xrpc_error e)
539
474
| Ok json -> parse_post_ref json))
540
-
541
-
(** {1 Feed Operations} *)
542
475
543
476
type feed_item = {
544
477
post_uri : string;
···
551
484
repost_count : int;
552
485
like_count : int;
553
486
}
554
-
(** A post in a feed *)
555
487
556
488
type feed = { items : feed_item list; cursor : string option }
557
-
(** Feed response with pagination *)
558
489
559
-
(** Parse feed item from JSON *)
560
490
let parse_feed_item json =
561
-
match json with
562
-
| `Assoc pairs -> (
563
-
match List.assoc_opt "post" pairs with
564
-
| Some (`Assoc post_pairs) ->
565
-
let get_string key pairs =
566
-
match List.assoc_opt key pairs with
567
-
| Some (`String s) -> s
568
-
| _ -> ""
491
+
match Atproto_json.to_object_opt json with
492
+
| Some pairs -> (
493
+
match Atproto_json.get_object_opt "post" pairs with
494
+
| Some post_pairs ->
495
+
let uri =
496
+
Option.value ~default:""
497
+
(Atproto_json.get_string_opt "uri" post_pairs)
569
498
in
570
-
let get_int key pairs =
571
-
match List.assoc_opt key pairs with Some (`Int i) -> i | _ -> 0
499
+
let cid =
500
+
Option.value ~default:""
501
+
(Atproto_json.get_string_opt "cid" post_pairs)
572
502
in
573
-
let uri = get_string "uri" post_pairs in
574
-
let cid = get_string "cid" post_pairs in
575
503
let author =
576
-
match List.assoc_opt "author" post_pairs with
577
-
| Some (`Assoc a) -> a
578
-
| _ -> []
504
+
match Atproto_json.get_object_opt "author" post_pairs with
505
+
| Some a -> a
506
+
| None -> []
579
507
in
580
508
let record =
581
-
match List.assoc_opt "record" post_pairs with
582
-
| Some (`Assoc r) -> r
583
-
| _ -> []
509
+
match Atproto_json.get_object_opt "record" post_pairs with
510
+
| Some r -> r
511
+
| None -> []
584
512
in
585
513
Some
586
514
{
587
515
post_uri = uri;
588
516
post_cid = cid;
589
-
author_did = get_string "did" author;
590
-
author_handle = get_string "handle" author;
591
-
text = get_string "text" record;
592
-
created_at = get_string "createdAt" record;
593
-
reply_count = get_int "replyCount" post_pairs;
594
-
repost_count = get_int "repostCount" post_pairs;
595
-
like_count = get_int "likeCount" post_pairs;
517
+
author_did =
518
+
Option.value ~default:""
519
+
(Atproto_json.get_string_opt "did" author);
520
+
author_handle =
521
+
Option.value ~default:""
522
+
(Atproto_json.get_string_opt "handle" author);
523
+
text =
524
+
Option.value ~default:""
525
+
(Atproto_json.get_string_opt "text" record);
526
+
created_at =
527
+
Option.value ~default:""
528
+
(Atproto_json.get_string_opt "createdAt" record);
529
+
reply_count =
530
+
Option.value ~default:0
531
+
(Atproto_json.get_int_opt "replyCount" post_pairs);
532
+
repost_count =
533
+
Option.value ~default:0
534
+
(Atproto_json.get_int_opt "repostCount" post_pairs);
535
+
like_count =
536
+
Option.value ~default:0
537
+
(Atproto_json.get_int_opt "likeCount" post_pairs);
596
538
}
597
-
| _ -> None)
598
-
| _ -> None
539
+
| None -> None)
540
+
| None -> None
599
541
600
-
(** Get timeline (following feed) *)
601
542
let get_timeline t ?cursor ?limit () =
602
543
match t.session with
603
544
| None -> Error Not_authenticated
···
619
560
match Client.query t.client ~nsid ~params () with
620
561
| Error e -> Error (Xrpc_error e)
621
562
| Ok json -> (
622
-
match json with
623
-
| `Assoc pairs ->
563
+
match Atproto_json.to_object_opt json with
564
+
| Some pairs ->
624
565
let items =
625
-
match List.assoc_opt "feed" pairs with
626
-
| Some (`List items) ->
627
-
List.filter_map parse_feed_item items
628
-
| _ -> []
629
-
in
630
-
let cursor =
631
-
match List.assoc_opt "cursor" pairs with
632
-
| Some (`String s) -> Some s
633
-
| _ -> None
566
+
match Atproto_json.get_array_opt "feed" pairs with
567
+
| Some items -> List.filter_map parse_feed_item items
568
+
| None -> []
634
569
in
570
+
let cursor = Atproto_json.get_string_opt "cursor" pairs in
635
571
Ok { items; cursor }
636
-
| _ -> Error (Invalid_response "Expected object"))))
572
+
| None -> Error (Invalid_response "Expected object"))))
637
573
638
-
(** Get author's feed *)
639
574
let get_author_feed t ~actor ?cursor ?limit () =
640
575
match Nsid.of_string "app.bsky.feed.getAuthorFeed" with
641
576
| Error _ -> Error (Parse_error "invalid nsid")
···
652
587
match Client.query t.client ~nsid ~params () with
653
588
| Error e -> Error (Xrpc_error e)
654
589
| Ok json -> (
655
-
match json with
656
-
| `Assoc pairs ->
590
+
match Atproto_json.to_object_opt json with
591
+
| Some pairs ->
657
592
let items =
658
-
match List.assoc_opt "feed" pairs with
659
-
| Some (`List items) -> List.filter_map parse_feed_item items
660
-
| _ -> []
661
-
in
662
-
let cursor =
663
-
match List.assoc_opt "cursor" pairs with
664
-
| Some (`String s) -> Some s
665
-
| _ -> None
593
+
match Atproto_json.get_array_opt "feed" pairs with
594
+
| Some items -> List.filter_map parse_feed_item items
595
+
| None -> []
666
596
in
597
+
let cursor = Atproto_json.get_string_opt "cursor" pairs in
667
598
Ok { items; cursor }
668
-
| _ -> Error (Invalid_response "Expected object")))
599
+
| None -> Error (Invalid_response "Expected object")))
+8
-1
lib/api/dune
+8
-1
lib/api/dune
+80
-55
lib/api/richtext.ml
+80
-55
lib/api/richtext.ml
···
177
177
178
178
(** {1 JSON Encoding} *)
179
179
180
+
type json = Atproto_json.t
181
+
180
182
(** Encode byte slice to JSON *)
181
-
let byte_slice_to_json slice =
182
-
`Assoc
183
-
[ ("byteStart", `Int slice.byte_start); ("byteEnd", `Int slice.byte_end) ]
183
+
let byte_slice_to_json slice : json =
184
+
Atproto_json.object_
185
+
[
186
+
("byteStart", Atproto_json.int slice.byte_start);
187
+
("byteEnd", Atproto_json.int slice.byte_end);
188
+
]
184
189
185
190
(** Encode feature to JSON *)
186
191
let feature_to_json = function
187
192
| Mention { did } ->
188
-
`Assoc
193
+
Atproto_json.object_
189
194
[
190
-
("$type", `String "app.bsky.richtext.facet#mention");
191
-
("did", `String did);
195
+
("$type", Atproto_json.string "app.bsky.richtext.facet#mention");
196
+
("did", Atproto_json.string did);
192
197
]
193
198
| Link { uri } ->
194
-
`Assoc
199
+
Atproto_json.object_
195
200
[
196
-
("$type", `String "app.bsky.richtext.facet#link"); ("uri", `String uri);
201
+
("$type", Atproto_json.string "app.bsky.richtext.facet#link");
202
+
("uri", Atproto_json.string uri);
197
203
]
198
204
| Tag { tag } ->
199
-
`Assoc
205
+
Atproto_json.object_
200
206
[
201
-
("$type", `String "app.bsky.richtext.facet#tag"); ("tag", `String tag);
207
+
("$type", Atproto_json.string "app.bsky.richtext.facet#tag");
208
+
("tag", Atproto_json.string tag);
202
209
]
203
210
204
211
(** Encode facet to JSON *)
205
-
let facet_to_json facet =
206
-
`Assoc
212
+
let facet_to_json facet : json =
213
+
Atproto_json.object_
207
214
[
208
215
("index", byte_slice_to_json facet.index);
209
-
("features", `List (List.map feature_to_json facet.features));
216
+
("features", Atproto_json.array (List.map feature_to_json facet.features));
210
217
]
211
218
212
219
(** Encode rich text to JSON (for post record) *)
213
-
let to_json t =
214
-
if t.facets = [] then `Assoc [ ("text", `String t.text) ]
220
+
let to_json t : json =
221
+
if t.facets = [] then
222
+
Atproto_json.object_ [ ("text", Atproto_json.string t.text) ]
215
223
else
216
-
`Assoc
224
+
Atproto_json.object_
217
225
[
218
-
("text", `String t.text);
219
-
("facets", `List (List.map facet_to_json t.facets));
226
+
("text", Atproto_json.string t.text);
227
+
("facets", Atproto_json.array (List.map facet_to_json t.facets));
220
228
]
221
229
222
230
(** {1 JSON Decoding} *)
231
+
232
+
let int_of_json ?(default = 0) (json : json) : int =
233
+
match Atproto_json.to_int64_opt json with
234
+
| Some i ->
235
+
if i > Int64.of_int max_int || i < Int64.of_int min_int then default
236
+
else Int64.to_int i
237
+
| None -> default
223
238
224
239
(** Decode byte slice from JSON *)
225
240
let byte_slice_of_json json =
226
-
match json with
227
-
| `Assoc pairs ->
241
+
match Atproto_json.to_object_opt json with
242
+
| Some pairs ->
228
243
let byte_start =
229
-
match List.assoc_opt "byteStart" pairs with
230
-
| Some (`Int i) -> i
231
-
| _ -> 0
244
+
match Atproto_json.get "byteStart" pairs with
245
+
| Some v -> int_of_json v
246
+
| None -> 0
232
247
in
233
248
let byte_end =
234
-
match List.assoc_opt "byteEnd" pairs with Some (`Int i) -> i | _ -> 0
249
+
match Atproto_json.get "byteEnd" pairs with
250
+
| Some v -> int_of_json v
251
+
| None -> 0
235
252
in
236
253
Some { byte_start; byte_end }
237
-
| _ -> None
254
+
| None -> None
238
255
239
256
(** Decode feature from JSON *)
240
257
let feature_of_json json =
241
-
match json with
242
-
| `Assoc pairs ->
258
+
match Atproto_json.to_object_opt json with
259
+
| Some pairs ->
243
260
let type_ =
244
-
match List.assoc_opt "$type" pairs with
245
-
| Some (`String s) -> s
246
-
| _ -> ""
261
+
match Atproto_json.get_string_opt "$type" pairs with
262
+
| Some s -> s
263
+
| None -> ""
247
264
in
248
265
if type_ = "app.bsky.richtext.facet#mention" then
249
-
match List.assoc_opt "did" pairs with
250
-
| Some (`String did) -> Some (Mention { did })
251
-
| _ -> None
266
+
match Atproto_json.get_string_opt "did" pairs with
267
+
| Some did -> Some (Mention { did })
268
+
| None -> None
252
269
else if type_ = "app.bsky.richtext.facet#link" then
253
-
match List.assoc_opt "uri" pairs with
254
-
| Some (`String uri) -> Some (Link { uri })
255
-
| _ -> None
270
+
match Atproto_json.get_string_opt "uri" pairs with
271
+
| Some uri -> Some (Link { uri })
272
+
| None -> None
256
273
else if type_ = "app.bsky.richtext.facet#tag" then
257
-
match List.assoc_opt "tag" pairs with
258
-
| Some (`String tag) -> Some (Tag { tag })
259
-
| _ -> None
274
+
match Atproto_json.get_string_opt "tag" pairs with
275
+
| Some tag -> Some (Tag { tag })
276
+
| None -> None
260
277
else None
261
-
| _ -> None
278
+
| None -> None
262
279
263
280
(** Decode facet from JSON *)
264
281
let facet_of_json json =
265
-
match json with
266
-
| `Assoc pairs -> (
282
+
match Atproto_json.to_object_opt json with
283
+
| Some pairs -> (
267
284
let index =
268
-
match List.assoc_opt "index" pairs with
285
+
match Atproto_json.get "index" pairs with
269
286
| Some idx -> byte_slice_of_json idx
270
-
| _ -> None
287
+
| None -> None
271
288
in
272
289
let features =
273
-
match List.assoc_opt "features" pairs with
274
-
| Some (`List items) -> List.filter_map feature_of_json items
275
-
| _ -> []
290
+
match Atproto_json.get "features" pairs with
291
+
| Some items -> (
292
+
match Atproto_json.to_array_opt items with
293
+
| Some items -> List.filter_map feature_of_json items
294
+
| None -> [])
295
+
| None -> []
276
296
in
277
297
match index with Some index -> Some { index; features } | None -> None)
278
-
| _ -> None
298
+
| None -> None
279
299
280
300
(** Decode rich text from JSON *)
281
301
let of_json json =
282
-
match json with
283
-
| `Assoc pairs ->
302
+
match Atproto_json.to_object_opt json with
303
+
| Some pairs ->
284
304
let text =
285
-
match List.assoc_opt "text" pairs with Some (`String s) -> s | _ -> ""
305
+
match Atproto_json.get_string_opt "text" pairs with
306
+
| Some s -> s
307
+
| None -> ""
286
308
in
287
309
let facets =
288
-
match List.assoc_opt "facets" pairs with
289
-
| Some (`List items) -> List.filter_map facet_of_json items
290
-
| _ -> []
310
+
match Atproto_json.get "facets" pairs with
311
+
| Some items -> (
312
+
match Atproto_json.to_array_opt items with
313
+
| Some items -> List.filter_map facet_of_json items
314
+
| None -> [])
315
+
| None -> []
291
316
in
292
317
Some { text; facets }
293
-
| _ -> None
318
+
| None -> None
294
319
295
320
(** {1 Utilities} *)
296
321
+8
-1
lib/crypto/dune
+8
-1
lib/crypto/dune
···
1
1
(library
2
2
(name atproto_crypto)
3
3
(public_name atproto-crypto)
4
-
(libraries atproto_multibase mirage-crypto-ec mirage-crypto-rng digestif zarith base64 yojson simdjsont)
4
+
(libraries
5
+
atproto_multibase
6
+
mirage-crypto-ec
7
+
mirage-crypto-rng
8
+
digestif
9
+
zarith
10
+
base64
11
+
atproto_json)
5
12
(preprocess no_preprocessing))
+27
-46
lib/crypto/jwt.ml
+27
-46
lib/crypto/jwt.ml
···
60
60
61
61
(** {1 Base64url Encoding} *)
62
62
63
-
(** Base64url encode without padding *)
64
63
let base64url_encode (s : string) : string =
65
64
Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet s
66
65
67
-
(** Base64url decode *)
68
66
let base64url_decode (s : string) : (string, [> `Invalid_base64 ]) result =
69
-
(* Add padding if needed *)
70
67
let padded =
71
68
let len = String.length s in
72
69
match len mod 4 with 2 -> s ^ "==" | 3 -> s ^ "=" | _ -> s
···
84
81
| "ES256K" -> Ok ES256K
85
82
| alg -> Error (`Unsupported_algorithm alg)
86
83
87
-
type json = Simdjsont.Json.t
84
+
type json = Atproto_json.t
88
85
89
86
let header_to_json (h : header) : json =
90
-
Simdjsont.Json.Object
87
+
Atproto_json.object_
91
88
[
92
-
("alg", Simdjsont.Json.String (algorithm_to_string h.alg));
93
-
("typ", Simdjsont.Json.String h.typ);
89
+
("alg", Atproto_json.string (algorithm_to_string h.alg));
90
+
("typ", Atproto_json.string h.typ);
94
91
]
95
92
96
93
let header_of_json (json : json) : (header, error) result =
97
-
match json with
98
-
| Simdjsont.Json.Object pairs -> (
94
+
match Atproto_json.to_object_opt json with
95
+
| Some pairs -> (
99
96
let get_string key =
100
97
match List.assoc_opt key pairs with
101
-
| Some (Simdjsont.Json.String s) -> Some s
102
-
| _ -> None
98
+
| Some v -> Atproto_json.to_string_opt v
99
+
| None -> None
103
100
in
104
101
match get_string "alg" with
105
102
| None -> Error (`Missing_claim "alg")
···
110
107
match get_string "typ" with
111
108
| None -> Error (`Missing_claim "typ")
112
109
| Some typ -> Ok { alg; typ })))
113
-
| _ -> Error (`Invalid_json "header must be an object")
110
+
| None -> Error (`Invalid_json "header must be an object")
114
111
115
112
let claims_to_json (c : claims) : json =
116
113
let fields =
117
114
[
118
-
("iss", Simdjsont.Json.String c.iss);
119
-
("aud", Simdjsont.Json.String c.aud);
120
-
("exp", Simdjsont.Json.Int c.exp);
121
-
("iat", Simdjsont.Json.Int c.iat);
115
+
("iss", Atproto_json.string c.iss);
116
+
("aud", Atproto_json.string c.aud);
117
+
("exp", Atproto_json.int64 c.exp);
118
+
("iat", Atproto_json.int64 c.iat);
122
119
]
123
120
in
124
121
let add_opt key = function
125
122
| None -> Fun.id
126
-
| Some v -> fun acc -> (key, Simdjsont.Json.String v) :: acc
123
+
| Some v -> fun acc -> (key, Atproto_json.string v) :: acc
127
124
in
128
125
let fields =
129
126
fields |> add_opt "sub" c.sub |> add_opt "jti" c.jti |> add_opt "lxm" c.lxm
130
127
|> add_opt "nonce" c.nonce |> add_opt "scope" c.scope
131
128
in
132
-
Simdjsont.Json.Object fields
129
+
Atproto_json.object_ fields
133
130
134
131
let claims_of_json (json : json) : (claims, error) result =
135
-
match json with
136
-
| Simdjsont.Json.Object pairs -> (
132
+
match Atproto_json.to_object_opt json with
133
+
| Some pairs -> (
137
134
let get_string key =
138
135
match List.assoc_opt key pairs with
139
-
| Some (Simdjsont.Json.String s) -> Some s
140
-
| _ -> None
136
+
| Some v -> Atproto_json.to_string_opt v
137
+
| None -> None
141
138
in
142
139
let get_int64 key =
143
140
match List.assoc_opt key pairs with
144
-
| Some (Simdjsont.Json.Int i) -> Some i
145
-
| _ -> None
141
+
| Some v -> Atproto_json.to_int64_opt v
142
+
| None -> None
146
143
in
147
144
match
148
145
(get_string "iss", get_string "aud", get_int64 "exp", get_int64 "iat")
···
164
161
| _, None, _, _ -> Error (`Missing_claim "aud")
165
162
| _, _, None, _ -> Error (`Missing_claim "exp")
166
163
| _, _, _, None -> Error (`Missing_claim "iat"))
167
-
| _ -> Error (`Invalid_json "claims must be an object")
164
+
| None -> Error (`Invalid_json "claims must be an object")
168
165
169
166
(** {1 Signing} *)
170
167
···
174
171
| P256_pub of P256.public_key
175
172
| K256_pub of K256.public_key
176
173
177
-
(** Sign data with the appropriate algorithm *)
178
174
let sign_data (key : signing_key) (data : string) : string =
179
175
match key with
180
176
| P256_key priv -> P256.sign priv data
181
177
| K256_key priv -> K256.sign priv data
182
178
183
-
(** Verify signature *)
184
179
let verify_signature (key : verification_key) (data : string)
185
180
(signature : string) : bool =
186
181
match key with
···
195
190
196
191
(** {1 JWT Creation and Verification} *)
197
192
198
-
(** Create and sign a JWT *)
199
193
let create ~(key : signing_key) ~(typ : string) ~(claims : claims) : t =
200
194
let alg = match key with P256_key _ -> ES256 | K256_key _ -> ES256K in
201
195
let header = { alg; typ } in
202
-
let header_json =
203
-
Simdjsont.encode Simdjsont.Codec.value (header_to_json header)
204
-
in
205
-
let claims_json =
206
-
Simdjsont.encode Simdjsont.Codec.value (claims_to_json claims)
207
-
in
196
+
let header_json = Atproto_json.encode (header_to_json header) in
197
+
let claims_json = Atproto_json.encode (claims_to_json claims) in
208
198
let header_b64 = base64url_encode header_json in
209
199
let claims_b64 = base64url_encode claims_json in
210
200
let signing_input = header_b64 ^ "." ^ claims_b64 in
···
213
203
let raw = signing_input ^ "." ^ signature_b64 in
214
204
{ header; claims; signature; raw }
215
205
216
-
(** Decode a JWT without verifying the signature *)
217
206
let decode_unverified (token : string) : (t, error) result =
218
207
match String.split_on_char '.' token with
219
208
| [ header_b64; claims_b64; sig_b64 ] -> (
···
227
216
| Error _ -> Error `Invalid_base64
228
217
| Ok signature -> (
229
218
let header_json =
230
-
match Simdjsont.decode Simdjsont.Codec.value header_str with
219
+
match Atproto_json.decode header_str with
231
220
| Ok v -> Ok v
232
221
| Error msg -> Error (`Invalid_json msg)
233
222
in
234
223
let claims_json =
235
-
match Simdjsont.decode Simdjsont.Codec.value claims_str with
224
+
match Atproto_json.decode claims_str with
236
225
| Ok v -> Ok v
237
226
| Error msg -> Error (`Invalid_json msg)
238
227
in
228
+
239
229
match (header_json, claims_json) with
240
230
| Error e, _ -> Error e
241
231
| _, Error e -> Error e
···
250
240
)
251
241
| _ -> Error `Invalid_format
252
242
253
-
(** Verify a JWT signature *)
254
243
let verify ~(key : verification_key) (token : t) : (t, error) result =
255
-
(* Check algorithm matches key type *)
256
244
let key_matches =
257
245
match (key, token.header.alg) with
258
246
| P256_pub _, ES256 -> true
···
261
249
in
262
250
if not key_matches then Error `Invalid_signature
263
251
else
264
-
(* Extract signing input from raw token *)
265
252
match String.rindex_opt token.raw '.' with
266
253
| None -> Error `Invalid_format
267
254
| Some last_dot ->
···
269
256
if verify_signature key signing_input token.signature then Ok token
270
257
else Error `Invalid_signature
271
258
272
-
(** Check if a token is expired *)
273
259
let check_expiration ~(now : int64) (token : t) : (t, error) result =
274
260
if token.claims.exp < now then Error `Expired else Ok token
275
261
276
-
(** Decode and verify a JWT *)
277
262
let decode_and_verify ~(key : verification_key) ~(now : int64) (token : string)
278
263
: (t, error) result =
279
264
match decode_unverified token with
···
283
268
| Error e -> Error e
284
269
| Ok t -> check_expiration ~now t)
285
270
286
-
(** Convert JWT to string *)
287
271
let to_string (token : t) : string = token.raw
288
272
289
273
(** {1 Convenience Functions} *)
290
274
291
-
(** Create an access token *)
292
275
let create_access_token ~key ~iss ~sub ~aud ~exp ~iat ?scope () =
293
276
let claims =
294
277
{
···
305
288
in
306
289
create ~key ~typ:"at+jwt" ~claims
307
290
308
-
(** Create a refresh token *)
309
291
let create_refresh_token ~key ~iss ~sub ~aud ~exp ~iat ?jti () =
310
292
let claims =
311
293
{
···
322
304
in
323
305
create ~key ~typ:"refresh+jwt" ~claims
324
306
325
-
(** Create a service-to-service token *)
326
307
let create_service_token ~key ~iss ~aud ~exp ~iat ~lxm () =
327
308
let claims =
328
309
{
+40
-38
lib/identity/did_resolver.ml
+40
-38
lib/identity/did_resolver.ml
···
15
15
open Atproto_syntax
16
16
module Effects = Atproto_effects.Effects
17
17
18
+
let get_string_opt = Atproto_json.get_string_opt
19
+
let get_array_opt = Atproto_json.get_array_opt
20
+
18
21
(** {1 Types} *)
19
22
20
23
type verification_method = {
···
70
73
71
74
(** Parse a verification method from JSON *)
72
75
let parse_verification_method json =
73
-
match json with
74
-
| `Assoc pairs ->
76
+
match Atproto_json.to_object_opt json with
77
+
| Some pairs ->
75
78
let id =
76
-
match List.assoc_opt "id" pairs with Some (`String s) -> s | _ -> ""
79
+
match get_string_opt "id" pairs with Some s -> s | None -> ""
77
80
in
78
81
let type_ =
79
-
match List.assoc_opt "type" pairs with Some (`String s) -> s | _ -> ""
82
+
match Atproto_json.get_string_opt "type" pairs with
83
+
| Some s -> s
84
+
| None -> ""
80
85
in
81
86
let controller =
82
-
match List.assoc_opt "controller" pairs with
83
-
| Some (`String s) -> s
84
-
| _ -> ""
87
+
match Atproto_json.get_string_opt "controller" pairs with
88
+
| Some s -> s
89
+
| None -> ""
85
90
in
86
91
let public_key_multibase =
87
-
match List.assoc_opt "publicKeyMultibase" pairs with
88
-
| Some (`String s) -> Some s
89
-
| _ -> None
92
+
Atproto_json.get_string_opt "publicKeyMultibase" pairs
90
93
in
91
94
{ id; type_; controller; public_key_multibase }
92
95
| _ -> { id = ""; type_ = ""; controller = ""; public_key_multibase = None }
93
96
94
97
(** Parse a service from JSON *)
95
98
let parse_service json =
96
-
match json with
97
-
| `Assoc pairs ->
99
+
match Atproto_json.to_object_opt json with
100
+
| Some pairs ->
98
101
let id =
99
-
match List.assoc_opt "id" pairs with Some (`String s) -> s | _ -> ""
102
+
match get_string_opt "id" pairs with Some s -> s | None -> ""
100
103
in
101
104
let type_ =
102
-
match List.assoc_opt "type" pairs with Some (`String s) -> s | _ -> ""
105
+
match Atproto_json.get_string_opt "type" pairs with
106
+
| Some s -> s
107
+
| None -> ""
103
108
in
104
109
let service_endpoint =
105
-
match List.assoc_opt "serviceEndpoint" pairs with
106
-
| Some (`String s) -> s
107
-
| _ -> ""
110
+
match Atproto_json.get_string_opt "serviceEndpoint" pairs with
111
+
| Some s -> s
112
+
| None -> ""
108
113
in
109
114
{ id; type_; service_endpoint }
110
115
| _ -> { id = ""; type_ = ""; service_endpoint = "" }
111
116
112
117
(** Parse a DID document from JSON *)
113
118
let parse_did_document json =
114
-
match json with
115
-
| `Assoc pairs ->
119
+
match Atproto_json.to_object_opt json with
120
+
| Some pairs ->
116
121
let id =
117
-
match List.assoc_opt "id" pairs with Some (`String s) -> s | _ -> ""
122
+
match get_string_opt "id" pairs with Some s -> s | None -> ""
118
123
in
119
124
let also_known_as =
120
-
match List.assoc_opt "alsoKnownAs" pairs with
121
-
| Some (`List items) ->
122
-
List.filter_map (function `String s -> Some s | _ -> None) items
123
-
| _ -> []
125
+
match Atproto_json.get_array_opt "alsoKnownAs" pairs with
126
+
| Some items -> List.filter_map Atproto_json.to_string_opt items
127
+
| None -> []
124
128
in
125
129
let verification_method =
126
-
match List.assoc_opt "verificationMethod" pairs with
127
-
| Some (`List items) -> List.map parse_verification_method items
128
-
| _ -> []
130
+
match Atproto_json.get_array_opt "verificationMethod" pairs with
131
+
| Some items -> List.map parse_verification_method items
132
+
| None -> []
129
133
in
130
134
let service =
131
-
match List.assoc_opt "service" pairs with
132
-
| Some (`List items) -> List.map parse_service items
133
-
| _ -> []
135
+
match Atproto_json.get_array_opt "service" pairs with
136
+
| Some items -> List.map parse_service items
137
+
| None -> []
134
138
in
135
139
Ok { id; also_known_as; verification_method; service }
136
140
| _ -> Error (Parse_error "expected object")
···
148
152
else if response.status >= 400 then
149
153
Error (Http_error (response.status, response.body))
150
154
else
151
-
try
152
-
let json = Yojson.Basic.from_string response.body in
153
-
parse_did_document json
154
-
with Yojson.Json_error msg -> Error (Parse_error msg)
155
+
match Atproto_json.decode response.body with
156
+
| Error msg -> Error (Parse_error msg)
157
+
| Ok json -> parse_did_document json
155
158
156
159
(** Resolve a did:web DID *)
157
160
let resolve_web identifier =
···
192
195
else if response.status >= 400 then
193
196
Error (Http_error (response.status, response.body))
194
197
else
195
-
try
196
-
let json = Yojson.Basic.from_string response.body in
197
-
parse_did_document json
198
-
with Yojson.Json_error msg -> Error (Parse_error msg)
198
+
match Atproto_json.decode response.body with
199
+
| Error msg -> Error (Parse_error msg)
200
+
| Ok json -> parse_did_document json
199
201
200
202
(** Resolve any DID *)
201
203
let resolve did =
+1
-1
lib/identity/dune
+1
-1
lib/identity/dune
+21
-32
lib/ipld/blob.ml
+21
-32
lib/ipld/blob.ml
···
86
86
in
87
87
let size =
88
88
match List.assoc_opt "size" pairs with
89
-
| Some (Dag_cbor.Int i) -> Some (Int64.to_int i)
89
+
| Some (Dag_cbor.Int i) -> Atproto_json.int_of_int64_opt i
90
90
| _ -> None
91
91
in
92
92
match (ref_field, mime_type, size) with
···
105
105
106
106
(** Encode a blob reference to JSON. Uses the standard AT Protocol JSON blob
107
107
format. *)
108
-
let to_json (blob : ref_) : Yojson.Safe.t =
109
-
`Assoc
108
+
let to_json (blob : ref_) : Atproto_json.t =
109
+
Atproto_json.object_
110
110
[
111
-
("$type", `String "blob");
112
-
("ref", `Assoc [ ("$link", `String (Cid.to_string blob.cid)) ]);
113
-
("mimeType", `String blob.mime_type);
114
-
("size", `Int blob.size);
111
+
("$type", Atproto_json.string "blob");
112
+
( "ref",
113
+
Atproto_json.object_
114
+
[ ("$link", Atproto_json.string (Cid.to_string blob.cid)) ] );
115
+
("mimeType", Atproto_json.string blob.mime_type);
116
+
("size", Atproto_json.int blob.size);
115
117
]
116
118
117
119
(** Decode a blob reference from JSON. *)
118
-
let of_json (json : Yojson.Safe.t) : (ref_, error) result =
119
-
match json with
120
-
| `Assoc pairs -> (
121
-
let type_field =
122
-
match List.assoc_opt "$type" pairs with
123
-
| Some (`String s) -> Some s
124
-
| _ -> None
125
-
in
126
-
match type_field with
120
+
let of_json (json : Atproto_json.t) : (ref_, error) result =
121
+
match Atproto_json.to_object_opt json with
122
+
| Some pairs -> (
123
+
match Atproto_json.get_string_opt "$type" pairs with
127
124
| Some "blob" -> (
128
125
let ref_field =
129
-
match List.assoc_opt "ref" pairs with
130
-
| Some (`Assoc ref_pairs) -> (
131
-
match List.assoc_opt "$link" ref_pairs with
132
-
| Some (`String s) -> Cid.of_string s |> Result.to_option
133
-
| _ -> None)
134
-
| _ -> None
135
-
in
136
-
let mime_type =
137
-
match List.assoc_opt "mimeType" pairs with
138
-
| Some (`String s) -> Some s
139
-
| _ -> None
126
+
match Atproto_json.get_object_opt "ref" pairs with
127
+
| Some ref_pairs -> (
128
+
match Atproto_json.get_string_opt "$link" ref_pairs with
129
+
| Some s -> Cid.of_string s |> Result.to_option
130
+
| None -> None)
131
+
| None -> None
140
132
in
141
-
let size =
142
-
match List.assoc_opt "size" pairs with
143
-
| Some (`Int i) -> Some i
144
-
| _ -> None
145
-
in
133
+
let mime_type = Atproto_json.get_string_opt "mimeType" pairs in
134
+
let size = Atproto_json.get_int_opt "size" pairs in
146
135
match (ref_field, mime_type, size) with
147
136
| Some cid, Some mt, Some sz -> Ok { cid; mime_type = mt; size = sz }
148
137
| None, _, _ -> Error (`Missing_field "ref")
+57
-41
lib/ipld/dag_json.ml
+57
-41
lib/ipld/dag_json.ml
···
1
1
(** DAG-JSON codec for IPLD.
2
2
3
3
DAG-JSON is a JSON serialization format for IPLD data that uses special
4
+
4
5
object structures to represent Links and Bytes:
5
6
6
7
- Links: {"/": "<cid-string>"}
···
55
56
match v with
56
57
| Dag_cbor.Null -> `Null
57
58
| Dag_cbor.Bool b -> `Bool b
58
-
| Dag_cbor.Int i ->
59
+
| Dag_cbor.Int i -> (
59
60
(* Check if it fits in int range *)
60
-
let i_int = Int64.to_int i in
61
-
if Int64.of_int i_int = i then `Int i_int else `Float (Int64.to_float i)
61
+
let i_opt =
62
+
if i >= Int64.of_int min_int && i <= Int64.of_int max_int then
63
+
Some (Int64.to_int i)
64
+
else None
65
+
in
66
+
match i_opt with
67
+
| Some i_int -> `Int i_int
68
+
| None -> `Float (Int64.to_float i))
62
69
| Dag_cbor.Float f -> `Float f
63
70
| Dag_cbor.String s -> `String s
64
71
| Dag_cbor.Bytes b ->
···
153
160
in
154
161
decode_pairs [] pairs
155
162
163
+
let rec json_to_atproto_json (j : json) : Atproto_json.t =
164
+
match j with
165
+
| `Null -> Atproto_json.null
166
+
| `Bool b -> Atproto_json.bool b
167
+
| `Int i -> Atproto_json.int i
168
+
| `Float f -> Atproto_json.float f
169
+
| `String s -> Atproto_json.string s
170
+
| `List xs -> Atproto_json.array (List.map json_to_atproto_json xs)
171
+
| `Assoc pairs ->
172
+
Atproto_json.object_
173
+
(List.map (fun (k, v) -> (k, json_to_atproto_json v)) pairs)
174
+
175
+
let rec json_of_atproto_json (t : Atproto_json.t) : json =
176
+
match Atproto_json.to_null_opt t with
177
+
| Some () -> `Null
178
+
| None -> (
179
+
match Atproto_json.to_bool_opt t with
180
+
| Some b -> `Bool b
181
+
| None -> (
182
+
match Atproto_json.to_int64_opt t with
183
+
| Some i64 ->
184
+
if i64 >= Int64.of_int min_int && i64 <= Int64.of_int max_int then
185
+
`Int (Int64.to_int i64)
186
+
else `Float (Int64.to_float i64)
187
+
| None -> (
188
+
match Atproto_json.to_float_opt t with
189
+
| Some f -> `Float f
190
+
| None -> (
191
+
match Atproto_json.to_string_opt t with
192
+
| Some s -> `String s
193
+
| None -> (
194
+
match Atproto_json.to_array_opt t with
195
+
| Some xs -> `List (List.map json_of_atproto_json xs)
196
+
| None -> (
197
+
match Atproto_json.to_object_opt t with
198
+
| Some pairs ->
199
+
`Assoc
200
+
(List.map
201
+
(fun (k, v) -> (k, json_of_atproto_json v))
202
+
pairs)
203
+
| None -> `Null))))))
204
+
156
205
(** Encode an IPLD value to a DAG-JSON string *)
157
206
let encode_string (v : Dag_cbor.value) : string =
158
-
let json = encode v in
159
-
(* Convert to Yojson for string serialization *)
160
-
let rec to_yojson : json -> Yojson.Basic.t = function
161
-
| `Null -> `Null
162
-
| `Bool b -> `Bool b
163
-
| `Int i -> `Int i
164
-
| `Float f -> `Float f
165
-
| `String s -> `String s
166
-
| `List l -> `List (List.map to_yojson l)
167
-
| `Assoc pairs -> `Assoc (List.map (fun (k, v) -> (k, to_yojson v)) pairs)
168
-
in
169
-
Yojson.Basic.to_string (to_yojson json)
207
+
encode v |> json_to_atproto_json |> Atproto_json.encode
170
208
171
209
(** Encode an IPLD value to a pretty-printed DAG-JSON string *)
172
-
let encode_string_pretty (v : Dag_cbor.value) : string =
173
-
let json = encode v in
174
-
let rec to_yojson : json -> Yojson.Basic.t = function
175
-
| `Null -> `Null
176
-
| `Bool b -> `Bool b
177
-
| `Int i -> `Int i
178
-
| `Float f -> `Float f
179
-
| `String s -> `String s
180
-
| `List l -> `List (List.map to_yojson l)
181
-
| `Assoc pairs -> `Assoc (List.map (fun (k, v) -> (k, to_yojson v)) pairs)
182
-
in
183
-
Yojson.Basic.pretty_to_string (to_yojson json)
210
+
let encode_string_pretty (v : Dag_cbor.value) : string = encode_string v ^ "\n"
184
211
185
212
(** Decode a DAG-JSON string to an IPLD value *)
186
213
let decode_string ?(mode = Dag_cbor.Atproto) (s : string) :
187
214
(Dag_cbor.value, error) result =
188
-
try
189
-
let yojson = Yojson.Basic.from_string s in
190
-
let rec of_yojson : Yojson.Basic.t -> json = function
191
-
| `Null -> `Null
192
-
| `Bool b -> `Bool b
193
-
| `Int i -> `Int i
194
-
| `Float f -> `Float f
195
-
| `String s -> `String s
196
-
| `List l -> `List (List.map of_yojson l)
197
-
| `Assoc pairs -> `Assoc (List.map (fun (k, v) -> (k, of_yojson v)) pairs)
198
-
in
199
-
decode ~mode (of_yojson yojson)
200
-
with Yojson.Json_error msg ->
201
-
Error (`Invalid_structure ("JSON parse error: " ^ msg))
215
+
match Atproto_json.decode s with
216
+
| Ok t -> decode ~mode (json_of_atproto_json t)
217
+
| Error msg -> Error (`Invalid_structure ("JSON parse error: " ^ msg))
202
218
203
219
(** Convert DAG-JSON to DAG-CBOR bytes *)
204
220
let to_dag_cbor ?(mode = Dag_cbor.Atproto) (j : json) : (string, error) result =
+1
-1
lib/ipld/dune
+1
-1
lib/ipld/dune
+140
lib/json/atproto_json.ml
+140
lib/json/atproto_json.ml
···
1
+
type t = Simdjsont.Json.t
2
+
type decode_error = string
3
+
4
+
let decode (s : string) : (t, decode_error) result =
5
+
Simdjsont.decode Simdjsont.Codec.value s
6
+
7
+
let encode (t : t) : string = Simdjsont.Json.to_string t
8
+
let null : t = Simdjsont.Json.Null
9
+
let bool (b : bool) : t = Simdjsont.Json.Bool b
10
+
let int64 (i : int64) : t = Simdjsont.Json.Int i
11
+
let int (i : int) : t = int64 (Int64.of_int i)
12
+
let float (f : float) : t = Simdjsont.Json.Float f
13
+
let string (s : string) : t = Simdjsont.Json.String s
14
+
let array (xs : t list) : t = Simdjsont.Json.Array xs
15
+
let object_ (pairs : (string * t) list) : t = Simdjsont.Json.Object pairs
16
+
17
+
let int_of_int64_opt (i : int64) : int option =
18
+
if i >= Int64.of_int min_int && i <= Int64.of_int max_int then
19
+
Some (Int64.to_int i)
20
+
else None
21
+
22
+
let int_of_int64_default ~default (i : int64) : int =
23
+
match int_of_int64_opt i with Some x -> x | None -> default
24
+
25
+
let get (key : string) (pairs : (string * t) list) : t option =
26
+
List.assoc_opt key pairs
27
+
28
+
let get_string_opt key pairs =
29
+
match get key pairs with
30
+
| Some (Simdjsont.Json.String s) -> Some s
31
+
| _ -> None
32
+
33
+
let get_object_opt key pairs =
34
+
match get key pairs with
35
+
| Some (Simdjsont.Json.Object o) -> Some o
36
+
| _ -> None
37
+
38
+
let get_array_opt key pairs =
39
+
match get key pairs with Some (Simdjsont.Json.Array a) -> Some a | _ -> None
40
+
41
+
let get_int64_opt key pairs =
42
+
match get key pairs with Some (Simdjsont.Json.Int i) -> Some i | _ -> None
43
+
44
+
let get_int_opt key pairs =
45
+
match get_int64_opt key pairs with
46
+
| Some i -> int_of_int64_opt i
47
+
| None -> None
48
+
49
+
let to_null_opt : t -> unit option = function
50
+
| Simdjsont.Json.Null -> Some ()
51
+
| _ -> None
52
+
53
+
let to_bool_opt : t -> bool option = function
54
+
| Simdjsont.Json.Bool b -> Some b
55
+
| _ -> None
56
+
57
+
let to_int64_opt : t -> int64 option = function
58
+
| Simdjsont.Json.Int i -> Some i
59
+
| _ -> None
60
+
61
+
let to_int_opt (t : t) : int option =
62
+
match to_int64_opt t with Some i -> int_of_int64_opt i | None -> None
63
+
64
+
let to_float_opt : t -> float option = function
65
+
| Simdjsont.Json.Float f -> Some f
66
+
| _ -> None
67
+
68
+
let to_string_opt : t -> string option = function
69
+
| Simdjsont.Json.String s -> Some s
70
+
| _ -> None
71
+
72
+
let to_array_opt : t -> t list option = function
73
+
| Simdjsont.Json.Array xs -> Some xs
74
+
| _ -> None
75
+
76
+
let to_object_opt : t -> (string * t) list option = function
77
+
| Simdjsont.Json.Object pairs -> Some pairs
78
+
| _ -> None
79
+
80
+
let escape_string s =
81
+
let buf = Buffer.create (String.length s * 2) in
82
+
String.iter
83
+
(fun c ->
84
+
match c with
85
+
| '"' -> Buffer.add_string buf "\\\""
86
+
| '\\' -> Buffer.add_string buf "\\\\"
87
+
| '\n' -> Buffer.add_string buf "\\n"
88
+
| '\r' -> Buffer.add_string buf "\\r"
89
+
| '\t' -> Buffer.add_string buf "\\t"
90
+
| c when Char.code c < 32 ->
91
+
Buffer.add_string buf (Printf.sprintf "\\u%04x" (Char.code c))
92
+
| c -> Buffer.add_char buf c)
93
+
s;
94
+
Buffer.contents buf
95
+
96
+
let rec encode_pretty_aux buf indent t =
97
+
let indent_str = String.make (indent * 2) ' ' in
98
+
let next_indent = indent + 1 in
99
+
match t with
100
+
| Simdjsont.Json.Null -> Buffer.add_string buf "null"
101
+
| Simdjsont.Json.Bool b ->
102
+
Buffer.add_string buf (if b then "true" else "false")
103
+
| Simdjsont.Json.Int i -> Buffer.add_string buf (Int64.to_string i)
104
+
| Simdjsont.Json.Float f -> Buffer.add_string buf (Printf.sprintf "%g" f)
105
+
| Simdjsont.Json.String s ->
106
+
Buffer.add_char buf '"';
107
+
Buffer.add_string buf (escape_string s);
108
+
Buffer.add_char buf '"'
109
+
| Simdjsont.Json.Array [] -> Buffer.add_string buf "[]"
110
+
| Simdjsont.Json.Array items ->
111
+
Buffer.add_string buf "[\n";
112
+
List.iteri
113
+
(fun i item ->
114
+
Buffer.add_string buf (String.make (next_indent * 2) ' ');
115
+
encode_pretty_aux buf next_indent item;
116
+
if i < List.length items - 1 then Buffer.add_char buf ',';
117
+
Buffer.add_char buf '\n')
118
+
items;
119
+
Buffer.add_string buf indent_str;
120
+
Buffer.add_char buf ']'
121
+
| Simdjsont.Json.Object [] -> Buffer.add_string buf "{}"
122
+
| Simdjsont.Json.Object pairs ->
123
+
Buffer.add_string buf "{\n";
124
+
List.iteri
125
+
(fun i (k, v) ->
126
+
Buffer.add_string buf (String.make (next_indent * 2) ' ');
127
+
Buffer.add_char buf '"';
128
+
Buffer.add_string buf (escape_string k);
129
+
Buffer.add_string buf "\": ";
130
+
encode_pretty_aux buf next_indent v;
131
+
if i < List.length pairs - 1 then Buffer.add_char buf ',';
132
+
Buffer.add_char buf '\n')
133
+
pairs;
134
+
Buffer.add_string buf indent_str;
135
+
Buffer.add_char buf '}'
136
+
137
+
let encode_pretty (t : t) : string =
138
+
let buf = Buffer.create 1024 in
139
+
encode_pretty_aux buf 0 t;
140
+
Buffer.contents buf
+7
lib/json/dune
+7
lib/json/dune
+4
-4
lib/lexicon/atproto_lexicon.ml
+4
-4
lib/lexicon/atproto_lexicon.ml
···
24
24
25
25
(* Validate a record against a schema *)
26
26
let record =
27
-
Simdjsont.Json.Object
27
+
Atproto_json.object_
28
28
[
29
-
("$type", Simdjsont.Json.String "app.bsky.feed.post");
30
-
("text", Simdjsont.Json.String "Hello!");
31
-
("createdAt", Simdjsont.Json.String "2024-01-01T00:00:00Z");
29
+
("$type", Atproto_json.string "app.bsky.feed.post");
30
+
("text", Atproto_json.string "Hello!");
31
+
("createdAt", Atproto_json.string "2024-01-01T00:00:00Z");
32
32
]
33
33
in
34
34
match Validator.validate_record schema record with
+446
-58
lib/lexicon/codegen.ml
+446
-58
lib/lexicon/codegen.ml
···
5
5
6
6
Generated code includes:
7
7
- OCaml record types matching Lexicon schemas
8
-
- JSON encoders/decoders using simdjsont
9
-
- DAG-CBOR encoders/decoders
8
+
- JSON encoders/decoders using Atproto_json
10
9
- Type-safe XRPC client methods *)
11
10
12
11
(** {1 Name Transformations} *)
···
103
102
| Schema.Array arr ->
104
103
Printf.sprintf "%s list" (field_type_to_ocaml arr.items)
105
104
| Schema.Object _ ->
106
-
"Simdjsont.Json.t" (* Inline objects become raw JSON for now *)
105
+
"Atproto_json.t" (* Inline objects become raw JSON for now *)
107
106
| Schema.Ref r -> ref_to_ocaml r.ref_
108
107
| Schema.Union u -> union_to_ocaml u
109
108
in
···
117
116
| Schema.String _ -> "string"
118
117
| Schema.Bytes _ -> "string"
119
118
| Schema.Cid_link _ -> "Cid.t"
120
-
| Schema.Unknown _ -> "Simdjsont.Json.t"
119
+
| Schema.Unknown _ -> "Atproto_json.t"
121
120
122
121
and format_to_ocaml (fmt : Schema.string_format) : string =
123
122
match fmt with
···
140
139
let name = String.sub ref_str 1 (String.length ref_str - 1) in
141
140
Printf.sprintf "%s.t" (String.capitalize_ascii (camel_to_snake name))
142
141
else
143
-
(* External ref like "app.bsky.actor.defs#basicView" -> Simdjsont.Json.t for now *)
144
-
"Simdjsont.Json.t"
142
+
(* External ref like "app.bsky.actor.defs#basicView" -> Atproto_json.t for now *)
143
+
"Atproto_json.t"
145
144
146
145
and union_to_ocaml (_u : Schema.union_type) : string =
147
146
(* Unions become raw JSON for now - full union support would require variant types *)
148
-
"Simdjsont.Json.t"
147
+
"Atproto_json.t"
149
148
150
149
(** {1 Code Generation} *)
151
150
···
180
179
dedent e;
181
180
emit e "}"
182
181
182
+
(** Generate field decoder expression for a field type *)
183
+
let rec gen_field_decoder (ft : Schema.field_type) (var : string) : string =
184
+
match ft with
185
+
| Schema.Primitive prim -> gen_primitive_decoder prim var
186
+
| Schema.Blob _ -> Printf.sprintf "(* blob decode *) %s" var
187
+
| Schema.Array arr ->
188
+
Printf.sprintf
189
+
"(match Atproto_json.to_array_opt %s with Some arr -> List.filter_map \
190
+
(fun x -> %s) arr | None -> [])"
191
+
var
192
+
(gen_field_decoder arr.items "x")
193
+
| Schema.Object _ -> Printf.sprintf "(Some %s)" var
194
+
| Schema.Ref _ -> Printf.sprintf "(Some %s)" var (* Raw JSON for refs *)
195
+
| Schema.Union _ -> Printf.sprintf "(Some %s)" var (* Raw JSON for unions *)
196
+
197
+
and gen_primitive_decoder (prim : Schema.primitive) (var : string) : string =
198
+
match prim with
199
+
| Schema.Boolean _ -> Printf.sprintf "Atproto_json.to_bool_opt %s" var
200
+
| Schema.Integer _ -> Printf.sprintf "Atproto_json.to_int_opt %s" var
201
+
| Schema.String _ -> Printf.sprintf "Atproto_json.to_string_opt %s" var
202
+
| Schema.Bytes _ -> Printf.sprintf "Atproto_json.to_string_opt %s" var
203
+
| Schema.Cid_link _ ->
204
+
Printf.sprintf
205
+
"(match Atproto_json.to_string_opt %s with Some s -> Cid.of_string s \
206
+
|> Result.to_option | None -> None)"
207
+
var
208
+
| Schema.Unknown _ -> Printf.sprintf "(Some %s)" var
209
+
210
+
(** Generate field encoder expression for a field type *)
211
+
let rec gen_field_encoder (ft : Schema.field_type) (var : string) : string =
212
+
match ft with
213
+
| Schema.Primitive prim -> gen_primitive_encoder prim var
214
+
| Schema.Blob _ -> Printf.sprintf "(* blob encode *) Atproto_json.null"
215
+
| Schema.Array arr ->
216
+
Printf.sprintf "Atproto_json.array (List.map (fun x -> %s) %s)"
217
+
(gen_field_encoder arr.items "x")
218
+
var
219
+
| Schema.Object _ -> var (* Already Atproto_json.t *)
220
+
| Schema.Ref _ -> var (* Already Atproto_json.t *)
221
+
| Schema.Union _ -> var (* Already Atproto_json.t *)
222
+
223
+
and gen_primitive_encoder (prim : Schema.primitive) (var : string) : string =
224
+
match prim with
225
+
| Schema.Boolean _ -> Printf.sprintf "Atproto_json.bool %s" var
226
+
| Schema.Integer _ -> Printf.sprintf "Atproto_json.int %s" var
227
+
| Schema.String _ -> Printf.sprintf "Atproto_json.string %s" var
228
+
| Schema.Bytes _ -> Printf.sprintf "Atproto_json.string %s" var
229
+
| Schema.Cid_link _ ->
230
+
Printf.sprintf "Atproto_json.string (Cid.to_string %s)" var
231
+
| Schema.Unknown _ -> var
232
+
183
233
(** Generate JSON decoder for a record type *)
184
234
let gen_json_decoder e (obj : Schema.object_type) =
185
235
emit e "let of_json json =";
186
236
indent e;
187
-
emit e "match json with";
188
-
emit e "| `Assoc pairs ->";
237
+
emit e "match Atproto_json.to_object_opt json with";
238
+
emit e "| Some pairs ->";
189
239
indent e;
190
240
191
241
(* Generate field extractors *)
···
193
243
(fun (prop : Schema.property) ->
194
244
let ocaml_name = field_to_ocaml prop.name in
195
245
let is_optional = not (List.mem prop.name obj.required) in
196
-
if is_optional then
197
-
emit e
198
-
(Printf.sprintf "let %s = List.assoc_opt \"%s\" pairs in" ocaml_name
199
-
prop.name)
200
-
else
201
-
emit e
202
-
(Printf.sprintf "let %s = List.assoc \"%s\" pairs in" ocaml_name
203
-
prop.name))
246
+
let decoder =
247
+
match prop.field with
248
+
| Schema.Primitive prim ->
249
+
let getter =
250
+
match prim with
251
+
| Schema.Boolean _ -> "Atproto_json.to_bool_opt"
252
+
| Schema.Integer _ -> "Atproto_json.to_int_opt"
253
+
| Schema.String _ -> "Atproto_json.get_string_opt"
254
+
| Schema.Bytes _ -> "Atproto_json.get_string_opt"
255
+
| _ -> "(fun _ _ -> None)"
256
+
in
257
+
if String.contains getter '.' && not (String.contains getter '_')
258
+
then Printf.sprintf "%s \"%s\" pairs" getter prop.name
259
+
else if String.starts_with ~prefix:"Atproto_json.get_" getter then
260
+
Printf.sprintf "%s \"%s\" pairs" getter prop.name
261
+
else
262
+
Printf.sprintf
263
+
"(match Atproto_json.get \"%s\" pairs with Some v -> %s v | \
264
+
None -> None)"
265
+
prop.name getter
266
+
| Schema.Array arr ->
267
+
Printf.sprintf
268
+
"(match Atproto_json.get_array_opt \"%s\" pairs with Some arr -> \
269
+
Some (List.filter_map (fun x -> %s) arr) | None -> %s)"
270
+
prop.name
271
+
(gen_field_decoder arr.items "x")
272
+
(if is_optional then "None" else "Some []")
273
+
| _ -> Printf.sprintf "Atproto_json.get \"%s\" pairs" prop.name
274
+
in
275
+
emit e (Printf.sprintf "let %s = %s in" ocaml_name decoder))
204
276
obj.properties;
205
277
206
-
(* Build record *)
207
-
emit e "Ok {";
208
-
indent e;
209
-
List.iter
210
-
(fun (prop : Schema.property) ->
211
-
let ocaml_name = field_to_ocaml prop.name in
212
-
let is_optional = not (List.mem prop.name obj.required) in
213
-
if is_optional then
214
-
emit e
215
-
(Printf.sprintf "%s = Option.map (fun v -> v) %s;" ocaml_name
216
-
ocaml_name)
217
-
else emit e (Printf.sprintf "%s;" ocaml_name))
218
-
obj.properties;
219
-
dedent e;
220
-
emit e "}";
278
+
let required_checks =
279
+
List.filter
280
+
(fun (p : Schema.property) -> List.mem p.name obj.required)
281
+
obj.properties
282
+
in
283
+
if List.length required_checks > 0 then begin
284
+
let check_pattern =
285
+
required_checks
286
+
|> List.map (fun (p : Schema.property) ->
287
+
Printf.sprintf "Some %s" (field_to_ocaml p.name))
288
+
|> String.concat ", "
289
+
in
290
+
let check_vars =
291
+
required_checks
292
+
|> List.map (fun (p : Schema.property) -> field_to_ocaml p.name)
293
+
|> String.concat ", "
294
+
in
295
+
emit e (Printf.sprintf "(match (%s) with" check_vars);
296
+
emit e (Printf.sprintf "| %s ->" check_pattern);
297
+
indent e;
298
+
emit e "Ok {";
299
+
indent e;
300
+
List.iter
301
+
(fun (prop : Schema.property) ->
302
+
let ocaml_name = field_to_ocaml prop.name in
303
+
let is_required = List.mem prop.name obj.required in
304
+
if is_required then emit e (Printf.sprintf "%s;" ocaml_name)
305
+
else emit e (Printf.sprintf "%s;" ocaml_name))
306
+
obj.properties;
307
+
dedent e;
308
+
emit e "}";
309
+
dedent e;
310
+
emit e "| _ -> Error \"Missing required fields\")"
311
+
end
312
+
else begin
313
+
emit e "Ok {";
314
+
indent e;
315
+
List.iter
316
+
(fun (prop : Schema.property) ->
317
+
let ocaml_name = field_to_ocaml prop.name in
318
+
emit e (Printf.sprintf "%s;" ocaml_name))
319
+
obj.properties;
320
+
dedent e;
321
+
emit e "}"
322
+
end;
221
323
dedent e;
222
-
emit e "| _ -> Error \"Expected object\"";
324
+
emit e "| None -> Error \"Expected JSON object\"";
223
325
dedent e
224
326
225
327
(** Generate JSON encoder for a record type *)
226
328
let gen_json_encoder e (obj : Schema.object_type) =
227
329
emit e "let to_json t =";
228
330
indent e;
229
-
emit e "`Assoc [";
230
-
indent e;
231
-
List.iteri
232
-
(fun i (prop : Schema.property) ->
331
+
emit e "let fields = [] in";
332
+
333
+
(* Build fields list, handling optional fields *)
334
+
List.iter
335
+
(fun (prop : Schema.property) ->
233
336
let ocaml_name = field_to_ocaml prop.name in
234
337
let is_optional = not (List.mem prop.name obj.required) in
235
-
let comma = if i < List.length obj.properties - 1 then ";" else "" in
338
+
let encoder =
339
+
gen_field_encoder prop.field
340
+
(if is_optional then "v" else Printf.sprintf "t.%s" ocaml_name)
341
+
in
236
342
if is_optional then
237
-
(* Optional fields need special handling *)
238
-
emit e (Printf.sprintf "(* %s is optional *)%s" ocaml_name comma)
343
+
emit e
344
+
(Printf.sprintf
345
+
"let fields = match t.%s with Some v -> (\"%s\", %s) :: fields | \
346
+
None -> fields in"
347
+
ocaml_name prop.name encoder)
239
348
else
240
349
emit e
241
-
(Printf.sprintf "(\"%s\", (* encode %s *) `Null)%s" prop.name
242
-
ocaml_name comma))
243
-
obj.properties;
244
-
dedent e;
245
-
emit e "]";
350
+
(Printf.sprintf "let fields = (\"%s\", %s) :: fields in" prop.name
351
+
encoder))
352
+
(List.rev obj.properties);
353
+
354
+
(* Reverse to get correct order *)
355
+
emit e "Atproto_json.object_ fields";
246
356
dedent e
247
357
248
358
(** Generate a module for a record definition *)
···
272
382
contents e
273
383
274
384
(** Generate code for a query definition *)
275
-
let gen_query_module (lexicon : Schema.lexicon) (_params : Schema.params option)
276
-
(_output : Schema.body option) : string =
385
+
let gen_query_module (lexicon : Schema.lexicon) (params : Schema.params option)
386
+
(output : Schema.body option) : string =
277
387
let e = create_emitter () in
278
388
let module_name = nsid_to_module_name lexicon.id in
279
389
···
288
398
289
399
(* Generate params type if present *)
290
400
emit_blank e;
291
-
emit e "type params = {";
292
-
indent e;
293
-
emit e "(* Query parameters *)";
294
-
dedent e;
295
-
emit e "}";
401
+
(match params with
402
+
| Some p when List.length p.properties > 0 ->
403
+
emit e "type params = {";
404
+
indent e;
405
+
List.iter
406
+
(fun (prop : Schema.property) ->
407
+
let ocaml_name = field_to_ocaml prop.name in
408
+
let is_optional = not (List.mem prop.name p.required) in
409
+
let ocaml_type =
410
+
field_type_to_ocaml ~optional:is_optional prop.field
411
+
in
412
+
emit e (Printf.sprintf "%s : %s;" ocaml_name ocaml_type))
413
+
p.properties;
414
+
dedent e;
415
+
emit e "}"
416
+
| _ -> emit e "type params = unit");
296
417
297
-
(* Generate output type if present *)
418
+
(* Generate output type *)
298
419
emit_blank e;
299
-
emit e "type output = Simdjsont.Json.t";
420
+
(match output with
421
+
| Some { schema = Some (Schema.Object obj); _ } ->
422
+
emit e "type output = {";
423
+
indent e;
424
+
List.iter
425
+
(fun (prop : Schema.property) ->
426
+
let ocaml_name = field_to_ocaml prop.name in
427
+
let is_optional = not (List.mem prop.name obj.required) in
428
+
let ocaml_type =
429
+
field_type_to_ocaml ~optional:is_optional prop.field
430
+
in
431
+
emit e (Printf.sprintf "%s : %s;" ocaml_name ocaml_type))
432
+
obj.properties;
433
+
dedent e;
434
+
emit e "}"
435
+
| _ -> emit e "type output = Atproto_json.t");
436
+
437
+
(* Generate params encoder *)
438
+
emit_blank e;
439
+
(match params with
440
+
| Some p when List.length p.properties > 0 ->
441
+
emit e "let params_to_query p =";
442
+
indent e;
443
+
emit e "let q = [] in";
444
+
List.iter
445
+
(fun (prop : Schema.property) ->
446
+
let ocaml_name = field_to_ocaml prop.name in
447
+
let is_optional = not (List.mem prop.name p.required) in
448
+
if is_optional then
449
+
emit e
450
+
(Printf.sprintf
451
+
"let q = match p.%s with Some v -> (\"%s\", v) :: q | None -> \
452
+
q in"
453
+
ocaml_name prop.name)
454
+
else
455
+
emit e
456
+
(Printf.sprintf "let q = (\"%s\", p.%s) :: q in" prop.name
457
+
ocaml_name))
458
+
(List.rev p.properties);
459
+
emit e "q";
460
+
dedent e
461
+
| _ -> emit e "let params_to_query () = []");
462
+
463
+
(* Generate output decoder *)
464
+
emit_blank e;
465
+
(match output with
466
+
| Some { schema = Some (Schema.Object obj); _ } ->
467
+
emit e "let output_of_json json =";
468
+
indent e;
469
+
emit e "match Atproto_json.to_object_opt json with";
470
+
emit e "| Some pairs ->";
471
+
indent e;
472
+
List.iter
473
+
(fun (prop : Schema.property) ->
474
+
let ocaml_name = field_to_ocaml prop.name in
475
+
emit e
476
+
(Printf.sprintf
477
+
"let %s = Atproto_json.get_string_opt \"%s\" pairs in" ocaml_name
478
+
prop.name))
479
+
obj.properties;
480
+
emit e "Ok {";
481
+
indent e;
482
+
List.iter
483
+
(fun (prop : Schema.property) ->
484
+
let ocaml_name = field_to_ocaml prop.name in
485
+
emit e (Printf.sprintf "%s;" ocaml_name))
486
+
obj.properties;
487
+
dedent e;
488
+
emit e "}";
489
+
dedent e;
490
+
emit e "| None -> Error \"Expected JSON object\"";
491
+
dedent e
492
+
| _ -> emit e "let output_of_json json = Ok json");
300
493
301
494
dedent e;
302
495
emit e "end";
···
305
498
306
499
(** Generate code for a procedure definition *)
307
500
let gen_procedure_module (lexicon : Schema.lexicon)
308
-
(_params : Schema.params option) (_input : Schema.body option)
309
-
(_output : Schema.body option) : string =
501
+
(params : Schema.params option) (input : Schema.body option)
502
+
(output : Schema.body option) : string =
310
503
let e = create_emitter () in
311
504
let module_name = nsid_to_module_name lexicon.id in
312
505
···
319
512
emit e "(* Procedure endpoint *)";
320
513
emit e (Printf.sprintf "let nsid = \"%s\"" lexicon.id);
321
514
515
+
(* Generate input type *)
322
516
emit_blank e;
323
-
emit e "type input = Simdjsont.Json.t";
517
+
(match input with
518
+
| Some { schema = Some (Schema.Object obj); _ } ->
519
+
emit e "type input = {";
520
+
indent e;
521
+
List.iter
522
+
(fun (prop : Schema.property) ->
523
+
let ocaml_name = field_to_ocaml prop.name in
524
+
let is_optional = not (List.mem prop.name obj.required) in
525
+
let ocaml_type =
526
+
field_type_to_ocaml ~optional:is_optional prop.field
527
+
in
528
+
emit e (Printf.sprintf "%s : %s;" ocaml_name ocaml_type))
529
+
obj.properties;
530
+
dedent e;
531
+
emit e "}";
532
+
533
+
(* Generate input encoder *)
534
+
emit_blank e;
535
+
emit e "let input_to_json t =";
536
+
indent e;
537
+
emit e "let fields = [] in";
538
+
List.iter
539
+
(fun (prop : Schema.property) ->
540
+
let ocaml_name = field_to_ocaml prop.name in
541
+
let is_optional = not (List.mem prop.name obj.required) in
542
+
let encoder =
543
+
gen_field_encoder prop.field
544
+
(if is_optional then "v" else Printf.sprintf "t.%s" ocaml_name)
545
+
in
546
+
if is_optional then
547
+
emit e
548
+
(Printf.sprintf
549
+
"let fields = match t.%s with Some v -> (\"%s\", %s) :: \
550
+
fields | None -> fields in"
551
+
ocaml_name prop.name encoder)
552
+
else
553
+
emit e
554
+
(Printf.sprintf "let fields = (\"%s\", %s) :: fields in" prop.name
555
+
encoder))
556
+
(List.rev obj.properties);
557
+
emit e "Atproto_json.object_ fields";
558
+
dedent e
559
+
| _ ->
560
+
emit e "type input = Atproto_json.t";
561
+
emit_blank e;
562
+
emit e "let input_to_json t = t");
563
+
564
+
(* Generate params encoder if present *)
324
565
emit_blank e;
325
-
emit e "type output = Simdjsont.Json.t";
566
+
(match params with
567
+
| Some p when List.length p.properties > 0 ->
568
+
emit e "type params = {";
569
+
indent e;
570
+
List.iter
571
+
(fun (prop : Schema.property) ->
572
+
let ocaml_name = field_to_ocaml prop.name in
573
+
let is_optional = not (List.mem prop.name p.required) in
574
+
let ocaml_type =
575
+
field_type_to_ocaml ~optional:is_optional prop.field
576
+
in
577
+
emit e (Printf.sprintf "%s : %s;" ocaml_name ocaml_type))
578
+
p.properties;
579
+
dedent e;
580
+
emit e "}";
581
+
emit_blank e;
582
+
emit e "let params_to_query p =";
583
+
indent e;
584
+
emit e "let q = [] in";
585
+
List.iter
586
+
(fun (prop : Schema.property) ->
587
+
let ocaml_name = field_to_ocaml prop.name in
588
+
let is_optional = not (List.mem prop.name p.required) in
589
+
if is_optional then
590
+
emit e
591
+
(Printf.sprintf
592
+
"let q = match p.%s with Some v -> (\"%s\", v) :: q | None -> \
593
+
q in"
594
+
ocaml_name prop.name)
595
+
else
596
+
emit e
597
+
(Printf.sprintf "let q = (\"%s\", p.%s) :: q in" prop.name
598
+
ocaml_name))
599
+
(List.rev p.properties);
600
+
emit e "q";
601
+
dedent e
602
+
| _ ->
603
+
emit e "type params = unit";
604
+
emit_blank e;
605
+
emit e "let params_to_query () = []");
606
+
607
+
(* Generate output type *)
608
+
emit_blank e;
609
+
(match output with
610
+
| Some { schema = Some (Schema.Object obj); _ } ->
611
+
emit e "type output = {";
612
+
indent e;
613
+
List.iter
614
+
(fun (prop : Schema.property) ->
615
+
let ocaml_name = field_to_ocaml prop.name in
616
+
let is_optional = not (List.mem prop.name obj.required) in
617
+
let ocaml_type =
618
+
field_type_to_ocaml ~optional:is_optional prop.field
619
+
in
620
+
emit e (Printf.sprintf "%s : %s;" ocaml_name ocaml_type))
621
+
obj.properties;
622
+
dedent e;
623
+
emit e "}";
624
+
625
+
(* Generate output decoder *)
626
+
emit_blank e;
627
+
emit e "let output_of_json json =";
628
+
indent e;
629
+
emit e "match Atproto_json.to_object_opt json with";
630
+
emit e "| Some pairs ->";
631
+
indent e;
632
+
List.iter
633
+
(fun (prop : Schema.property) ->
634
+
let ocaml_name = field_to_ocaml prop.name in
635
+
let is_optional = not (List.mem prop.name obj.required) in
636
+
let decoder =
637
+
match prop.field with
638
+
| Schema.Primitive (Schema.Boolean _) ->
639
+
if is_optional then
640
+
Printf.sprintf
641
+
"(match Atproto_json.get \"%s\" pairs with Some v -> \
642
+
Atproto_json.to_bool_opt v | None -> None)"
643
+
prop.name
644
+
else
645
+
Printf.sprintf
646
+
"(match Atproto_json.get \"%s\" pairs with Some v -> \
647
+
Atproto_json.to_bool_opt v | None -> None)"
648
+
prop.name
649
+
| Schema.Primitive (Schema.Integer _) ->
650
+
if is_optional then
651
+
Printf.sprintf "Atproto_json.get_int_opt \"%s\" pairs"
652
+
prop.name
653
+
else
654
+
Printf.sprintf "Atproto_json.get_int_opt \"%s\" pairs"
655
+
prop.name
656
+
| Schema.Primitive (Schema.String _) ->
657
+
Printf.sprintf "Atproto_json.get_string_opt \"%s\" pairs"
658
+
prop.name
659
+
| _ -> Printf.sprintf "Atproto_json.get \"%s\" pairs" prop.name
660
+
in
661
+
emit e (Printf.sprintf "let %s = %s in" ocaml_name decoder))
662
+
obj.properties;
663
+
664
+
let required_props =
665
+
List.filter
666
+
(fun (p : Schema.property) -> List.mem p.name obj.required)
667
+
obj.properties
668
+
in
669
+
if List.length required_props > 0 then begin
670
+
let check_vars =
671
+
required_props
672
+
|> List.map (fun (p : Schema.property) -> field_to_ocaml p.name)
673
+
|> String.concat ", "
674
+
in
675
+
let check_pattern =
676
+
required_props
677
+
|> List.map (fun (p : Schema.property) ->
678
+
Printf.sprintf "Some %s" (field_to_ocaml p.name))
679
+
|> String.concat ", "
680
+
in
681
+
emit e (Printf.sprintf "(match (%s) with" check_vars);
682
+
emit e (Printf.sprintf "| %s ->" check_pattern);
683
+
indent e;
684
+
emit e "Ok {";
685
+
indent e;
686
+
List.iter
687
+
(fun (prop : Schema.property) ->
688
+
let ocaml_name = field_to_ocaml prop.name in
689
+
emit e (Printf.sprintf "%s;" ocaml_name))
690
+
obj.properties;
691
+
dedent e;
692
+
emit e "}";
693
+
dedent e;
694
+
emit e "| _ -> Error \"Missing required fields\")"
695
+
end
696
+
else begin
697
+
emit e "Ok {";
698
+
indent e;
699
+
List.iter
700
+
(fun (prop : Schema.property) ->
701
+
let ocaml_name = field_to_ocaml prop.name in
702
+
emit e (Printf.sprintf "%s;" ocaml_name))
703
+
obj.properties;
704
+
dedent e;
705
+
emit e "}"
706
+
end;
707
+
dedent e;
708
+
emit e "| None -> Error \"Expected JSON object\"";
709
+
dedent e
710
+
| _ ->
711
+
emit e "type output = Atproto_json.t";
712
+
emit_blank e;
713
+
emit e "let output_of_json json = Ok json");
326
714
327
715
dedent e;
328
716
emit e "end";
+1
-1
lib/lexicon/dune
+1
-1
lib/lexicon/dune
+72
-75
lib/lexicon/parser.ml
+72
-75
lib/lexicon/parser.ml
···
17
17
18
18
let error_to_string e = Format.asprintf "%a" pp_error e
19
19
20
-
type json = Simdjsont.Json.t
20
+
type json = Atproto_json.t
21
21
22
-
let int_of_int64 (i : int64) : int option =
23
-
if
24
-
Int64.compare i (Int64.of_int max_int) > 0
25
-
|| Int64.compare i (Int64.of_int min_int) < 0
26
-
then None
27
-
else Some (Int64.to_int i)
22
+
let int_of_int64 (i : int64) : int option = Atproto_json.int_of_int64_opt i
28
23
29
24
(** Helper to get string from JSON *)
30
25
let get_string key json =
31
-
match json with
32
-
| Simdjsont.Json.Object pairs -> (
26
+
match Atproto_json.to_object_opt json with
27
+
| Some pairs -> (
33
28
match List.assoc_opt key pairs with
34
-
| Some (Simdjsont.Json.String s) -> Some s
35
-
| _ -> None)
36
-
| _ -> None
29
+
| Some v -> Atproto_json.to_string_opt v
30
+
| None -> None)
31
+
| None -> None
37
32
38
33
(** Helper to get optional string from JSON *)
39
34
let get_string_opt key json = get_string key json
40
35
41
36
(** Helper to get int from JSON *)
42
37
let get_int key json =
43
-
match json with
44
-
| Simdjsont.Json.Object pairs -> (
38
+
match Atproto_json.to_object_opt json with
39
+
| Some pairs -> (
45
40
match List.assoc_opt key pairs with
46
-
| Some (Simdjsont.Json.Int i) -> int_of_int64 i
47
-
| _ -> None)
48
-
| _ -> None
41
+
| Some v -> (
42
+
match Atproto_json.to_int64_opt v with
43
+
| Some i -> int_of_int64 i
44
+
| None -> None)
45
+
| None -> None)
46
+
| None -> None
49
47
50
48
(** Helper to get bool from JSON *)
51
49
let get_bool key json =
52
-
match json with
53
-
| Simdjsont.Json.Object pairs -> (
50
+
match Atproto_json.to_object_opt json with
51
+
| Some pairs -> (
54
52
match List.assoc_opt key pairs with
55
-
| Some (Simdjsont.Json.Bool b) -> Some b
56
-
| _ -> None)
57
-
| _ -> None
53
+
| Some v -> Atproto_json.to_bool_opt v
54
+
| None -> None)
55
+
| None -> None
58
56
59
57
(** Helper to get list from JSON *)
60
58
let get_list key json =
61
-
match json with
62
-
| Simdjsont.Json.Object pairs -> (
59
+
match Atproto_json.to_object_opt json with
60
+
| Some pairs -> (
63
61
match List.assoc_opt key pairs with
64
-
| Some (Simdjsont.Json.Array l) -> Some l
65
-
| _ -> None)
66
-
| _ -> None
62
+
| Some v -> Atproto_json.to_array_opt v
63
+
| None -> None)
64
+
| None -> None
67
65
68
66
(** Helper to get assoc from JSON *)
69
67
let get_assoc key json =
70
-
match json with
71
-
| Simdjsont.Json.Object pairs -> (
68
+
match Atproto_json.to_object_opt json with
69
+
| Some pairs -> (
72
70
match List.assoc_opt key pairs with
73
-
| Some (Simdjsont.Json.Object a) -> Some a
74
-
| _ -> None)
75
-
| _ -> None
71
+
| Some v -> Atproto_json.to_object_opt v
72
+
| None -> None)
73
+
| None -> None
76
74
77
75
(** Helper to get string list *)
78
76
let get_string_list key json =
79
77
match get_list key json with
80
-
| Some l ->
81
-
Some
82
-
(List.filter_map
83
-
(function Simdjsont.Json.String s -> Some s | _ -> None)
84
-
l)
78
+
| Some l -> Some (List.filter_map Atproto_json.to_string_opt l)
85
79
| None -> None
86
80
87
81
(** Helper to get int list *)
···
90
84
| Some l ->
91
85
Some
92
86
(List.filter_map
93
-
(function Simdjsont.Json.Int i -> int_of_int64 i | _ -> None)
87
+
(fun v ->
88
+
match Atproto_json.to_int64_opt v with
89
+
| Some i -> int_of_int64 i
90
+
| None -> None)
94
91
l)
95
92
| None -> None
96
93
···
170
167
max_size = get_int "maxSize" json;
171
168
})
172
169
| "array" -> (
173
-
match json with
174
-
| Simdjsont.Json.Object pairs -> (
170
+
match Atproto_json.to_object_opt json with
171
+
| Some pairs -> (
175
172
match List.assoc_opt "items" pairs with
176
173
| Some items_json -> (
177
174
match parse_field_type items_json with
···
186
183
})
187
184
| Error e -> Error e)
188
185
| None -> Error (`Missing_field "items"))
189
-
| _ -> Error (`Invalid_type "array"))
186
+
| None -> Error (`Invalid_type "array"))
190
187
| "object" -> parse_object_type json
191
188
| "ref" -> (
192
189
match get_string "ref" json with
···
284
281
| None -> Error (`Missing_field "encoding")
285
282
| Some encoding ->
286
283
let schema =
287
-
match json with
288
-
| Simdjsont.Json.Object pairs -> (
284
+
match Atproto_json.to_object_opt json with
285
+
| Some pairs -> (
289
286
match List.assoc_opt "schema" pairs with
290
287
| Some schema_json -> (
291
288
match parse_field_type schema_json with
292
289
| Ok ft -> Some ft
293
290
| Error _ -> None)
294
291
| None -> None)
295
-
| _ -> None
292
+
| None -> None
296
293
in
297
294
Ok
298
295
Schema.
···
313
310
314
311
(** Parse message from JSON *)
315
312
let parse_message json : (Schema.message, error) result =
316
-
match json with
317
-
| Simdjsont.Json.Object pairs -> (
313
+
match Atproto_json.to_object_opt json with
314
+
| Some pairs -> (
318
315
match List.assoc_opt "schema" pairs with
319
316
| Some schema_json -> (
320
317
match parse_field_type schema_json with
···
324
321
{ description = get_string_opt "description" json; schema }
325
322
| Error e -> Error e)
326
323
| None -> Error (`Missing_field "schema"))
327
-
| _ -> Error (`Invalid_type "message")
324
+
| None -> Error (`Invalid_type "message")
328
325
329
326
(** Parse permission from JSON *)
330
327
let parse_permission json : Schema.permission option =
···
354
351
| Some k -> Schema.record_key_of_string k
355
352
| None -> Schema.Any
356
353
in
357
-
match json with
358
-
| Simdjsont.Json.Object pairs -> (
354
+
match Atproto_json.to_object_opt json with
355
+
| Some pairs -> (
359
356
match List.assoc_opt "record" pairs with
360
357
| Some record_json -> (
361
358
match parse_object_type record_json with
···
370
367
| Ok _ -> Error (`Invalid_type "record.record must be object")
371
368
| Error e -> Error e)
372
369
| None -> Error (`Missing_field "record"))
373
-
| _ -> Error (`Invalid_type "record"))
370
+
| None -> Error (`Invalid_type "record"))
374
371
| "query" ->
375
372
let parameters =
376
-
match json with
377
-
| Simdjsont.Json.Object pairs -> (
373
+
match Atproto_json.to_object_opt json with
374
+
| Some pairs -> (
378
375
match List.assoc_opt "parameters" pairs with
379
376
| Some p -> (
380
377
match parse_params p with
381
378
| Ok params -> Some params
382
379
| Error _ -> None)
383
380
| None -> None)
384
-
| _ -> None
381
+
| None -> None
385
382
in
386
383
let output =
387
-
match json with
388
-
| Simdjsont.Json.Object pairs -> (
384
+
match Atproto_json.to_object_opt json with
385
+
| Some pairs -> (
389
386
match List.assoc_opt "output" pairs with
390
387
| Some o -> (
391
388
match parse_body o with
392
389
| Ok body -> Some body
393
390
| Error _ -> None)
394
391
| None -> None)
395
-
| _ -> None
392
+
| None -> None
396
393
in
397
394
Ok
398
395
(Schema.Query
···
404
401
})
405
402
| "procedure" ->
406
403
let parameters =
407
-
match json with
408
-
| Simdjsont.Json.Object pairs -> (
404
+
match Atproto_json.to_object_opt json with
405
+
| Some pairs -> (
409
406
match List.assoc_opt "parameters" pairs with
410
407
| Some p -> (
411
408
match parse_params p with
412
409
| Ok params -> Some params
413
410
| Error _ -> None)
414
411
| None -> None)
415
-
| _ -> None
412
+
| None -> None
416
413
in
417
414
let input =
418
-
match json with
419
-
| Simdjsont.Json.Object pairs -> (
415
+
match Atproto_json.to_object_opt json with
416
+
| Some pairs -> (
420
417
match List.assoc_opt "input" pairs with
421
418
| Some i -> (
422
419
match parse_body i with
423
420
| Ok body -> Some body
424
421
| Error _ -> None)
425
422
| None -> None)
426
-
| _ -> None
423
+
| None -> None
427
424
in
428
425
let output =
429
-
match json with
430
-
| Simdjsont.Json.Object pairs -> (
426
+
match Atproto_json.to_object_opt json with
427
+
| Some pairs -> (
431
428
match List.assoc_opt "output" pairs with
432
429
| Some o -> (
433
430
match parse_body o with
434
431
| Ok body -> Some body
435
432
| Error _ -> None)
436
433
| None -> None)
437
-
| _ -> None
434
+
| None -> None
438
435
in
439
436
Ok
440
437
(Schema.Procedure
···
447
444
})
448
445
| "subscription" ->
449
446
let parameters =
450
-
match json with
451
-
| Simdjsont.Json.Object pairs -> (
447
+
match Atproto_json.to_object_opt json with
448
+
| Some pairs -> (
452
449
match List.assoc_opt "parameters" pairs with
453
450
| Some p -> (
454
451
match parse_params p with
455
452
| Ok params -> Some params
456
453
| Error _ -> None)
457
454
| None -> None)
458
-
| _ -> None
455
+
| None -> None
459
456
in
460
457
let message =
461
-
match json with
462
-
| Simdjsont.Json.Object pairs -> (
458
+
match Atproto_json.to_object_opt json with
459
+
| Some pairs -> (
463
460
match List.assoc_opt "message" pairs with
464
461
| Some m -> (
465
462
match parse_message m with
466
463
| Ok msg -> Some msg
467
464
| Error _ -> None)
468
465
| None -> None)
469
-
| _ -> None
466
+
| None -> None
470
467
in
471
468
Ok
472
469
(Schema.Subscription
···
586
583
587
584
(** Parse a complete lexicon from JSON *)
588
585
let parse_lexicon json : (Schema.lexicon, error) result =
589
-
match json with
590
-
| Simdjsont.Json.Object _ -> (
586
+
match Atproto_json.to_object_opt json with
587
+
| Some _ -> (
591
588
match get_int "lexicon" json with
592
589
| None -> Error (`Missing_field "lexicon")
593
590
| Some version -> (
···
611
608
| Ok defs ->
612
609
Ok Schema.{ version; id; revision; description; defs }
613
610
| Error e -> Error e))))
614
-
| _ -> Error (`Parse_error "expected object")
611
+
| None -> Error (`Parse_error "expected object")
615
612
616
613
(** Parse a lexicon from a JSON string *)
617
614
let of_string s : (Schema.lexicon, error) result =
618
-
match Simdjsont.decode Simdjsont.Codec.value s with
615
+
match Atproto_json.decode s with
619
616
| Ok json -> parse_lexicon json
620
617
| Error msg -> Error (`Parse_error msg)
621
618
+192
-262
lib/lexicon/validator.ml
+192
-262
lib/lexicon/validator.ml
···
19
19
else Format.fprintf fmt "%s: %s" path_str err.message
20
20
21
21
let error_to_string err = Format.asprintf "%a" pp_error err
22
-
23
-
(** Create a validation error at a path *)
24
22
let error ~path message = { path; message }
25
-
26
-
(** Add a path segment to an error *)
27
23
let add_path segment err = { err with path = segment :: err.path }
28
-
29
-
(** Add path to all errors in a list *)
30
24
let add_path_to_errors segment errs = List.map (add_path segment) errs
31
25
32
26
(* === Format validators === *)
33
27
34
-
(** Validate a DID string *)
35
28
let validate_did s = Result.is_ok (Did.of_string s)
36
-
37
-
(** Validate a handle string *)
38
29
let validate_handle s = Result.is_ok (Handle.of_string s)
39
-
40
-
(** Validate an NSID string *)
41
30
let validate_nsid s = Result.is_ok (Nsid.of_string s)
42
-
43
-
(** Validate a TID string *)
44
31
let validate_tid s = Result.is_ok (Tid.of_string s)
45
-
46
-
(** Validate a record key string *)
47
32
let validate_record_key s = Result.is_ok (Record_key.of_string s)
48
-
49
-
(** Validate an AT-URI string *)
50
33
let validate_at_uri s = Result.is_ok (At_uri.of_string s)
51
-
52
-
(** Validate an AT-identifier (DID or handle) *)
53
34
let validate_at_identifier s = validate_did s || validate_handle s
54
-
55
-
(** Validate a datetime string *)
56
35
let validate_datetime s = Result.is_ok (Datetime.of_string s)
57
-
58
-
(** Validate a CID string (simplified - just check basic format) *)
59
-
let validate_cid s =
60
-
(* CIDs should start with 'b' (base32) and be reasonable length *)
61
-
String.length s >= 46 && s.[0] = 'b'
36
+
let validate_cid s = String.length s >= 46 && s.[0] = 'b'
62
37
63
-
(** Validate a language tag (BCP-47, simplified) *)
64
38
let validate_language s =
65
-
(* Basic BCP-47: 2-3 letter primary tag, optional subtags *)
66
39
let len = String.length s in
67
40
len >= 2 && len <= 35
68
41
&&
69
42
let first = s.[0] in
70
43
(first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')
71
44
72
-
(** Validate a URI *)
73
45
let validate_uri s =
74
-
(* Simple check: must contain :// *)
75
46
String.length s > 3
76
47
&& (String.sub s 0 7 = "http://"
77
48
|| String.sub s 0 8 = "https://"
78
49
|| String.contains s ':')
79
50
80
-
(** Validate a string against a format *)
81
51
let validate_format format s =
82
52
match format with
83
53
| Schema.Did -> validate_did s
···
107
77
let count_graphemes s =
108
78
let len = String.length s in
109
79
110
-
(* Get the codepoint at position i, returns (codepoint, next_pos) *)
111
80
let get_codepoint i =
112
81
if i >= len then (0, i)
113
82
else
114
83
let byte = Char.code s.[i] in
115
-
if byte land 0x80 = 0 then
116
-
(* ASCII *)
117
-
(byte, i + 1)
84
+
if byte land 0x80 = 0 then (byte, i + 1)
118
85
else if byte land 0xE0 = 0xC0 then
119
-
(* 2-byte sequence *)
120
86
let cp = (byte land 0x1F) lsl 6 in
121
87
if i + 1 < len then
122
88
let cp = cp lor (Char.code s.[i + 1] land 0x3F) in
123
89
(cp, i + 2)
124
90
else (cp, i + 2)
125
91
else if byte land 0xF0 = 0xE0 then
126
-
(* 3-byte sequence *)
127
92
let cp = (byte land 0x0F) lsl 12 in
128
93
if i + 2 < len then
129
94
let cp = cp lor ((Char.code s.[i + 1] land 0x3F) lsl 6) in
···
131
96
(cp, i + 3)
132
97
else (cp, i + 3)
133
98
else if byte land 0xF8 = 0xF0 then
134
-
(* 4-byte sequence *)
135
99
let cp = (byte land 0x07) lsl 18 in
136
100
if i + 3 < len then
137
101
let cp = cp lor ((Char.code s.[i + 1] land 0x3F) lsl 12) in
···
139
103
let cp = cp lor (Char.code s.[i + 3] land 0x3F) in
140
104
(cp, i + 4)
141
105
else (cp, i + 4)
142
-
else
143
-
(* Invalid, skip *)
144
-
(0, i + 1)
106
+
else (0, i + 1)
145
107
in
146
108
147
-
(* Check if codepoint is a regional indicator (flag letters) *)
148
109
let is_regional_indicator cp = cp >= 0x1F1E6 && cp <= 0x1F1FF in
149
-
150
-
(* Check if codepoint is ZWJ *)
151
110
let is_zwj cp = cp = 0x200D in
152
-
153
-
(* Check if codepoint is a variation selector *)
154
111
let is_variation_selector cp = cp >= 0xFE00 && cp <= 0xFE0F in
155
-
156
-
(* Check if codepoint is a skin tone modifier *)
157
112
let is_skin_tone cp = cp >= 0x1F3FB && cp <= 0x1F3FF in
158
113
159
-
(* Check if codepoint is a combining mark or modifier *)
160
114
let is_combining_or_modifier cp =
161
115
is_variation_selector cp || is_skin_tone cp
162
116
|| (cp >= 0x0300 && cp <= 0x036F)
163
-
(* Combining diacritical marks *)
164
117
|| (cp >= 0x1F3FB && cp <= 0x1F3FF)
165
-
||
166
-
(* Emoji modifiers *)
167
-
(cp >= 0xE0100 && cp <= 0xE01EF)
168
-
(* Variation selectors supplement *)
118
+
|| (cp >= 0xE0100 && cp <= 0xE01EF)
169
119
in
170
120
171
121
let rec count i acc =
···
174
124
let cp, next = get_codepoint i in
175
125
if cp = 0 then count next acc
176
126
else if is_regional_indicator cp then
177
-
(* Flag sequence: two regional indicators = one grapheme *)
178
127
let cp2, next2 = get_codepoint next in
179
128
if is_regional_indicator cp2 then count next2 (acc + 1)
180
129
else count next (acc + 1)
181
-
else if is_zwj cp || is_combining_or_modifier cp then
182
-
(* Skip ZWJ and modifiers - they extend the previous grapheme *)
183
-
count next acc
130
+
else if is_zwj cp || is_combining_or_modifier cp then count next acc
184
131
else
185
-
(* Start of a new grapheme cluster *)
186
-
(* Consume any following modifiers, variation selectors, or ZWJ sequences *)
187
132
let rec skip_extending pos =
188
133
if pos >= len then pos
189
134
else
190
135
let cp2, next2 = get_codepoint pos in
191
136
if is_zwj cp2 then
192
-
(* ZWJ: skip it and the following character *)
193
137
let _, next3 = get_codepoint next2 in
194
138
skip_extending next3
195
139
else if is_combining_or_modifier cp2 then skip_extending next2
···
202
146
203
147
(* === JSON value helpers === *)
204
148
205
-
type json = Simdjsont.Json.t
149
+
type json = Atproto_json.t
206
150
207
-
let get_string key = function
208
-
| Simdjsont.Json.Object pairs -> (
151
+
let get_string key json =
152
+
match Atproto_json.to_object_opt json with
153
+
| Some pairs -> (
209
154
match List.assoc_opt key pairs with
210
-
| Some (Simdjsont.Json.String s) -> Some s
211
-
| _ -> None)
212
-
| _ -> None
155
+
| Some v -> Atproto_json.to_string_opt v
156
+
| None -> None)
157
+
| None -> None
213
158
214
-
let get_int key = function
215
-
| Simdjsont.Json.Object pairs -> (
159
+
let get_int key json =
160
+
match Atproto_json.to_object_opt json with
161
+
| Some pairs -> (
216
162
match List.assoc_opt key pairs with
217
-
| Some (Simdjsont.Json.Int i) ->
218
-
if
219
-
Int64.compare i (Int64.of_int max_int) > 0
220
-
|| Int64.compare i (Int64.of_int min_int) < 0
221
-
then None
222
-
else Some (Int64.to_int i)
223
-
| _ -> None)
224
-
| _ -> None
163
+
| Some v -> (
164
+
match Atproto_json.to_int64_opt v with
165
+
| Some i -> Atproto_json.int_of_int64_opt i
166
+
| None -> None)
167
+
| None -> None)
168
+
| None -> None
225
169
226
-
let get_bool key = function
227
-
| Simdjsont.Json.Object pairs -> (
170
+
let get_bool key json =
171
+
match Atproto_json.to_object_opt json with
172
+
| Some pairs -> (
228
173
match List.assoc_opt key pairs with
229
-
| Some (Simdjsont.Json.Bool b) -> Some b
230
-
| _ -> None)
231
-
| _ -> None
174
+
| Some v -> Atproto_json.to_bool_opt v
175
+
| None -> None)
176
+
| None -> None
232
177
233
-
let is_null = function Simdjsont.Json.Null -> true | _ -> false
178
+
let is_null json = Option.is_some (Atproto_json.to_null_opt json)
234
179
235
180
(* === Field validators === *)
236
181
237
-
(** Validate a boolean value *)
238
182
let validate_boolean ~path json =
239
-
match json with
240
-
| Simdjsont.Json.Bool _ -> []
241
-
| _ -> [ error ~path "expected boolean" ]
183
+
match Atproto_json.to_bool_opt json with
184
+
| Some _ -> []
185
+
| None -> [ error ~path "expected boolean" ]
242
186
243
-
(** Validate an integer value with constraints *)
244
187
let validate_integer ~path ?minimum ?maximum ?enum ?const json =
245
-
match json with
246
-
| Simdjsont.Json.Int i64 -> (
247
-
let i_opt =
248
-
if
249
-
Int64.compare i64 (Int64.of_int max_int) > 0
250
-
|| Int64.compare i64 (Int64.of_int min_int) < 0
251
-
then None
252
-
else Some (Int64.to_int i64)
253
-
in
254
-
match i_opt with
188
+
match Atproto_json.to_int64_opt json with
189
+
| Some i64 -> (
190
+
match Atproto_json.int_of_int64_opt i64 with
255
191
| None -> [ error ~path "integer out of int range" ]
256
192
| Some i ->
257
193
let errs = ref [] in
···
272
208
errs := error ~path (Printf.sprintf "must be <= %d" max) :: !errs
273
209
| _ -> ());
274
210
!errs)
275
-
| _ -> [ error ~path "expected integer" ]
211
+
| None -> [ error ~path "expected integer" ]
276
212
277
-
(** Validate a string value with constraints *)
278
213
let validate_string ~path ?format ?min_length ?max_length ?min_graphemes
279
214
?max_graphemes ?enum ?const ?known_values:_ json =
280
-
match json with
281
-
| Simdjsont.Json.String s ->
215
+
match Atproto_json.to_string_opt json with
216
+
| Some s ->
282
217
let errs = ref [] in
283
218
(match const with
284
219
| Some c when s <> c ->
···
317
252
:: !errs
318
253
| _ -> ());
319
254
!errs
320
-
| _ -> [ error ~path "expected string" ]
255
+
| None -> [ error ~path "expected string" ]
321
256
322
-
(** Validate a bytes value (expects $bytes object) *)
323
257
let validate_bytes ~path ?min_length ?max_length json =
324
-
match json with
325
-
| Simdjsont.Json.Object pairs -> (
258
+
match Atproto_json.to_object_opt json with
259
+
| Some pairs -> (
326
260
match List.assoc_opt "$bytes" pairs with
327
-
| Some (Simdjsont.Json.String b64) ->
328
-
(* Decode base64 to get actual length *)
329
-
let len = String.length b64 * 3 / 4 in
330
-
(* approximate *)
331
-
let errs = ref [] in
332
-
(match min_length with
333
-
| Some min when len < min ->
334
-
errs :=
335
-
error ~path (Printf.sprintf "bytes length must be >= %d" min)
336
-
:: !errs
337
-
| _ -> ());
338
-
(match max_length with
339
-
| Some max when len > max ->
340
-
errs :=
341
-
error ~path (Printf.sprintf "bytes length must be <= %d" max)
342
-
:: !errs
343
-
| _ -> ());
344
-
!errs
345
-
| _ -> [ error ~path "expected $bytes object" ])
346
-
| _ -> [ error ~path "expected $bytes object" ]
261
+
| Some v -> (
262
+
match Atproto_json.to_string_opt v with
263
+
| Some b64 ->
264
+
let len = String.length b64 * 3 / 4 in
265
+
let errs = ref [] in
266
+
(match min_length with
267
+
| Some min when len < min ->
268
+
errs :=
269
+
error ~path
270
+
(Printf.sprintf "bytes length must be >= %d" min)
271
+
:: !errs
272
+
| _ -> ());
273
+
(match max_length with
274
+
| Some max when len > max ->
275
+
errs :=
276
+
error ~path
277
+
(Printf.sprintf "bytes length must be <= %d" max)
278
+
:: !errs
279
+
| _ -> ());
280
+
!errs
281
+
| None -> [ error ~path "expected $bytes object" ])
282
+
| None -> [ error ~path "expected $bytes object" ])
283
+
| None -> [ error ~path "expected $bytes object" ]
347
284
348
-
(** Validate a CID link (expects $link object) *)
349
285
let validate_cid_link ~path json =
350
-
match json with
351
-
| Simdjsont.Json.Object pairs -> (
286
+
match Atproto_json.to_object_opt json with
287
+
| Some pairs -> (
352
288
match List.assoc_opt "$link" pairs with
353
-
| Some (Simdjsont.Json.String _cid) -> []
354
-
| _ -> [ error ~path "expected $link object" ])
355
-
| _ -> [ error ~path "expected $link object" ]
289
+
| Some v -> (
290
+
match Atproto_json.to_string_opt v with
291
+
| Some _ -> []
292
+
| None -> [ error ~path "expected $link object" ])
293
+
| None -> [ error ~path "expected $link object" ])
294
+
| None -> [ error ~path "expected $link object" ]
356
295
357
-
(** Validate a blob value *)
358
296
let validate_blob ~path ?max_size ?accept json =
359
-
match json with
360
-
| Simdjsont.Json.Object pairs -> (
297
+
match Atproto_json.to_object_opt json with
298
+
| Some pairs -> (
361
299
match List.assoc_opt "$type" pairs with
362
-
| Some (Simdjsont.Json.String "blob") ->
363
-
let errs = ref [] in
364
-
(* Check mimeType *)
365
-
(match List.assoc_opt "mimeType" pairs with
366
-
| Some (Simdjsont.Json.String mime) -> (
367
-
match accept with
368
-
| Some patterns ->
369
-
let matches =
370
-
List.exists
371
-
(fun pat ->
372
-
if String.contains pat '*' then
373
-
(* Wildcard pattern like "image/*" *)
374
-
let prefix =
375
-
String.sub pat 0 (String.index pat '*')
300
+
| Some type_val -> (
301
+
match Atproto_json.to_string_opt type_val with
302
+
| Some "blob" ->
303
+
let errs = ref [] in
304
+
(match List.assoc_opt "mimeType" pairs with
305
+
| Some mime_val -> (
306
+
match Atproto_json.to_string_opt mime_val with
307
+
| Some mime -> (
308
+
match accept with
309
+
| Some patterns ->
310
+
let matches =
311
+
List.exists
312
+
(fun pat ->
313
+
if String.contains pat '*' then
314
+
let prefix =
315
+
String.sub pat 0 (String.index pat '*')
316
+
in
317
+
String.length mime >= String.length prefix
318
+
&& String.sub mime 0 (String.length prefix)
319
+
= prefix
320
+
else pat = mime)
321
+
patterns
376
322
in
377
-
String.length mime >= String.length prefix
378
-
&& String.sub mime 0 (String.length prefix) = prefix
379
-
else pat = mime)
380
-
patterns
381
-
in
382
-
if not matches then
383
-
errs := error ~path "MIME type not accepted" :: !errs
384
-
| None -> ())
385
-
| Some _ -> errs := error ~path "mimeType must be string" :: !errs
386
-
| None -> errs := error ~path "missing mimeType" :: !errs);
387
-
(* Check size *)
388
-
(match List.assoc_opt "size" pairs with
389
-
| Some (Simdjsont.Json.Int size64) -> (
390
-
let size =
391
-
if
392
-
Int64.compare size64 (Int64.of_int max_int) > 0
393
-
|| Int64.compare size64 (Int64.of_int min_int) < 0
394
-
then None
395
-
else Some (Int64.to_int size64)
396
-
in
397
-
match size with
398
-
| None -> errs := error ~path "size out of int range" :: !errs
399
-
| Some size -> (
400
-
match max_size with
401
-
| Some max when size > max ->
402
-
errs :=
403
-
error ~path
404
-
(Printf.sprintf "blob size must be <= %d" max)
405
-
:: !errs
406
-
| _ -> ()))
407
-
| Some _ -> errs := error ~path "size must be integer" :: !errs
408
-
| None -> errs := error ~path "missing size" :: !errs);
409
-
(* Check ref *)
410
-
(match List.assoc_opt "ref" pairs with
411
-
| Some ref_val ->
412
-
errs := validate_cid_link ~path:(path @ [ "ref" ]) ref_val @ !errs
413
-
| None -> errs := error ~path "missing ref" :: !errs);
414
-
!errs
415
-
| _ -> [ error ~path "expected blob with $type" ])
416
-
| _ -> [ error ~path "expected blob object" ]
323
+
if not matches then
324
+
errs :=
325
+
error ~path "MIME type not accepted" :: !errs
326
+
| None -> ())
327
+
| None ->
328
+
errs := error ~path "mimeType must be string" :: !errs)
329
+
| None -> errs := error ~path "missing mimeType" :: !errs);
330
+
(match List.assoc_opt "size" pairs with
331
+
| Some size_val -> (
332
+
match Atproto_json.to_int64_opt size_val with
333
+
| Some size64 -> (
334
+
match Atproto_json.int_of_int64_opt size64 with
335
+
| None ->
336
+
errs := error ~path "size out of int range" :: !errs
337
+
| Some size -> (
338
+
match max_size with
339
+
| Some max when size > max ->
340
+
errs :=
341
+
error ~path
342
+
(Printf.sprintf "blob size must be <= %d" max)
343
+
:: !errs
344
+
| _ -> ()))
345
+
| None -> errs := error ~path "size must be integer" :: !errs)
346
+
| None -> errs := error ~path "missing size" :: !errs);
347
+
(match List.assoc_opt "ref" pairs with
348
+
| Some ref_val ->
349
+
errs :=
350
+
validate_cid_link ~path:(path @ [ "ref" ]) ref_val @ !errs
351
+
| None -> errs := error ~path "missing ref" :: !errs);
352
+
!errs
353
+
| _ -> [ error ~path "expected blob with $type" ])
354
+
| None -> [ error ~path "expected blob with $type" ])
355
+
| None -> [ error ~path "expected blob object" ]
417
356
418
357
(** Validate unknown type.
419
358
···
424
363
425
364
This is part of the data model restrictions. *)
426
365
let validate_unknown ~path json =
427
-
match json with
428
-
| Simdjsont.Json.Bool _ ->
429
-
[ error ~path "unknown type cannot contain boolean" ]
430
-
| Simdjsont.Json.Object pairs -> (
431
-
(* Check for $bytes - not allowed in unknown *)
432
-
match List.assoc_opt "$bytes" pairs with
433
-
| Some _ -> [ error ~path "unknown type cannot contain bytes ($bytes)" ]
434
-
| None -> (
435
-
(* Check for blob ($type: "blob") - not allowed in unknown *)
436
-
match List.assoc_opt "$type" pairs with
437
-
| Some (Simdjsont.Json.String "blob") ->
438
-
[ error ~path "unknown type cannot contain blob" ]
439
-
| _ -> []))
440
-
| _ -> []
366
+
match Atproto_json.to_bool_opt json with
367
+
| Some _ -> [ error ~path "unknown type cannot contain boolean" ]
368
+
| None -> (
369
+
match Atproto_json.to_object_opt json with
370
+
| Some pairs -> (
371
+
match List.assoc_opt "$bytes" pairs with
372
+
| Some _ ->
373
+
[ error ~path "unknown type cannot contain bytes ($bytes)" ]
374
+
| None -> (
375
+
match List.assoc_opt "$type" pairs with
376
+
| Some type_val -> (
377
+
match Atproto_json.to_string_opt type_val with
378
+
| Some "blob" ->
379
+
[ error ~path "unknown type cannot contain blob" ]
380
+
| _ -> [])
381
+
| None -> []))
382
+
| None -> [])
441
383
442
384
(* === Recursive validators === *)
443
385
444
386
type ref_resolver = string -> Schema.field_type option
445
-
(** Type for resolving refs to their schema definitions *)
446
387
447
-
(** Default resolver that doesn't resolve anything *)
448
388
let no_resolver : ref_resolver = fun _ -> None
449
389
450
-
(** Validate a field type *)
451
390
let rec validate_field_type ?(resolver : ref_resolver = no_resolver) ~path
452
391
~schema (json : json) : validation_error list =
453
392
match schema with
···
487
426
| Schema.Unknown { description = _ } -> validate_unknown ~path json
488
427
489
428
and validate_array ~resolver ~path (arr : Schema.array_type) json =
490
-
match json with
491
-
| Simdjsont.Json.Array items ->
429
+
match Atproto_json.to_array_opt json with
430
+
| Some items ->
492
431
let errs = ref [] in
493
-
(* Check length constraints *)
494
432
let len = List.length items in
495
433
(match arr.min_length with
496
434
| Some min when len < min ->
···
504
442
error ~path (Printf.sprintf "array must have <= %d items" max)
505
443
:: !errs
506
444
| _ -> ());
507
-
(* Validate each item *)
508
445
List.iteri
509
446
(fun i item ->
510
447
let item_path = path @ [ string_of_int i ] in
···
513
450
@ !errs)
514
451
items;
515
452
!errs
516
-
| _ -> [ error ~path "expected array" ]
453
+
| None -> [ error ~path "expected array" ]
517
454
518
455
and validate_object ~resolver ~path (obj : Schema.object_type) json =
519
-
match json with
520
-
| Simdjsont.Json.Object pairs ->
456
+
match Atproto_json.to_object_opt json with
457
+
| Some pairs ->
521
458
let errs = ref [] in
522
-
(* Check required fields *)
523
459
List.iter
524
460
(fun req ->
525
461
if not (List.mem_assoc req pairs) then
526
462
errs :=
527
463
error ~path:(path @ [ req ]) "required field missing" :: !errs)
528
464
obj.required;
529
-
(* Validate each property *)
530
465
List.iter
531
466
(fun (prop : Schema.property) ->
532
467
match List.assoc_opt prop.name pairs with
533
468
| Some value ->
534
-
(* Check if null is allowed *)
535
469
if is_null value && not (List.mem prop.name obj.nullable) then
536
470
errs :=
537
471
error ~path:(path @ [ prop.name ]) "field cannot be null"
···
544
478
| None -> ())
545
479
obj.properties;
546
480
!errs
547
-
| _ -> [ error ~path "expected object" ]
481
+
| None -> [ error ~path "expected object" ]
548
482
549
483
and validate_ref ~resolver ~path (ref_type : Schema.ref_type) json =
550
-
(* Try to resolve the ref and validate against the resolved schema *)
551
484
match resolver ref_type.ref_ with
552
485
| Some schema -> validate_field_type ~resolver ~path ~schema json
553
486
| None -> (
554
-
(* Fallback: require an object for unresolved refs *)
555
-
match json with
556
-
| Simdjsont.Json.Object _ -> []
557
-
| _ -> [ error ~path "expected object for ref" ])
487
+
match Atproto_json.to_object_opt json with
488
+
| Some _ -> []
489
+
| None -> [ error ~path "expected object for ref" ])
558
490
559
491
and validate_union ~resolver ~path (union : Schema.union_type) json =
560
-
match json with
561
-
| Simdjsont.Json.Object pairs -> (
492
+
match Atproto_json.to_object_opt json with
493
+
| Some pairs -> (
562
494
match List.assoc_opt "$type" pairs with
563
-
| Some (Simdjsont.Json.String type_ref) ->
564
-
let errs = ref [] in
565
-
(* Check if type is in allowed refs for closed unions *)
566
-
(if union.closed then
567
-
let allowed =
568
-
List.exists
569
-
(fun ref_str ->
570
-
(* Handle both full refs and local refs *)
571
-
ref_str = type_ref
572
-
|| String.contains ref_str '#'
573
-
&& String.contains type_ref '#'
574
-
&& String.sub ref_str
575
-
(String.rindex ref_str '#')
576
-
(String.length ref_str - String.rindex ref_str '#')
577
-
= String.sub type_ref
578
-
(String.rindex type_ref '#')
579
-
(String.length type_ref
580
-
- String.rindex type_ref '#'))
581
-
union.refs
582
-
in
583
-
if not allowed then
584
-
errs :=
585
-
error ~path
586
-
(Printf.sprintf "type %s not allowed in closed union"
587
-
type_ref)
588
-
:: !errs);
589
-
(* Validate inner content against the resolved type *)
590
-
(match resolver type_ref with
591
-
| Some schema ->
592
-
errs := validate_field_type ~resolver ~path ~schema json @ !errs
593
-
| None -> ());
594
-
!errs
595
-
| Some _ -> [ error ~path "union $type must be a string" ]
596
-
| None ->
597
-
[ error ~path "union requires $type" ]
598
-
(* Both open and closed need $type *))
599
-
| _ -> [ error ~path "expected object for union" ]
495
+
| Some type_val -> (
496
+
match Atproto_json.to_string_opt type_val with
497
+
| Some type_ref ->
498
+
let errs = ref [] in
499
+
(if union.closed then
500
+
let allowed =
501
+
List.exists
502
+
(fun ref_str ->
503
+
ref_str = type_ref
504
+
|| String.contains ref_str '#'
505
+
&& String.contains type_ref '#'
506
+
&& String.sub ref_str
507
+
(String.rindex ref_str '#')
508
+
(String.length ref_str
509
+
- String.rindex ref_str '#')
510
+
= String.sub type_ref
511
+
(String.rindex type_ref '#')
512
+
(String.length type_ref
513
+
- String.rindex type_ref '#'))
514
+
union.refs
515
+
in
516
+
if not allowed then
517
+
errs :=
518
+
error ~path
519
+
(Printf.sprintf "type %s not allowed in closed union"
520
+
type_ref)
521
+
:: !errs);
522
+
(match resolver type_ref with
523
+
| Some schema ->
524
+
errs :=
525
+
validate_field_type ~resolver ~path ~schema json @ !errs
526
+
| None -> ());
527
+
!errs
528
+
| None -> [ error ~path "union $type must be a string" ])
529
+
| None -> [ error ~path "union requires $type" ])
530
+
| None -> [ error ~path "expected object for union" ]
600
531
601
-
(** Validate a record against a record definition *)
602
532
let validate_record ?(resolver : ref_resolver = no_resolver) ~path
603
533
(record_def : Schema.object_type) json =
604
534
validate_object ~resolver ~path record_def json
+1
-1
lib/sync/dune
+1
-1
lib/sync/dune
+68
-55
lib/sync/firehose.ml
+68
-55
lib/sync/firehose.ml
···
119
119
| Dag_cbor.Map pairs ->
120
120
let op =
121
121
match List.assoc_opt "op" pairs with
122
-
| Some (Dag_cbor.Int i) -> Int64.to_int i
122
+
| Some (Dag_cbor.Int i) ->
123
+
Atproto_json.int_of_int64_default ~default:0 i
123
124
| _ -> 0
124
125
in
125
126
let t =
···
346
347
347
348
(** Convert an operation to JSON. Does not include record content - use
348
349
[add_record_to_op_json] to add it. *)
349
-
let operation_to_json (op : operation) : Yojson.Safe.t =
350
+
let operation_to_json (op : operation) : Atproto_json.t =
350
351
let action =
351
352
match op.action with
352
353
| `Create -> "create"
353
354
| `Update -> "update"
354
355
| `Delete -> "delete"
355
356
in
356
-
let base = [ ("action", `String action); ("path", `String op.path) ] in
357
-
let with_cid =
358
-
match op.cid with
359
-
| Some c -> base @ [ ("cid", `String (Cid.to_string c)) ]
360
-
| None -> base
357
+
let base =
358
+
[
359
+
("action", Atproto_json.string action);
360
+
("path", Atproto_json.string op.path);
361
+
]
361
362
in
362
-
`Assoc with_cid
363
+
match op.cid with
364
+
| Some c ->
365
+
Atproto_json.object_
366
+
(base @ [ ("cid", Atproto_json.string (Cid.to_string c)) ])
367
+
| None -> Atproto_json.object_ base
363
368
364
369
(** Add a record field to an operation JSON object *)
365
-
let add_record_to_op_json (op_json : Yojson.Safe.t) (record : Yojson.Safe.t) :
366
-
Yojson.Safe.t =
367
-
match op_json with
368
-
| `Assoc fields -> `Assoc (fields @ [ ("record", record) ])
369
-
| _ -> op_json
370
+
let add_record_to_op_json (op_json : Atproto_json.t) (record : Atproto_json.t) :
371
+
Atproto_json.t =
372
+
match Atproto_json.to_object_opt op_json with
373
+
| Some fields -> Atproto_json.object_ (fields @ [ ("record", record) ])
374
+
| None -> op_json
370
375
371
376
(** Convert a commit event to JSON. Operations don't include record content. *)
372
-
let commit_event_to_json (evt : commit_event) : Yojson.Safe.t =
373
-
let ops = `List (List.map operation_to_json evt.ops) in
374
-
`Assoc
377
+
let commit_event_to_json (evt : commit_event) : Atproto_json.t =
378
+
let ops = Atproto_json.array (List.map operation_to_json evt.ops) in
379
+
Atproto_json.object_
375
380
[
376
-
("type", `String "commit");
377
-
("seq", `Intlit (Int64.to_string evt.seq));
378
-
("repo", `String evt.repo);
379
-
("rev", `String evt.rev);
380
-
("commit", `String (Cid.to_string evt.commit));
381
+
("type", Atproto_json.string "commit");
382
+
("seq", Atproto_json.int64 evt.seq);
383
+
("repo", Atproto_json.string evt.repo);
384
+
("rev", Atproto_json.string evt.rev);
385
+
("commit", Atproto_json.string (Cid.to_string evt.commit));
381
386
("ops", ops);
382
387
]
383
388
384
389
(** Convert an identity event to JSON *)
385
-
let identity_event_to_json (e : identity_event) : Yojson.Safe.t =
390
+
let identity_event_to_json (e : identity_event) : Atproto_json.t =
386
391
let base =
387
392
[
388
-
("type", `String "identity");
389
-
("seq", `Intlit (Int64.to_string e.seq));
390
-
("did", `String e.did);
391
-
("time", `String e.time);
393
+
("type", Atproto_json.string "identity");
394
+
("seq", Atproto_json.int64 e.seq);
395
+
("did", Atproto_json.string e.did);
396
+
("time", Atproto_json.string e.time);
392
397
]
393
398
in
394
399
let with_handle =
395
400
match e.handle with
396
-
| Some h -> base @ [ ("handle", `String h) ]
401
+
| Some h -> base @ [ ("handle", Atproto_json.string h) ]
397
402
| None -> base
398
403
in
399
-
`Assoc with_handle
404
+
Atproto_json.object_ with_handle
400
405
401
406
(** Convert an account event to JSON *)
402
-
let account_event_to_json (e : account_event) : Yojson.Safe.t =
407
+
let account_event_to_json (e : account_event) : Atproto_json.t =
403
408
let base =
404
409
[
405
-
("type", `String "account");
406
-
("seq", `Intlit (Int64.to_string e.seq));
407
-
("did", `String e.did);
408
-
("time", `String e.time);
409
-
("active", `Bool e.active);
410
+
("type", Atproto_json.string "account");
411
+
("seq", Atproto_json.int64 e.seq);
412
+
("did", Atproto_json.string e.did);
413
+
("time", Atproto_json.string e.time);
414
+
("active", Atproto_json.bool e.active);
410
415
]
411
416
in
412
417
let with_status =
413
418
match e.status with
414
-
| Some s -> base @ [ ("status", `String s) ]
419
+
| Some s -> base @ [ ("status", Atproto_json.string s) ]
415
420
| None -> base
416
421
in
417
-
`Assoc with_status
422
+
Atproto_json.object_ with_status
418
423
419
424
(** Convert a handle event to JSON *)
420
-
let handle_event_to_json (e : handle_event) : Yojson.Safe.t =
421
-
`Assoc
425
+
let handle_event_to_json (e : handle_event) : Atproto_json.t =
426
+
Atproto_json.object_
422
427
[
423
-
("type", `String "handle");
424
-
("seq", `Intlit (Int64.to_string e.seq));
425
-
("did", `String e.did);
426
-
("time", `String e.time);
427
-
("handle", `String e.handle);
428
+
("type", Atproto_json.string "handle");
429
+
("seq", Atproto_json.int64 e.seq);
430
+
("did", Atproto_json.string e.did);
431
+
("time", Atproto_json.string e.time);
432
+
("handle", Atproto_json.string e.handle);
428
433
]
429
434
430
435
(** Convert a tombstone event to JSON *)
431
-
let tombstone_event_to_json (e : tombstone_event) : Yojson.Safe.t =
432
-
`Assoc
436
+
let tombstone_event_to_json (e : tombstone_event) : Atproto_json.t =
437
+
Atproto_json.object_
433
438
[
434
-
("type", `String "tombstone");
435
-
("seq", `Intlit (Int64.to_string e.seq));
436
-
("did", `String e.did);
437
-
("time", `String e.time);
439
+
("type", Atproto_json.string "tombstone");
440
+
("seq", Atproto_json.int64 e.seq);
441
+
("did", Atproto_json.string e.did);
442
+
("time", Atproto_json.string e.time);
438
443
]
439
444
440
445
(** Convert an info message to JSON *)
441
-
let info_message_to_json (m : info_message) : Yojson.Safe.t =
442
-
let base = [ ("type", `String "info"); ("name", `String m.name) ] in
446
+
let info_message_to_json (m : info_message) : Atproto_json.t =
447
+
let base =
448
+
[
449
+
("type", Atproto_json.string "info"); ("name", Atproto_json.string m.name);
450
+
]
451
+
in
443
452
let with_msg =
444
453
match m.message with
445
-
| Some msg -> base @ [ ("message", `String msg) ]
454
+
| Some msg -> base @ [ ("message", Atproto_json.string msg) ]
446
455
| None -> base
447
456
in
448
-
`Assoc with_msg
457
+
Atproto_json.object_ with_msg
449
458
450
459
(** Convert any event to JSON. For commit events, operations don't include
451
460
record content - extract blocks separately and use [add_record_to_op_json].
···
458
467
| Tombstone e -> tombstone_event_to_json e
459
468
| Info m -> info_message_to_json m
460
469
| StreamError msg ->
461
-
`Assoc [ ("type", `String "error"); ("message", `String msg) ]
470
+
Atproto_json.object_
471
+
[
472
+
("type", Atproto_json.string "error");
473
+
("message", Atproto_json.string msg);
474
+
]
462
475
463
476
(** Convert an event to a JSON string *)
464
-
let event_to_json_string evt = Yojson.Safe.to_string (event_to_json evt)
477
+
let event_to_json_string evt = Atproto_json.encode (event_to_json evt)
+22
-20
lib/xrpc/client.ml
+22
-20
lib/xrpc/client.ml
···
16
16
17
17
(** {1 Types} *)
18
18
19
+
type json = Atproto_json.t
20
+
19
21
type request = {
20
22
meth : [ `GET | `POST ];
21
23
uri : Uri.t;
···
36
38
(** XRPC error returned by the server *)
37
39
38
40
(** Parse an XRPC error from JSON *)
39
-
let parse_xrpc_error json =
40
-
match json with
41
-
| `Assoc pairs ->
41
+
let parse_xrpc_error (json : json) =
42
+
match Atproto_json.to_object_opt json with
43
+
| Some pairs ->
42
44
let error =
43
-
match List.assoc_opt "error" pairs with
44
-
| Some (`String s) -> s
45
-
| _ -> "Unknown"
46
-
in
47
-
let message =
48
-
match List.assoc_opt "message" pairs with
49
-
| Some (`String s) -> Some s
50
-
| _ -> None
45
+
match Atproto_json.get_string_opt "error" pairs with
46
+
| Some s -> s
47
+
| None -> "Unknown"
51
48
in
49
+
let message = Atproto_json.get_string_opt "message" pairs in
52
50
{ error; message }
53
-
| _ -> { error = "Unknown"; message = None }
51
+
| None -> { error = "Unknown"; message = None }
54
52
55
53
(** Format an XRPC error as string *)
56
54
let xrpc_error_to_string err =
···
159
157
160
158
(** Parse JSON response body *)
161
159
let parse_json_body body =
162
-
try Ok (Yojson.Basic.from_string body)
163
-
with Yojson.Json_error msg -> Error (Parse_error msg)
160
+
match Atproto_json.decode body with
161
+
| Ok (json : json) -> Ok json
162
+
| Error msg -> Error (Parse_error msg)
164
163
165
164
(** Handle response based on status code *)
166
165
let handle_response response =
167
166
if response.status >= 200 && response.status < 300 then
168
-
if String.length response.body = 0 then Ok `Null
167
+
if String.length response.body = 0 then Ok Atproto_json.null
169
168
else parse_json_body response.body
170
169
else if response.status >= 400 then
171
170
match parse_json_body response.body with
···
176
175
(** {1 XRPC Methods} *)
177
176
178
177
(** Make a query (GET) request *)
179
-
let query client ~nsid ?(params = []) () =
178
+
let query client ~nsid ?(params = []) () : (json, error) result =
180
179
let uri = build_url client nsid params in
181
180
let headers = build_headers client None in
182
181
let request = { meth = `GET; uri; headers; body = None } in
···
184
183
handle_response response
185
184
186
185
(** Make a procedure (POST) request *)
187
-
let procedure client ~nsid ?(params = []) ?input () =
186
+
let procedure client ~nsid ?(params = []) ?input () : (json, error) result =
188
187
let uri = build_url client nsid params in
189
188
let body, content_type =
190
189
match input with
191
-
| Some json -> (Some (Yojson.Basic.to_string json), Some "application/json")
190
+
| Some json -> (Some (Atproto_json.encode json), Some "application/json")
192
191
| None -> (None, None)
193
192
in
194
193
let headers = build_headers client content_type in
···
227
226
match Nsid.of_string "com.atproto.server.createSession" with
228
227
| Ok nsid ->
229
228
let input =
230
-
`Assoc
231
-
[ ("identifier", `String identifier); ("password", `String password) ]
229
+
Atproto_json.object_
230
+
[
231
+
("identifier", Atproto_json.string identifier);
232
+
("password", Atproto_json.string password);
233
+
]
232
234
in
233
235
procedure client ~nsid ~input ()
234
236
| Error _ -> Error (Parse_error "invalid nsid")
+11
-2
lib/xrpc/dune
+11
-2
lib/xrpc/dune
···
1
1
(library
2
2
(name atproto_xrpc)
3
3
(public_name atproto-xrpc)
4
-
(libraries atproto_effects atproto_syntax atproto_lexicon yojson uri
5
-
mirage-crypto-rng digestif base64 cstruct unix))
4
+
(libraries
5
+
atproto_effects
6
+
atproto_syntax
7
+
atproto_lexicon
8
+
atproto_json
9
+
uri
10
+
mirage-crypto-rng
11
+
digestif
12
+
base64
13
+
cstruct
14
+
unix))
+48
-39
lib/xrpc/oauth.ml
+48
-39
lib/xrpc/oauth.ml
···
11
11
12
12
(** {1 Types} *)
13
13
14
+
type json = Atproto_json.t
15
+
16
+
let int_of_int64_opt i =
17
+
if i >= Int64.of_int min_int && i <= Int64.of_int max_int then
18
+
Some (Int64.to_int i)
19
+
else None
20
+
14
21
type client_config = {
15
22
client_id : string;
16
23
redirect_uri : Uri.t;
···
82
89
(** Authorization server metadata *)
83
90
84
91
(** Parse authorization server metadata from JSON *)
85
-
let parse_authorization_server json : (authorization_server, error) result =
86
-
match json with
87
-
| `Assoc pairs -> (
88
-
let get_string key =
89
-
match List.assoc_opt key pairs with
90
-
| Some (`String s) -> Some s
91
-
| _ -> None
92
-
in
92
+
let parse_authorization_server (json : json) :
93
+
(authorization_server, error) result =
94
+
match Atproto_json.to_object_opt json with
95
+
| Some pairs -> (
96
+
let get_string key = Atproto_json.get_string_opt key pairs in
93
97
let get_string_list key =
94
-
match List.assoc_opt key pairs with
95
-
| Some (`List items) ->
96
-
List.filter_map (function `String s -> Some s | _ -> None) items
97
-
| _ -> []
98
+
match Atproto_json.get_array_opt key pairs with
99
+
| Some items ->
100
+
List.filter_map (fun item -> Atproto_json.to_string_opt item) items
101
+
| None -> []
98
102
in
99
103
match
100
104
( get_string "issuer",
···
155
159
{ state; code_verifier; authorization_url }
156
160
157
161
(** Parse tokens from JSON response *)
158
-
let parse_tokens json : (tokens, error) result =
159
-
match json with
160
-
| `Assoc pairs -> (
161
-
let get_string key =
162
-
match List.assoc_opt key pairs with
163
-
| Some (`String s) -> Some s
164
-
| _ -> None
165
-
in
162
+
let parse_tokens (json : json) : (tokens, error) result =
163
+
match Atproto_json.to_object_opt json with
164
+
| Some pairs -> (
165
+
let get_string key = Atproto_json.get_string_opt key pairs in
166
166
let get_int key =
167
-
match List.assoc_opt key pairs with
168
-
| Some (`Int i) -> Some i
169
-
| _ -> None
167
+
match Atproto_json.get_int64_opt key pairs with
168
+
| Some i -> Atproto_json.int_of_int64_opt i
169
+
| None -> None
170
170
in
171
171
let get_string_list key =
172
-
match List.assoc_opt key pairs with
173
-
| Some (`String s) -> String.split_on_char ' ' s
174
-
| Some (`List items) ->
175
-
List.filter_map (function `String s -> Some s | _ -> None) items
176
-
| _ -> []
172
+
match Atproto_json.get key pairs with
173
+
| Some json -> (
174
+
match Atproto_json.to_string_opt json with
175
+
| Some s -> String.split_on_char ' ' s
176
+
| None -> (
177
+
match Atproto_json.to_array_opt json with
178
+
| Some items -> List.filter_map Atproto_json.to_string_opt items
179
+
| None -> []))
180
+
| None -> []
177
181
in
178
182
match get_string "access_token" with
179
183
| Some access_token ->
···
220
224
}
221
225
222
226
(** Create DPoP proof JWT header *)
223
-
let make_header ~jwk =
224
-
`Assoc
225
-
[ ("typ", `String "dpop+jwt"); ("alg", `String "ES256"); ("jwk", jwk) ]
227
+
let make_header ~jwk : json =
228
+
Atproto_json.object_
229
+
[
230
+
("typ", Atproto_json.string "dpop+jwt");
231
+
("alg", Atproto_json.string "ES256");
232
+
("jwk", jwk);
233
+
]
226
234
227
235
(** Create DPoP proof JWT payload *)
228
-
let make_payload ~params ~jti ~iat =
236
+
let make_payload ~params ~jti ~iat : json =
229
237
let claims =
230
238
[
231
-
("jti", `String jti);
232
-
("htm", `String params.method_);
233
-
("htu", `String (Uri.to_string (Uri.with_query params.uri [])));
234
-
("iat", `Int iat);
239
+
("jti", Atproto_json.string jti);
240
+
("htm", Atproto_json.string params.method_);
241
+
( "htu",
242
+
Atproto_json.string (Uri.to_string (Uri.with_query params.uri [])) );
243
+
("iat", Atproto_json.int iat);
235
244
]
236
245
in
237
246
let claims =
238
247
match params.nonce with
239
-
| Some n -> ("nonce", `String n) :: claims
248
+
| Some n -> ("nonce", Atproto_json.string n) :: claims
240
249
| None -> claims
241
250
in
242
251
let claims =
···
248
257
Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet
249
258
(Digestif.SHA256.to_raw_string hash)
250
259
in
251
-
("ath", `String ath) :: claims
260
+
("ath", Atproto_json.string ath) :: claims
252
261
| None -> claims
253
262
in
254
-
`Assoc claims
263
+
Atproto_json.object_ claims
255
264
256
265
(** Generate a JTI (JWT ID) *)
257
266
let generate_jti () =
+34
-24
lib/xrpc/server.ml
+34
-24
lib/xrpc/server.ml
···
30
30
}
31
31
(** HTTP response to send *)
32
32
33
+
type json = Atproto_json.t
34
+
33
35
type context = {
34
36
params : (string * string) list;
35
-
input : Yojson.Basic.t option;
37
+
input : json option;
36
38
auth : auth_info option;
37
39
headers : (string * string) list;
38
40
}
···
62
64
let json =
63
65
match err.message with
64
66
| Some msg ->
65
-
`Assoc [ ("error", `String err.error); ("message", `String msg) ]
66
-
| None -> `Assoc [ ("error", `String err.error) ]
67
+
Atproto_json.object_
68
+
[
69
+
("error", Atproto_json.string err.error);
70
+
("message", Atproto_json.string msg);
71
+
]
72
+
| None -> Atproto_json.object_ [ ("error", Atproto_json.string err.error) ]
67
73
in
68
74
{
69
75
status = err.status;
70
76
headers = [ ("Content-Type", "application/json") ];
71
-
body = Yojson.Basic.to_string json;
77
+
body = Atproto_json.encode json;
72
78
}
73
79
74
80
(** {1 Handler Types} *)
75
81
76
-
type handler_result = (Yojson.Basic.t, xrpc_error) result
82
+
type handler_result = (json, xrpc_error) result
77
83
(** Result returned by handlers *)
78
84
79
85
type handler = context -> handler_result
···
128
134
|> List.map (fun (k, vs) -> match vs with [] -> (k, "") | v :: _ -> (k, v))
129
135
130
136
(** Parse JSON body *)
131
-
let parse_body (body : string option) : Yojson.Basic.t option =
137
+
let parse_body (body : string option) : json option =
132
138
match body with
133
139
| None -> None
134
140
| Some "" -> None
135
141
| Some s -> (
136
-
try Some (Yojson.Basic.from_string s) with Yojson.Json_error _ -> None)
142
+
match Atproto_json.decode s with Ok json -> Some json | Error _ -> None)
137
143
138
144
(** Find endpoint by NSID *)
139
145
let find_endpoint server nsid =
···
186
192
{
187
193
status = 200;
188
194
headers = [ ("Content-Type", "application/json") ];
189
-
body = Yojson.Basic.to_string json;
195
+
body = Atproto_json.encode json;
190
196
}
191
197
| Error err -> error_to_response err))
192
198
···
244
250
let optional_param ctx name : string option = List.assoc_opt name ctx.params
245
251
246
252
(** Get a required field from JSON input *)
247
-
let require_input_field ctx field : (Yojson.Basic.t, xrpc_error) result =
253
+
let require_input_field ctx field : (json, xrpc_error) result =
248
254
match ctx.input with
249
255
| None -> Error (invalid_request ~message:"Missing request body" ())
250
-
| Some (`Assoc pairs) -> (
251
-
match List.assoc_opt field pairs with
252
-
| Some v -> Ok v
256
+
| Some body -> (
257
+
match Atproto_json.to_object_opt body with
258
+
| Some pairs -> (
259
+
match List.assoc_opt field pairs with
260
+
| Some v -> Ok v
261
+
| None ->
262
+
Error
263
+
(invalid_request
264
+
~message:(Printf.sprintf "Missing required field: %s" field)
265
+
()))
253
266
| None ->
254
-
Error
255
-
(invalid_request
256
-
~message:(Printf.sprintf "Missing required field: %s" field)
257
-
()))
258
-
| Some _ ->
259
-
Error (invalid_request ~message:"Request body must be an object" ())
267
+
Error (invalid_request ~message:"Request body must be an object" ()))
260
268
261
269
(** Get a required string field from JSON input *)
262
270
let require_input_string ctx field : (string, xrpc_error) result =
263
271
match require_input_field ctx field with
264
-
| Ok (`String s) -> Ok s
265
-
| Ok _ ->
266
-
Error
267
-
(invalid_request
268
-
~message:(Printf.sprintf "Field %s must be a string" field)
269
-
())
272
+
| Ok json -> (
273
+
match Atproto_json.to_string_opt json with
274
+
| Some s -> Ok s
275
+
| None ->
276
+
Error
277
+
(invalid_request
278
+
~message:(Printf.sprintf "Field %s must be a string" field)
279
+
()))
270
280
| Error e -> Error e
271
281
272
282
(** Require authentication in context *)
+27
-21
test/api/test_api.ml
+27
-21
test/api/test_api.ml
···
83
83
let rt = Richtext.of_string "Hello" in
84
84
let json = Richtext.to_json rt in
85
85
match json with
86
-
| `Assoc pairs ->
86
+
| Simdjsont.Json.Object pairs ->
87
87
Alcotest.(check bool) "has text" true (List.mem_assoc "text" pairs);
88
88
Alcotest.(check bool)
89
89
"no facets key" false
···
96
96
let rt = Richtext.add_facet rt facet in
97
97
let json = Richtext.to_json rt in
98
98
match json with
99
-
| `Assoc pairs ->
99
+
| Simdjsont.Json.Object pairs ->
100
100
Alcotest.(check bool) "has text" true (List.mem_assoc "text" pairs);
101
101
Alcotest.(check bool) "has facets" true (List.mem_assoc "facets" pairs)
102
102
| _ -> Alcotest.fail "expected object"
103
103
104
104
let test_richtext_of_json () =
105
-
let json =
106
-
`Assoc
105
+
let json : Richtext.json =
106
+
Simdjsont.Json.Object
107
107
[
108
-
("text", `String "Hello");
108
+
("text", Simdjsont.Json.String "Hello");
109
109
( "facets",
110
-
`List
110
+
Simdjsont.Json.Array
111
111
[
112
-
`Assoc
112
+
Simdjsont.Json.Object
113
113
[
114
114
( "index",
115
-
`Assoc [ ("byteStart", `Int 0); ("byteEnd", `Int 5) ] );
115
+
Simdjsont.Json.Object
116
+
[
117
+
("byteStart", Simdjsont.Json.Int 0L);
118
+
("byteEnd", Simdjsont.Json.Int 5L);
119
+
] );
116
120
( "features",
117
-
`List
121
+
Simdjsont.Json.Array
118
122
[
119
-
`Assoc
123
+
Simdjsont.Json.Object
120
124
[
121
-
("$type", `String "app.bsky.richtext.facet#mention");
122
-
("did", `String "did:plc:test");
125
+
( "$type",
126
+
Simdjsont.Json.String
127
+
"app.bsky.richtext.facet#mention" );
128
+
("did", Simdjsont.Json.String "did:plc:test");
123
129
];
124
130
] );
125
131
];
···
136
142
let slice = Richtext.byte_slice ~start:10 ~end_:20 in
137
143
let json = Richtext.byte_slice_to_json slice in
138
144
match json with
139
-
| `Assoc pairs ->
145
+
| Simdjsont.Json.Object pairs ->
140
146
Alcotest.(check (option int))
141
147
"byteStart" (Some 10)
142
148
(match List.assoc_opt "byteStart" pairs with
143
-
| Some (`Int i) -> Some i
149
+
| Some (Simdjsont.Json.Int i) -> Atproto_json.int_of_int64_opt i
144
150
| _ -> None);
145
151
Alcotest.(check (option int))
146
152
"byteEnd" (Some 20)
147
153
(match List.assoc_opt "byteEnd" pairs with
148
-
| Some (`Int i) -> Some i
154
+
| Some (Simdjsont.Json.Int i) -> Atproto_json.int_of_int64_opt i
149
155
| _ -> None)
150
156
| _ -> Alcotest.fail "expected object"
151
157
···
153
159
let feature = Richtext.Mention { did = "did:plc:test" } in
154
160
let json = Richtext.feature_to_json feature in
155
161
match json with
156
-
| `Assoc pairs ->
162
+
| Simdjsont.Json.Object pairs ->
157
163
Alcotest.(check (option string))
158
164
"$type" (Some "app.bsky.richtext.facet#mention")
159
165
(match List.assoc_opt "$type" pairs with
160
-
| Some (`String s) -> Some s
166
+
| Some (Simdjsont.Json.String s) -> Some s
161
167
| _ -> None)
162
168
| _ -> Alcotest.fail "expected object"
163
169
···
165
171
let feature = Richtext.Link { uri = "https://example.com" } in
166
172
let json = Richtext.feature_to_json feature in
167
173
match json with
168
-
| `Assoc pairs ->
174
+
| Simdjsont.Json.Object pairs ->
169
175
Alcotest.(check (option string))
170
176
"$type" (Some "app.bsky.richtext.facet#link")
171
177
(match List.assoc_opt "$type" pairs with
172
-
| Some (`String s) -> Some s
178
+
| Some (Simdjsont.Json.String s) -> Some s
173
179
| _ -> None)
174
180
| _ -> Alcotest.fail "expected object"
175
181
···
177
183
let feature = Richtext.Tag { tag = "ocaml" } in
178
184
let json = Richtext.feature_to_json feature in
179
185
match json with
180
-
| `Assoc pairs ->
186
+
| Simdjsont.Json.Object pairs ->
181
187
Alcotest.(check (option string))
182
188
"$type" (Some "app.bsky.richtext.facet#tag")
183
189
(match List.assoc_opt "$type" pairs with
184
-
| Some (`String s) -> Some s
190
+
| Some (Simdjsont.Json.String s) -> Some s
185
191
| _ -> None)
186
192
| _ -> Alcotest.fail "expected object"
187
193
+40
-36
test/compliance/compliance_report.ml
+40
-36
test/compliance/compliance_report.ml
···
118
118
report_pass_rate = pass_rate;
119
119
}
120
120
121
-
(** Convert report to JSON *)
122
121
let result_to_json (r : test_result) =
123
-
`Assoc
122
+
Atproto_json.object_
124
123
[
125
-
("input", `String r.input);
124
+
("input", Atproto_json.string r.input);
126
125
( "expected",
127
-
`String
126
+
Atproto_json.string
128
127
(match r.expected with `Valid -> "valid" | `Invalid -> "invalid") );
129
128
( "actual",
130
-
`String
129
+
Atproto_json.string
131
130
(match r.actual with `Valid -> "valid" | `Invalid -> "invalid") );
132
-
("passed", `Bool r.passed);
133
-
("error", match r.error_msg with Some s -> `String s | None -> `Null);
131
+
("passed", Atproto_json.bool r.passed);
132
+
( "error",
133
+
match r.error_msg with
134
+
| Some s -> Atproto_json.string s
135
+
| None -> Atproto_json.null );
134
136
]
135
137
136
138
let category_to_json (c : category_result) =
137
-
`Assoc
139
+
Atproto_json.object_
138
140
[
139
-
("name", `String c.cat_name);
140
-
("description", `String c.cat_description);
141
-
("fixture_file", `String c.cat_fixture_file);
142
-
("total", `Int c.cat_total);
143
-
("passed", `Int c.cat_passed);
144
-
("failed", `Int c.cat_failed);
141
+
("name", Atproto_json.string c.cat_name);
142
+
("description", Atproto_json.string c.cat_description);
143
+
("fixture_file", Atproto_json.string c.cat_fixture_file);
144
+
("total", Atproto_json.int c.cat_total);
145
+
("passed", Atproto_json.int c.cat_passed);
146
+
("failed", Atproto_json.int c.cat_failed);
145
147
( "pass_rate",
146
-
`Float
148
+
Atproto_json.float
147
149
(if c.cat_total > 0 then
148
150
float_of_int c.cat_passed /. float_of_int c.cat_total *. 100.0
149
151
else 0.0) );
150
-
("results", `List (List.map result_to_json c.cat_results));
152
+
("results", Atproto_json.array (List.map result_to_json c.cat_results));
151
153
]
152
154
153
155
let suite_to_json (s : suite_result) =
154
-
`Assoc
156
+
Atproto_json.object_
155
157
[
156
-
("name", `String s.suite_name);
158
+
("name", Atproto_json.string s.suite_name);
157
159
( "spec_url",
158
-
match s.suite_spec_url with Some u -> `String u | None -> `Null );
159
-
("total", `Int s.suite_total);
160
-
("passed", `Int s.suite_passed);
161
-
("failed", `Int s.suite_failed);
160
+
match s.suite_spec_url with
161
+
| Some u -> Atproto_json.string u
162
+
| None -> Atproto_json.null );
163
+
("total", Atproto_json.int s.suite_total);
164
+
("passed", Atproto_json.int s.suite_passed);
165
+
("failed", Atproto_json.int s.suite_failed);
162
166
( "pass_rate",
163
-
`Float
167
+
Atproto_json.float
164
168
(if s.suite_total > 0 then
165
169
float_of_int s.suite_passed /. float_of_int s.suite_total *. 100.0
166
170
else 0.0) );
167
-
("categories", `List (List.map category_to_json s.suite_categories));
171
+
( "categories",
172
+
Atproto_json.array (List.map category_to_json s.suite_categories) );
168
173
]
169
174
170
175
let report_to_json (r : report) =
171
-
`Assoc
176
+
Atproto_json.object_
172
177
[
173
-
("title", `String r.report_title);
174
-
("version", `String r.report_version);
175
-
("generated_at", `String r.report_generated_at);
176
-
("repository", `String r.report_repository);
177
-
("total_tests", `Int r.report_total_tests);
178
-
("total_passed", `Int r.report_total_passed);
179
-
("total_failed", `Int r.report_total_failed);
180
-
("pass_rate", `Float r.report_pass_rate);
181
-
("suites", `List (List.map suite_to_json r.report_suites));
178
+
("title", Atproto_json.string r.report_title);
179
+
("version", Atproto_json.string r.report_version);
180
+
("generated_at", Atproto_json.string r.report_generated_at);
181
+
("repository", Atproto_json.string r.report_repository);
182
+
("total_tests", Atproto_json.int r.report_total_tests);
183
+
("total_passed", Atproto_json.int r.report_total_passed);
184
+
("total_failed", Atproto_json.int r.report_total_failed);
185
+
("pass_rate", Atproto_json.float r.report_pass_rate);
186
+
("suites", Atproto_json.array (List.map suite_to_json r.report_suites));
182
187
]
183
188
184
-
(** Write report to JSON file *)
185
189
let write_json_report filename (report : report) =
186
190
let json = report_to_json report in
187
191
let oc = open_out filename in
188
-
output_string oc (Yojson.Safe.pretty_to_string json);
192
+
output_string oc (Atproto_json.encode_pretty json);
189
193
output_char oc '\n';
190
194
close_out oc
191
195
+2
-2
test/compliance/dune
+2
-2
test/compliance/dune
···
1
1
(library
2
2
(name compliance_report)
3
3
(modules compliance_report)
4
-
(libraries yojson unix))
4
+
(libraries atproto_json unix))
5
5
6
6
(executable
7
7
(name run_compliance)
···
13
13
atproto-ipld
14
14
atproto-mst
15
15
mirage-crypto-rng.unix
16
-
yojson
16
+
atproto_json
17
17
unix))
+72
-117
test/compliance/run_compliance.ml
+72
-117
test/compliance/run_compliance.ml
···
38
38
let ic = open_in filename in
39
39
let content = In_channel.input_all ic in
40
40
close_in ic;
41
-
Yojson.Safe.from_string content
41
+
match Atproto_json.decode content with
42
+
| Ok json -> json
43
+
| Error e -> failwith ("JSON parse error: " ^ e)
42
44
43
45
(** Base64 decode *)
44
46
let base64_decode s =
···
187
189
188
190
(* ==================== Crypto Tests ==================== *)
189
191
192
+
let get_string_field key pairs =
193
+
match Atproto_json.get_string_opt key pairs with
194
+
| Some s -> s
195
+
| None -> failwith ("missing " ^ key)
196
+
197
+
let get_bool_field key pairs =
198
+
match Atproto_json.get key pairs with
199
+
| Some v -> (
200
+
match Atproto_json.to_bool_opt v with
201
+
| Some b -> b
202
+
| None -> failwith ("invalid bool: " ^ key))
203
+
| None -> failwith ("missing " ^ key)
204
+
205
+
let get_int_field key pairs =
206
+
match Atproto_json.get_int_opt key pairs with
207
+
| Some i -> i
208
+
| None -> failwith ("missing or invalid int: " ^ key)
209
+
190
210
let run_crypto_tests () =
191
-
(* Signature verification tests *)
192
211
let sig_fixtures =
193
212
load_json_fixture (fixture_dir ^ "/crypto/signature-fixtures.json")
194
213
in
195
214
let sig_results =
196
-
match sig_fixtures with
197
-
| `List items ->
215
+
match Atproto_json.to_array_opt sig_fixtures with
216
+
| Some items ->
198
217
List.map
199
218
(fun item ->
200
-
match item with
201
-
| `Assoc fields ->
219
+
match Atproto_json.to_object_opt item with
220
+
| Some fields ->
202
221
let comment =
203
-
match List.assoc_opt "comment" fields with
204
-
| Some (`String s) -> s
205
-
| _ -> "unknown"
222
+
match Atproto_json.get_string_opt "comment" fields with
223
+
| Some s -> s
224
+
| None -> "unknown"
206
225
in
207
-
let message_b64 =
208
-
match List.assoc_opt "messageBase64" fields with
209
-
| Some (`String s) -> s
210
-
| _ -> failwith "missing messageBase64"
211
-
in
212
-
let algorithm =
213
-
match List.assoc_opt "algorithm" fields with
214
-
| Some (`String s) -> s
215
-
| _ -> failwith "missing algorithm"
216
-
in
217
-
let public_key_did =
218
-
match List.assoc_opt "publicKeyDid" fields with
219
-
| Some (`String s) -> s
220
-
| _ -> failwith "missing publicKeyDid"
221
-
in
222
-
let signature_b64 =
223
-
match List.assoc_opt "signatureBase64" fields with
224
-
| Some (`String s) -> s
225
-
| _ -> failwith "missing signatureBase64"
226
-
in
227
-
let valid_signature =
228
-
match List.assoc_opt "validSignature" fields with
229
-
| Some (`Bool b) -> b
230
-
| _ -> failwith "missing validSignature"
231
-
in
226
+
let message_b64 = get_string_field "messageBase64" fields in
227
+
let algorithm = get_string_field "algorithm" fields in
228
+
let public_key_did = get_string_field "publicKeyDid" fields in
229
+
let signature_b64 = get_string_field "signatureBase64" fields in
230
+
let valid_signature = get_bool_field "validSignature" fields in
232
231
233
232
let message = base64_decode message_b64 in
234
233
let signature = base64_decode signature_b64 in
···
262
261
with _ -> `Invalid
263
262
in
264
263
make_result ~input:comment ~expected ~actual ()
265
-
| _ -> failwith "invalid fixture format")
264
+
| None -> failwith "invalid fixture format")
266
265
items
267
-
| _ -> failwith "invalid fixtures format"
266
+
| None -> failwith "invalid fixtures format"
268
267
in
269
268
270
269
let sig_category =
···
273
272
~fixture_file:"signature-fixtures.json" sig_results
274
273
in
275
274
276
-
(* did:key encoding tests for P-256 *)
277
275
let p256_fixtures =
278
276
load_json_fixture (fixture_dir ^ "/crypto/w3c_didkey_P256.json")
279
277
in
280
278
let p256_results =
281
-
match p256_fixtures with
282
-
| `List items ->
279
+
match Atproto_json.to_array_opt p256_fixtures with
280
+
| Some items ->
283
281
List.map
284
282
(fun item ->
285
-
match item with
286
-
| `Assoc fields ->
287
-
let did =
288
-
match List.assoc_opt "publicDidKey" fields with
289
-
| Some (`String s) -> s
290
-
| _ -> failwith "missing publicDidKey"
291
-
in
283
+
match Atproto_json.to_object_opt item with
284
+
| Some fields ->
285
+
let did = get_string_field "publicDidKey" fields in
292
286
let actual =
293
287
try
294
288
match Atproto_crypto.Did_key.decode did with
···
297
291
with _ -> `Invalid
298
292
in
299
293
make_result ~input:did ~expected:`Valid ~actual ()
300
-
| _ -> failwith "invalid fixture format")
294
+
| None -> failwith "invalid fixture format")
301
295
items
302
-
| _ -> []
296
+
| None -> []
303
297
in
304
298
305
299
let p256_category =
···
308
302
~fixture_file:"w3c_didkey_P256.json" p256_results
309
303
in
310
304
311
-
(* did:key encoding tests for K-256 *)
312
305
let k256_fixtures =
313
306
load_json_fixture (fixture_dir ^ "/crypto/w3c_didkey_K256.json")
314
307
in
315
308
let k256_results =
316
-
match k256_fixtures with
317
-
| `List items ->
309
+
match Atproto_json.to_array_opt k256_fixtures with
310
+
| Some items ->
318
311
List.map
319
312
(fun item ->
320
-
match item with
321
-
| `Assoc fields ->
322
-
let did =
323
-
match List.assoc_opt "publicDidKey" fields with
324
-
| Some (`String s) -> s
325
-
| _ -> failwith "missing publicDidKey"
326
-
in
313
+
match Atproto_json.to_object_opt item with
314
+
| Some fields ->
315
+
let did = get_string_field "publicDidKey" fields in
327
316
let actual =
328
317
try
329
318
match Atproto_crypto.Did_key.decode did with
···
332
321
with _ -> `Invalid
333
322
in
334
323
make_result ~input:did ~expected:`Valid ~actual ()
335
-
| _ -> failwith "invalid fixture format")
324
+
| None -> failwith "invalid fixture format")
336
325
items
337
-
| _ -> []
326
+
| None -> []
338
327
in
339
328
340
329
let k256_category =
···
350
339
(* ==================== Data Model Tests ==================== *)
351
340
352
341
let run_data_model_tests () =
353
-
(* DAG-CBOR encoding and CID computation *)
354
342
let fixtures =
355
343
load_json_fixture (fixture_dir ^ "/data-model/data-model-fixtures.json")
356
344
in
357
345
let results =
358
-
match fixtures with
359
-
| `List items ->
346
+
match Atproto_json.to_array_opt fixtures with
347
+
| Some items ->
360
348
List.mapi
361
349
(fun i item ->
362
-
match item with
363
-
| `Assoc fields ->
364
-
let expected_cid =
365
-
match List.assoc_opt "cid" fields with
366
-
| Some (`String s) -> s
367
-
| _ -> failwith "missing cid"
368
-
in
369
-
let cbor_b64 =
370
-
match List.assoc_opt "cbor_base64" fields with
371
-
| Some (`String s) -> s
372
-
| _ -> failwith "missing cbor_base64"
373
-
in
374
-
let json_value =
375
-
match List.assoc_opt "json" fields with
376
-
| Some j -> j
377
-
| _ -> failwith "missing json"
378
-
in
379
-
380
-
let _ = json_value in
381
-
(* We'll use cbor_base64 directly *)
350
+
match Atproto_json.to_object_opt item with
351
+
| Some fields ->
352
+
let expected_cid = get_string_field "cid" fields in
353
+
let cbor_b64 = get_string_field "cbor_base64" fields in
382
354
let cbor_str = base64_decode cbor_b64 in
383
355
384
356
let actual =
···
391
363
make_result
392
364
~input:(Printf.sprintf "fixture[%d]" i)
393
365
~expected:`Valid ~actual ()
394
-
| _ -> failwith "invalid fixture format")
366
+
| None -> failwith "invalid fixture format")
395
367
items
396
-
| _ -> []
368
+
| None -> []
397
369
in
398
370
399
371
let cbor_category =
···
402
374
~fixture_file:"data-model-fixtures.json" results
403
375
in
404
376
405
-
(* CID syntax validation - preserve whitespace to test whitespace handling *)
406
377
let cid_valid =
407
378
load_test_vectors (fixture_dir ^ "/syntax/cid_syntax_valid.txt")
408
379
in
···
415
386
List.map
416
387
(fun input ->
417
388
let actual =
418
-
(* Use syntax validation for CID syntax tests - this matches the Go
419
-
implementation which does lenient validation without fully decoding *)
420
389
if Atproto_ipld.Cid.is_valid_syntax input then `Valid else `Invalid
421
390
in
422
391
make_result ~input ~expected:`Valid ~actual ())
···
446
415
(* ==================== MST Tests ==================== *)
447
416
448
417
let run_mst_tests () =
449
-
(* Key height tests *)
450
418
let height_fixtures =
451
419
load_json_fixture (fixture_dir ^ "/mst/key_heights.json")
452
420
in
453
421
let height_results =
454
-
match height_fixtures with
455
-
| `Assoc items ->
422
+
match Atproto_json.to_object_opt height_fixtures with
423
+
| Some items ->
456
424
List.map
457
425
(fun (key, expected_height) ->
458
426
let expected_h =
459
-
match expected_height with
460
-
| `Int h -> h
461
-
| _ -> failwith "invalid height"
427
+
match Atproto_json.to_int_opt expected_height with
428
+
| Some h -> h
429
+
| None -> failwith "invalid height"
462
430
in
463
431
let actual =
464
432
try
···
468
436
in
469
437
make_result ~input:key ~expected:`Valid ~actual ())
470
438
items
471
-
| _ -> []
439
+
| None -> []
472
440
in
473
441
474
442
let height_category =
···
476
444
~fixture_file:"key_heights.json" height_results
477
445
in
478
446
479
-
(* Common prefix tests *)
480
447
let prefix_fixtures =
481
448
load_json_fixture (fixture_dir ^ "/mst/common_prefix.json")
482
449
in
483
450
let prefix_results =
484
-
match prefix_fixtures with
485
-
| `List items ->
451
+
match Atproto_json.to_array_opt prefix_fixtures with
452
+
| Some items ->
486
453
List.mapi
487
454
(fun i item ->
488
-
match item with
489
-
| `Assoc fields ->
490
-
let left =
491
-
match List.assoc_opt "left" fields with
492
-
| Some (`String s) -> s
493
-
| _ -> failwith "missing left"
494
-
in
495
-
let right =
496
-
match List.assoc_opt "right" fields with
497
-
| Some (`String s) -> s
498
-
| _ -> failwith "missing right"
499
-
in
500
-
let expected_len =
501
-
match List.assoc_opt "len" fields with
502
-
| Some (`Int n) -> n
503
-
| _ -> failwith "missing len"
504
-
in
455
+
match Atproto_json.to_object_opt item with
456
+
| Some fields ->
457
+
let left = get_string_field "left" fields in
458
+
let right = get_string_field "right" fields in
459
+
let expected_len = get_int_field "len" fields in
505
460
let actual =
506
461
try
507
462
let len = Atproto_mst.common_prefix_len left right in
···
511
466
make_result
512
467
~input:(Printf.sprintf "prefix[%d]: %s, %s" i left right)
513
468
~expected:`Valid ~actual ()
514
-
| _ -> failwith "invalid fixture format")
469
+
| None -> failwith "invalid fixture format")
515
470
items
516
-
| _ -> []
471
+
| None -> []
517
472
in
518
473
519
474
let prefix_category =
+1
-1
test/crypto/dune
+1
-1
test/crypto/dune
+61
-55
test/crypto/test_crypto.ml
+61
-55
test/crypto/test_crypto.ml
···
13
13
let ic = open_in path in
14
14
let content = In_channel.input_all ic in
15
15
close_in ic;
16
-
Yojson.Safe.from_string content
16
+
match Atproto_json.decode content with
17
+
| Ok json -> json
18
+
| Error e -> failwith ("JSON parse error: " ^ e)
17
19
18
20
(** Base64 decode *)
19
21
let base64_decode s =
···
80
82
81
83
let test_signature_verification () =
82
84
let fixtures = read_fixture "signature-fixtures.json" in
83
-
match fixtures with
84
-
| `List items ->
85
+
match Atproto_json.to_array_opt fixtures with
86
+
| Some items ->
85
87
List.iter
86
88
(fun item ->
87
-
match item with
88
-
| `Assoc fields ->
89
+
match Atproto_json.to_object_opt item with
90
+
| Some fields ->
89
91
let comment =
90
-
match List.assoc_opt "comment" fields with
91
-
| Some (`String s) -> s
92
-
| _ -> "unknown"
92
+
match Atproto_json.get_string_opt "comment" fields with
93
+
| Some s -> s
94
+
| None -> "unknown"
93
95
in
94
96
let message_b64 =
95
-
match List.assoc_opt "messageBase64" fields with
96
-
| Some (`String s) -> s
97
-
| _ -> failwith "missing messageBase64"
97
+
match Atproto_json.get_string_opt "messageBase64" fields with
98
+
| Some s -> s
99
+
| None -> failwith "missing messageBase64"
98
100
in
99
101
let algorithm =
100
-
match List.assoc_opt "algorithm" fields with
101
-
| Some (`String s) -> s
102
-
| _ -> failwith "missing algorithm"
102
+
match Atproto_json.get_string_opt "algorithm" fields with
103
+
| Some s -> s
104
+
| None -> failwith "missing algorithm"
103
105
in
104
106
let public_key_did =
105
-
match List.assoc_opt "publicKeyDid" fields with
106
-
| Some (`String s) -> s
107
-
| _ -> failwith "missing publicKeyDid"
107
+
match Atproto_json.get_string_opt "publicKeyDid" fields with
108
+
| Some s -> s
109
+
| None -> failwith "missing publicKeyDid"
108
110
in
109
111
let signature_b64 =
110
-
match List.assoc_opt "signatureBase64" fields with
111
-
| Some (`String s) -> s
112
-
| _ -> failwith "missing signatureBase64"
112
+
match Atproto_json.get_string_opt "signatureBase64" fields with
113
+
| Some s -> s
114
+
| None -> failwith "missing signatureBase64"
113
115
in
114
116
let valid_signature =
115
-
match List.assoc_opt "validSignature" fields with
116
-
| Some (`Bool b) -> b
117
-
| _ -> failwith "missing validSignature"
117
+
match Atproto_json.get "validSignature" fields with
118
+
| Some json -> (
119
+
match Atproto_json.to_bool_opt json with
120
+
| Some b -> b
121
+
| None -> failwith "missing validSignature")
122
+
| None -> failwith "missing validSignature"
118
123
in
119
124
let tags =
120
-
match List.assoc_opt "tags" fields with
121
-
| Some (`List tags) ->
122
-
List.filter_map
123
-
(function `String s -> Some s | _ -> None)
124
-
tags
125
-
| _ -> []
125
+
match Atproto_json.get_array_opt "tags" fields with
126
+
| Some tags -> List.filter_map Atproto_json.to_string_opt tags
127
+
| None -> []
126
128
in
127
129
128
130
(* Decode inputs *)
···
168
170
(Printf.sprintf "signature validity: %s" comment)
169
171
valid_signature is_valid
170
172
end
171
-
| _ -> failwith "expected object in fixture array")
173
+
| None -> failwith "expected object in fixture array")
172
174
items
173
-
| _ -> failwith "expected array in fixture file"
175
+
| None -> failwith "expected array in fixture file"
174
176
175
177
(* === did:key encoding tests for K-256 === *)
176
178
177
179
let test_didkey_k256 () =
178
180
let fixtures = read_fixture "w3c_didkey_K256.json" in
179
-
match fixtures with
180
-
| `List items ->
181
+
match Atproto_json.to_array_opt fixtures with
182
+
| Some items ->
181
183
List.iter
182
184
(fun item ->
183
-
match item with
184
-
| `Assoc fields -> (
185
+
match Atproto_json.to_object_opt item with
186
+
| Some fields -> (
185
187
let private_key_hex =
186
-
match List.assoc_opt "privateKeyBytesHex" fields with
187
-
| Some (`String s) -> s
188
-
| _ -> failwith "missing privateKeyBytesHex"
188
+
match
189
+
Atproto_json.get_string_opt "privateKeyBytesHex" fields
190
+
with
191
+
| Some s -> s
192
+
| None -> failwith "missing privateKeyBytesHex"
189
193
in
190
194
let expected_did =
191
-
match List.assoc_opt "publicDidKey" fields with
192
-
| Some (`String s) -> s
193
-
| _ -> failwith "missing publicDidKey"
195
+
match Atproto_json.get_string_opt "publicDidKey" fields with
196
+
| Some s -> s
197
+
| None -> failwith "missing publicDidKey"
194
198
in
195
199
196
200
(* Decode private key and derive public key *)
···
215
219
| Ok (K256 _pub') -> ()
216
220
| Ok (P256 _) ->
217
221
Alcotest.fail "decoded as P256 instead of K256"))
218
-
| _ -> failwith "expected object in fixture array")
222
+
| None -> failwith "expected object in fixture array")
219
223
items
220
-
| _ -> failwith "expected array in fixture file"
224
+
| None -> failwith "expected array in fixture file"
221
225
222
226
(* === did:key encoding tests for P-256 === *)
223
227
224
228
let test_didkey_p256 () =
225
229
let fixtures = read_fixture "w3c_didkey_P256.json" in
226
-
match fixtures with
227
-
| `List items ->
230
+
match Atproto_json.to_array_opt fixtures with
231
+
| Some items ->
228
232
List.iter
229
233
(fun item ->
230
-
match item with
231
-
| `Assoc fields -> (
234
+
match Atproto_json.to_object_opt item with
235
+
| Some fields -> (
232
236
let private_key_b58 =
233
-
match List.assoc_opt "privateKeyBytesBase58" fields with
234
-
| Some (`String s) -> s
235
-
| _ -> failwith "missing privateKeyBytesBase58"
237
+
match
238
+
Atproto_json.get_string_opt "privateKeyBytesBase58" fields
239
+
with
240
+
| Some s -> s
241
+
| None -> failwith "missing privateKeyBytesBase58"
236
242
in
237
243
let expected_did =
238
-
match List.assoc_opt "publicDidKey" fields with
239
-
| Some (`String s) -> s
240
-
| _ -> failwith "missing publicDidKey"
244
+
match Atproto_json.get_string_opt "publicDidKey" fields with
245
+
| Some s -> s
246
+
| None -> failwith "missing publicDidKey"
241
247
in
242
248
243
249
(* Decode private key and derive public key *)
···
266
272
| Ok (P256 _pub') -> ()
267
273
| Ok (K256 _) ->
268
274
Alcotest.fail "decoded as K256 instead of P256")))
269
-
| _ -> failwith "expected object in fixture array")
275
+
| None -> failwith "expected object in fixture array")
270
276
items
271
-
| _ -> failwith "expected array in fixture file"
277
+
| None -> failwith "expected array in fixture file"
272
278
273
279
(* === Basic P256 signing tests === *)
274
280
+1
-1
test/ipld/dune
+1
-1
test/ipld/dune
+173
-132
test/ipld/test_ipld.ml
+173
-132
test/ipld/test_ipld.ml
···
255
255
let ic = open_in path in
256
256
let content = really_input_string ic (in_channel_length ic) in
257
257
close_in ic;
258
-
Yojson.Basic.from_string content
258
+
match Atproto_json.decode content with
259
+
| Ok json -> json
260
+
| Error e -> failwith ("JSON parse error: " ^ e)
259
261
260
262
(** Base64 decode helper using the base64 library *)
261
263
let base64_decode_test s =
···
264
266
| Ok decoded -> decoded
265
267
| Error _ -> failwith ("base64 decode failed: " ^ s)
266
268
267
-
(** Convert Yojson.Basic.t to Dag_cbor.json *)
268
-
let rec yojson_to_dag_cbor_json (j : Yojson.Basic.t) : Dag_cbor.json =
269
-
match j with
270
-
| `Null -> `Null
271
-
| `Bool b -> `Bool b
272
-
| `Int i -> `Int i
273
-
| `Float f -> `Float f
274
-
| `String s -> `String s
275
-
| `List l -> `List (List.map yojson_to_dag_cbor_json l)
276
-
| `Assoc pairs ->
277
-
`Assoc (List.map (fun (k, v) -> (k, yojson_to_dag_cbor_json v)) pairs)
269
+
(** Convert Atproto_json.t to Dag_cbor.json *)
270
+
let rec atproto_json_to_dag_cbor_json (j : Atproto_json.t) : Dag_cbor.json =
271
+
match Atproto_json.to_null_opt j with
272
+
| Some () -> `Null
273
+
| None -> (
274
+
match Atproto_json.to_bool_opt j with
275
+
| Some b -> `Bool b
276
+
| None -> (
277
+
match Atproto_json.to_int_opt j with
278
+
| Some i -> `Int i
279
+
| None -> (
280
+
match Atproto_json.to_float_opt j with
281
+
| Some f -> `Float f
282
+
| None -> (
283
+
match Atproto_json.to_string_opt j with
284
+
| Some s -> `String s
285
+
| None -> (
286
+
match Atproto_json.to_array_opt j with
287
+
| Some l ->
288
+
`List (List.map atproto_json_to_dag_cbor_json l)
289
+
| None -> (
290
+
match Atproto_json.to_object_opt j with
291
+
| Some pairs ->
292
+
`Assoc
293
+
(List.map
294
+
(fun (k, v) ->
295
+
(k, atproto_json_to_dag_cbor_json v))
296
+
pairs)
297
+
| None -> failwith "Unknown JSON type"))))))
278
298
279
299
let test_dag_cbor_fixtures () =
280
300
let fixtures = read_fixture_json "data-model/data-model-fixtures.json" in
281
-
match fixtures with
282
-
| `List items ->
301
+
match Atproto_json.to_array_opt fixtures with
302
+
| Some items ->
283
303
List.iteri
284
304
(fun idx item ->
285
-
match item with
286
-
| `Assoc pairs -> (
305
+
match Atproto_json.to_object_opt item with
306
+
| Some pairs -> (
287
307
let json_val =
288
-
List.assoc_opt "json" pairs
289
-
|> Option.map yojson_to_dag_cbor_json
290
-
in
291
-
let cbor_b64 =
292
-
match List.assoc_opt "cbor_base64" pairs with
293
-
| Some (`String s) -> Some s
294
-
| _ -> None
295
-
in
296
-
let expected_cid =
297
-
match List.assoc_opt "cid" pairs with
298
-
| Some (`String s) -> Some s
299
-
| _ -> None
308
+
Atproto_json.get "json" pairs
309
+
|> Option.map atproto_json_to_dag_cbor_json
300
310
in
311
+
let cbor_b64 = Atproto_json.get_string_opt "cbor_base64" pairs in
312
+
let expected_cid = Atproto_json.get_string_opt "cid" pairs in
301
313
match (json_val, cbor_b64, expected_cid) with
302
314
| Some json, Some b64, Some cid_str -> (
303
-
(* Parse JSON to value *)
304
315
match Dag_cbor.of_json json with
305
316
| Ok value -> (
306
-
(* Encode to CBOR *)
307
317
let encoded = Dag_cbor.encode value in
308
-
(* Decode expected CBOR *)
309
318
let expected_cbor = base64_decode_test b64 in
310
-
(* Check CBOR matches *)
311
319
Alcotest.(check string)
312
320
(Printf.sprintf "fixture %d: CBOR encoding" idx)
313
321
expected_cbor encoded;
314
-
(* Check CID matches *)
315
322
let cid = Cid.of_dag_cbor encoded in
316
323
Alcotest.(check string)
317
324
(Printf.sprintf "fixture %d: CID" idx)
318
325
cid_str (Cid.to_string cid);
319
-
(* Test decode roundtrip *)
320
326
match Dag_cbor.decode encoded with
321
327
| Ok decoded ->
322
328
Alcotest.(check bool)
···
331
337
Alcotest.fail
332
338
(Printf.sprintf "fixture %d: JSON parse failed: %s" idx
333
339
(Dag_cbor.error_to_string e)))
334
-
| _ -> () (* Skip incomplete fixtures *))
335
-
| _ -> ())
340
+
| _ -> ())
341
+
| None -> ())
336
342
items
337
-
| _ -> Alcotest.fail "Expected JSON array"
343
+
| None -> Alcotest.fail "Expected JSON array"
338
344
339
345
let test_dag_cbor_key_sorting () =
340
346
(* Test that map keys are sorted by length first, then lexicographically *)
···
1010
1016
1011
1017
(** Validate AT Protocol data model JSON. This validates the structural rules
1012
1018
beyond basic JSON parsing. *)
1013
-
let rec validate_data_model (j : Yojson.Basic.t) :
1019
+
let rec validate_data_model (j : Atproto_json.t) :
1014
1020
(unit, data_model_error) result =
1015
-
match j with
1016
-
| `Null | `Bool _ | `String _ -> Ok ()
1017
-
| `Int _ -> Ok ()
1018
-
| `Float f ->
1019
-
(* Floats must be integer-like in AT Protocol *)
1020
-
if Float.is_integer f then Ok () else Error Float_not_integer
1021
-
| `List items ->
1022
-
(* Validate each item in the list *)
1023
-
List.fold_left
1024
-
(fun acc item ->
1025
-
match acc with
1026
-
| Error e -> Error e
1027
-
| Ok () -> validate_data_model item)
1028
-
(Ok ()) items
1029
-
| `Assoc pairs ->
1030
-
(* Check for special AT Protocol objects *)
1031
-
let keys = List.map fst pairs in
1032
-
if List.mem "$link" keys then validate_link pairs
1033
-
else if List.mem "$bytes" keys then validate_bytes pairs
1034
-
else if List.mem "$type" keys then validate_typed_object pairs
1035
-
else
1036
-
(* Regular object - validate all values *)
1037
-
List.fold_left
1038
-
(fun acc (_, v) ->
1039
-
match acc with Error e -> Error e | Ok () -> validate_data_model v)
1040
-
(Ok ()) pairs
1021
+
match Atproto_json.to_null_opt j with
1022
+
| Some () -> Ok ()
1023
+
| None -> (
1024
+
match Atproto_json.to_bool_opt j with
1025
+
| Some _ -> Ok ()
1026
+
| None -> (
1027
+
match Atproto_json.to_string_opt j with
1028
+
| Some _ -> Ok ()
1029
+
| None -> (
1030
+
match Atproto_json.to_int_opt j with
1031
+
| Some _ -> Ok ()
1032
+
| None -> (
1033
+
match Atproto_json.to_float_opt j with
1034
+
| Some f ->
1035
+
if Float.is_integer f then Ok ()
1036
+
else Error Float_not_integer
1037
+
| None -> (
1038
+
match Atproto_json.to_array_opt j with
1039
+
| Some items ->
1040
+
List.fold_left
1041
+
(fun acc item ->
1042
+
match acc with
1043
+
| Error e -> Error e
1044
+
| Ok () -> validate_data_model item)
1045
+
(Ok ()) items
1046
+
| None -> (
1047
+
match Atproto_json.to_object_opt j with
1048
+
| Some pairs ->
1049
+
let keys = List.map fst pairs in
1050
+
if List.mem "$link" keys then validate_link pairs
1051
+
else if List.mem "$bytes" keys then
1052
+
validate_bytes pairs
1053
+
else if List.mem "$type" keys then
1054
+
validate_typed_object pairs
1055
+
else
1056
+
List.fold_left
1057
+
(fun acc (_, v) ->
1058
+
match acc with
1059
+
| Error e -> Error e
1060
+
| Ok () -> validate_data_model v)
1061
+
(Ok ()) pairs
1062
+
| None -> Ok ()))))))
1041
1063
1042
1064
and validate_link pairs =
1043
-
match pairs with
1044
-
| [ ("$link", `String cid_str) ] ->
1045
-
(* Validate CID string *)
1046
-
if Cid.is_valid_syntax cid_str then Ok () else Error Link_invalid_cid
1047
-
| [ ("$link", _) ] -> Error Link_wrong_type
1048
-
| _ when List.length pairs > 1 -> Error Link_extra_fields
1049
-
| _ -> Error Link_wrong_type
1065
+
let link_val = Atproto_json.get "$link" pairs in
1066
+
match link_val with
1067
+
| Some v -> (
1068
+
if List.length pairs > 1 then Error Link_extra_fields
1069
+
else
1070
+
match Atproto_json.to_string_opt v with
1071
+
| Some cid_str ->
1072
+
if Cid.is_valid_syntax cid_str then Ok ()
1073
+
else Error Link_invalid_cid
1074
+
| None -> Error Link_wrong_type)
1075
+
| None -> Error Link_wrong_type
1050
1076
1051
1077
and validate_bytes pairs =
1052
-
match pairs with
1053
-
| [ ("$bytes", `String _) ] -> Ok ()
1054
-
| [ ("$bytes", _) ] -> Error Bytes_wrong_type
1055
-
| _ when List.length pairs > 1 -> Error Bytes_extra_fields
1056
-
| _ -> Error Bytes_wrong_type
1078
+
let bytes_val = Atproto_json.get "$bytes" pairs in
1079
+
match bytes_val with
1080
+
| Some v -> (
1081
+
if List.length pairs > 1 then Error Bytes_extra_fields
1082
+
else
1083
+
match Atproto_json.to_string_opt v with
1084
+
| Some _ -> Ok ()
1085
+
| None -> Error Bytes_wrong_type)
1086
+
| None -> Error Bytes_wrong_type
1057
1087
1058
1088
and validate_typed_object pairs =
1059
-
(* Check $type field *)
1060
-
let type_val = List.assoc_opt "$type" pairs in
1089
+
let type_val = Atproto_json.get "$type" pairs in
1061
1090
match type_val with
1062
-
| Some `Null -> Error Type_null
1063
-
| Some (`String s) when String.length s = 0 -> Error Type_empty
1064
-
| Some (`String s) when s = "blob" ->
1065
-
(* Validate blob structure *)
1066
-
validate_blob pairs
1067
-
| Some (`String _) ->
1068
-
(* Valid record - validate all values *)
1069
-
List.fold_left
1070
-
(fun acc (_, v) ->
1071
-
match acc with Error e -> Error e | Ok () -> validate_data_model v)
1072
-
(Ok ()) pairs
1073
-
| Some _ -> Error Type_not_string
1074
-
| None -> Ok () (* No $type is fine for non-records *)
1091
+
| Some v -> (
1092
+
match Atproto_json.to_null_opt v with
1093
+
| Some () -> Error Type_null
1094
+
| None -> (
1095
+
match Atproto_json.to_string_opt v with
1096
+
| Some s when String.length s = 0 -> Error Type_empty
1097
+
| Some s when s = "blob" -> validate_blob pairs
1098
+
| Some _ ->
1099
+
List.fold_left
1100
+
(fun acc (_, v) ->
1101
+
match acc with
1102
+
| Error e -> Error e
1103
+
| Ok () -> validate_data_model v)
1104
+
(Ok ()) pairs
1105
+
| None -> Error Type_not_string))
1106
+
| None -> Ok ()
1075
1107
1076
1108
and validate_blob pairs =
1077
-
(* Blob must have: $type = "blob", ref (CID link), mimeType (string), size (int) *)
1078
-
let size_val = List.assoc_opt "size" pairs in
1079
-
let ref_val = List.assoc_opt "ref" pairs in
1080
-
match (size_val, ref_val) with
1081
-
| Some (`String _), _ -> Error Blob_size_not_int
1082
-
| _, None -> Error Blob_missing_ref
1083
-
| Some (`Int _), Some ref_json ->
1084
-
(* Validate the ref is a proper link *)
1085
-
validate_data_model ref_json
1086
-
| _ ->
1087
-
(* Validate all fields *)
1088
-
List.fold_left
1089
-
(fun acc (_, v) ->
1090
-
match acc with Error e -> Error e | Ok () -> validate_data_model v)
1091
-
(Ok ()) pairs
1109
+
let size_val = Atproto_json.get "size" pairs in
1110
+
let ref_val = Atproto_json.get "ref" pairs in
1111
+
match ref_val with
1112
+
| None -> Error Blob_missing_ref
1113
+
| Some ref_json -> (
1114
+
match size_val with
1115
+
| Some sv -> (
1116
+
match Atproto_json.to_string_opt sv with
1117
+
| Some _ -> Error Blob_size_not_int
1118
+
| None -> (
1119
+
match Atproto_json.to_int_opt sv with
1120
+
| Some _ -> validate_data_model ref_json
1121
+
| None ->
1122
+
List.fold_left
1123
+
(fun acc (_, v) ->
1124
+
match acc with
1125
+
| Error e -> Error e
1126
+
| Ok () -> validate_data_model v)
1127
+
(Ok ()) pairs))
1128
+
| None ->
1129
+
List.fold_left
1130
+
(fun acc (_, v) ->
1131
+
match acc with
1132
+
| Error e -> Error e
1133
+
| Ok () -> validate_data_model v)
1134
+
(Ok ()) pairs)
1092
1135
1093
-
(** Validate top-level - must be an object *)
1094
-
let validate_top_level (j : Yojson.Basic.t) : (unit, data_model_error) result =
1095
-
match j with
1096
-
| `Assoc pairs ->
1097
-
(* Validate all values recursively *)
1136
+
let validate_top_level (j : Atproto_json.t) : (unit, data_model_error) result =
1137
+
match Atproto_json.to_object_opt j with
1138
+
| Some pairs ->
1098
1139
List.fold_left
1099
1140
(fun acc (_, v) ->
1100
1141
match acc with Error e -> Error e | Ok () -> validate_data_model v)
1101
1142
(Ok ()) pairs
1102
-
| _ -> Error Top_level_not_object
1143
+
| None -> Error Top_level_not_object
1103
1144
1104
1145
let test_data_model_valid () =
1105
1146
let fixtures = read_fixture_json "data-model/data-model-valid.json" in
1106
-
match fixtures with
1107
-
| `List items ->
1147
+
match Atproto_json.to_array_opt fixtures with
1148
+
| Some items ->
1108
1149
List.iter
1109
1150
(fun item ->
1110
-
match item with
1111
-
| `Assoc pairs -> (
1151
+
match Atproto_json.to_object_opt item with
1152
+
| Some pairs -> (
1112
1153
let note =
1113
-
match List.assoc_opt "note" pairs with
1114
-
| Some (`String s) -> s
1115
-
| _ -> "unknown"
1154
+
match Atproto_json.get_string_opt "note" pairs with
1155
+
| Some s -> s
1156
+
| None -> "unknown"
1116
1157
in
1117
-
match List.assoc_opt "json" pairs with
1158
+
match Atproto_json.get "json" pairs with
1118
1159
| Some json ->
1119
1160
let result = validate_top_level json in
1120
1161
Alcotest.(check bool)
1121
1162
(Printf.sprintf "valid: %s" note)
1122
1163
true (Result.is_ok result)
1123
1164
| None -> ())
1124
-
| _ -> ())
1165
+
| None -> ())
1125
1166
items
1126
-
| _ -> Alcotest.fail "Expected JSON array"
1167
+
| None -> Alcotest.fail "Expected JSON array"
1127
1168
1128
1169
let test_data_model_invalid () =
1129
1170
let fixtures = read_fixture_json "data-model/data-model-invalid.json" in
1130
-
match fixtures with
1131
-
| `List items ->
1171
+
match Atproto_json.to_array_opt fixtures with
1172
+
| Some items ->
1132
1173
List.iter
1133
1174
(fun item ->
1134
-
match item with
1135
-
| `Assoc pairs -> (
1175
+
match Atproto_json.to_object_opt item with
1176
+
| Some pairs -> (
1136
1177
let note =
1137
-
match List.assoc_opt "note" pairs with
1138
-
| Some (`String s) -> s
1139
-
| _ -> "unknown"
1178
+
match Atproto_json.get_string_opt "note" pairs with
1179
+
| Some s -> s
1180
+
| None -> "unknown"
1140
1181
in
1141
-
match List.assoc_opt "json" pairs with
1182
+
match Atproto_json.get "json" pairs with
1142
1183
| Some json ->
1143
1184
let result = validate_top_level json in
1144
1185
Alcotest.(check bool)
1145
1186
(Printf.sprintf "invalid: %s" note)
1146
1187
true (Result.is_error result)
1147
1188
| None -> ())
1148
-
| _ -> ())
1189
+
| None -> ())
1149
1190
items
1150
-
| _ -> Alcotest.fail "Expected JSON array"
1191
+
| None -> Alcotest.fail "Expected JSON array"
1151
1192
1152
1193
let data_model_tests =
1153
1194
[
+1
-1
test/lexicon/dune
+1
-1
test/lexicon/dune
+37
-37
test/mst/debug_mst.ml
+37
-37
test/mst/debug_mst.ml
···
2
2
open Atproto_mst
3
3
open Atproto_ipld
4
4
5
-
let leaf_value = match Cid.of_string "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454" with
5
+
let leaf_value =
6
+
match
7
+
Cid.of_string "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454"
8
+
with
6
9
| Ok cid -> cid
7
10
| Error _ -> failwith "Invalid CID"
8
11
9
-
let keys = [
10
-
"A0/374913";
11
-
"B1/986427";
12
-
"C0/451630";
13
-
"E0/670489";
14
-
"F1/085263";
15
-
"G0/765327";
16
-
]
12
+
let keys =
13
+
[
14
+
"A0/374913"; "B1/986427"; "C0/451630"; "E0/670489"; "F1/085263"; "G0/765327";
15
+
]
17
16
18
17
let () =
19
18
Printf.printf "=== MST Debug ===\n\n";
20
-
19
+
21
20
(* Print key heights *)
22
21
Printf.printf "Key heights:\n";
23
-
List.iter (fun k ->
24
-
Printf.printf " %-15s -> height %d\n" k (key_height k)
25
-
) keys;
26
-
22
+
List.iter
23
+
(fun k -> Printf.printf " %-15s -> height %d\n" k (key_height k))
24
+
keys;
25
+
27
26
let store = Memory_blockstore.create () in
28
-
let module M = Make(Memory_blockstore) in
29
-
27
+
let module M = Make (Memory_blockstore) in
30
28
(* Build MST *)
31
29
let entries = List.map (fun k -> (k, leaf_value)) keys in
32
30
let root = M.of_entries store entries in
33
-
31
+
34
32
Printf.printf "\nRoot CID: %s\n" (Cid.to_string root);
35
-
Printf.printf "Expected: bafyreicraprx2xwnico4tuqir3ozsxpz46qkcpox3obf5bagicqwurghpy\n";
36
-
33
+
Printf.printf
34
+
"Expected: bafyreicraprx2xwnico4tuqir3ozsxpz46qkcpox3obf5bagicqwurghpy\n";
35
+
37
36
(* Dump the encoded node *)
38
37
let blocks = Memory_blockstore.blocks store in
39
38
Printf.printf "\n%d blocks in store:\n" (List.length blocks);
40
-
List.iter (fun (cid, data) ->
41
-
Printf.printf "\nCID: %s\n" (Cid.to_string cid);
42
-
Printf.printf " Raw bytes (%d): " (String.length data);
43
-
String.iter (fun c -> Printf.printf "%02x" (Char.code c)) data;
44
-
Printf.printf "\n";
45
-
match decode_node_raw data with
46
-
| Ok node ->
47
-
Printf.printf " Left: %s\n"
48
-
(match node.l with Some c -> Cid.to_string c | None -> "None");
49
-
Printf.printf " Entries (%d):\n" (List.length node.e);
50
-
List.iter (fun e ->
51
-
Printf.printf " p=%d k=%S v=%s t=%s\n"
52
-
e.p e.k (Cid.to_string e.v)
53
-
(match e.t with Some c -> Cid.to_string c | None -> "None")
54
-
) node.e
55
-
| Error (`Decode_error msg) ->
56
-
Printf.printf " DECODE ERROR: %s\n" msg
57
-
) blocks
39
+
List.iter
40
+
(fun (cid, data) ->
41
+
Printf.printf "\nCID: %s\n" (Cid.to_string cid);
42
+
Printf.printf " Raw bytes (%d): " (String.length data);
43
+
String.iter (fun c -> Printf.printf "%02x" (Char.code c)) data;
44
+
Printf.printf "\n";
45
+
match decode_node_raw data with
46
+
| Ok node ->
47
+
Printf.printf " Left: %s\n"
48
+
(match node.l with Some c -> Cid.to_string c | None -> "None");
49
+
Printf.printf " Entries (%d):\n" (List.length node.e);
50
+
List.iter
51
+
(fun e ->
52
+
Printf.printf " p=%d k=%S v=%s t=%s\n" e.p e.k
53
+
(Cid.to_string e.v)
54
+
(match e.t with Some c -> Cid.to_string c | None -> "None"))
55
+
node.e
56
+
| Error (`Decode_error msg) -> Printf.printf " DECODE ERROR: %s\n" msg)
57
+
blocks
+1
-1
test/mst/dune
+1
-1
test/mst/dune
+20
-38
test/mst/test_mst.ml
+20
-38
test/mst/test_mst.ml
···
12
12
let ic = open_in path in
13
13
let content = really_input_string ic (in_channel_length ic) in
14
14
close_in ic;
15
-
Yojson.Basic.from_string content
15
+
match Atproto_json.decode content with
16
+
| Ok json -> json
17
+
| Error e -> failwith ("JSON parse error: " ^ e)
16
18
17
19
(* === Key height tests === *)
18
20
19
21
let test_key_heights () =
20
22
let fixtures = read_fixture_json "key_heights.json" in
21
-
match fixtures with
22
-
| `List items ->
23
+
match Atproto_json.to_array_opt fixtures with
24
+
| Some items ->
23
25
List.iter
24
26
(fun item ->
25
-
match item with
26
-
| `Assoc pairs -> (
27
-
let key =
28
-
match List.assoc_opt "key" pairs with
29
-
| Some (`String s) -> Some s
30
-
| _ -> None
31
-
in
32
-
let expected_height =
33
-
match List.assoc_opt "height" pairs with
34
-
| Some (`Int h) -> Some h
35
-
| _ -> None
36
-
in
27
+
match Atproto_json.to_object_opt item with
28
+
| Some pairs -> (
29
+
let key = Atproto_json.get_string_opt "key" pairs in
30
+
let expected_height = Atproto_json.get_int_opt "height" pairs in
37
31
match (key, expected_height) with
38
32
| Some k, Some expected ->
39
33
let actual = key_height k in
···
41
35
(Printf.sprintf "height of %S" k)
42
36
expected actual
43
37
| _ -> ())
44
-
| _ -> ())
38
+
| None -> ())
45
39
items
46
-
| _ -> Alcotest.fail "Expected JSON array"
40
+
| None -> Alcotest.fail "Expected JSON array"
47
41
48
42
(* === Common prefix tests === *)
49
43
50
44
let test_common_prefix () =
51
45
let fixtures = read_fixture_json "common_prefix.json" in
52
-
match fixtures with
53
-
| `List items ->
46
+
match Atproto_json.to_array_opt fixtures with
47
+
| Some items ->
54
48
List.iter
55
49
(fun item ->
56
-
match item with
57
-
| `Assoc pairs -> (
58
-
let left =
59
-
match List.assoc_opt "left" pairs with
60
-
| Some (`String s) -> Some s
61
-
| _ -> None
62
-
in
63
-
let right =
64
-
match List.assoc_opt "right" pairs with
65
-
| Some (`String s) -> Some s
66
-
| _ -> None
67
-
in
68
-
let expected_len =
69
-
match List.assoc_opt "len" pairs with
70
-
| Some (`Int n) -> Some n
71
-
| _ -> None
72
-
in
50
+
match Atproto_json.to_object_opt item with
51
+
| Some pairs -> (
52
+
let left = Atproto_json.get_string_opt "left" pairs in
53
+
let right = Atproto_json.get_string_opt "right" pairs in
54
+
let expected_len = Atproto_json.get_int_opt "len" pairs in
73
55
match (left, right, expected_len) with
74
56
| Some l, Some r, Some expected ->
75
57
let actual = common_prefix_len l r in
···
77
59
(Printf.sprintf "prefix(%S, %S)" l r)
78
60
expected actual
79
61
| _ -> ())
80
-
| _ -> ())
62
+
| None -> ())
81
63
items
82
-
| _ -> Alcotest.fail "Expected JSON array"
64
+
| None -> Alcotest.fail "Expected JSON array"
83
65
84
66
(* === MST operations tests === *)
85
67
+8
-1
test/repo/dune
+8
-1
test/repo/dune
···
1
1
(test
2
2
(name test_repo)
3
3
(package atproto-repo)
4
-
(libraries atproto-repo atproto-crypto atproto-ipld atproto-mst atproto-syntax mirage-crypto-rng.unix alcotest))
4
+
(libraries
5
+
atproto-repo
6
+
atproto-crypto
7
+
atproto-ipld
8
+
atproto-mst
9
+
atproto-syntax
10
+
mirage-crypto-rng.unix
11
+
alcotest))
+1
-1
test/sync/dune
+1
-1
test/sync/dune
+21
-10
test/sync/test_sync.ml
+21
-10
test/sync/test_sync.ml
···
635
635
content)
636
636
else try_paths rest
637
637
in
638
-
match Yojson.Safe.from_string (try_paths paths) with
639
-
| `List fixtures -> fixtures
640
-
| _ -> failwith "Expected array of fixtures"
638
+
match Atproto_json.decode (try_paths paths) with
639
+
| Ok json -> (
640
+
match Atproto_json.to_array_opt json with
641
+
| Some fixtures -> fixtures
642
+
| None -> failwith "Expected array of fixtures")
643
+
| Error e -> failwith ("JSON parse error: " ^ e)
641
644
642
645
(** Extract string from JSON *)
643
-
let json_string = function `String s -> s | _ -> failwith "Expected string"
646
+
let json_string json =
647
+
match Atproto_json.to_string_opt json with
648
+
| Some s -> s
649
+
| None -> failwith "Expected string"
644
650
645
651
(** Extract string list from JSON *)
646
-
let json_string_list = function
647
-
| `List items -> List.map json_string items
648
-
| _ -> failwith "Expected array of strings"
652
+
let json_string_list json =
653
+
match Atproto_json.to_array_opt json with
654
+
| Some items -> List.map json_string items
655
+
| None -> failwith "Expected array of strings"
649
656
650
657
(** Get field from JSON object *)
651
-
let json_field name = function
652
-
| `Assoc pairs -> List.assoc name pairs
653
-
| _ -> failwith ("Expected object with field " ^ name)
658
+
let json_field name json =
659
+
match Atproto_json.to_object_opt json with
660
+
| Some pairs -> (
661
+
match Atproto_json.get name pairs with
662
+
| Some v -> v
663
+
| None -> failwith ("Expected object with field " ^ name))
664
+
| None -> failwith ("Expected object with field " ^ name)
654
665
655
666
module Mst = Atproto_mst
656
667
+6
-1
test/xrpc/dune
+6
-1
test/xrpc/dune
+86
-40
test/xrpc/test_xrpc.ml
+86
-40
test/xrpc/test_xrpc.ml
···
106
106
Nsid.of_string "com.atproto.server.createSession" |> Result.get_ok
107
107
in
108
108
let input =
109
-
`Assoc
109
+
Simdjsont.Json.Object
110
110
[
111
-
("identifier", `String "test@example.com");
112
-
("password", `String "password");
111
+
("identifier", Simdjsont.Json.String "test@example.com");
112
+
("password", Simdjsont.Json.String "password");
113
113
]
114
114
in
115
115
let _ = Client.procedure client ~nsid ~input () in
···
164
164
match Client.query client ~nsid () with
165
165
| Ok json -> (
166
166
match json with
167
-
| `Assoc pairs ->
167
+
| Simdjsont.Json.Object pairs ->
168
168
Alcotest.(check bool) "has did" true (List.mem_assoc "did" pairs)
169
169
| _ -> Alcotest.fail "expected object")
170
170
| Error e -> Alcotest.fail (Client.error_to_string e))
···
196
196
197
197
let test_server_query_endpoint () =
198
198
let nsid = Nsid.of_string "com.example.test" |> Result.get_ok in
199
-
let handler _ctx = Ok (`Assoc [ ("result", `String "success") ]) in
199
+
let handler _ctx =
200
+
Ok (Simdjsont.Json.Object [ ("result", Simdjsont.Json.String "success") ])
201
+
in
200
202
let server = Server.create () |> Server.query ~nsid ~handler in
201
203
let request = server_request "/xrpc/com.example.test" in
202
204
let response = Server.handle server request in
203
205
Alcotest.(check int) "status" 200 response.status;
204
-
let body = Yojson.Basic.from_string response.body in
206
+
let body =
207
+
match Simdjsont.decode Simdjsont.Codec.value response.body with
208
+
| Ok json -> json
209
+
| Error msg -> Alcotest.fail msg
210
+
in
211
+
205
212
match body with
206
-
| `Assoc pairs ->
213
+
| Simdjsont.Json.Object pairs ->
207
214
Alcotest.(check (option string))
208
215
"result" (Some "success")
209
216
(match List.assoc_opt "result" pairs with
210
-
| Some (`String s) -> Some s
217
+
| Some (Simdjsont.Json.String s) -> Some s
211
218
| _ -> None)
212
219
| _ -> Alcotest.fail "expected object"
213
220
···
215
222
let nsid = Nsid.of_string "com.example.createThing" |> Result.get_ok in
216
223
let handler ctx =
217
224
match Server.require_input_string ctx "name" with
218
-
| Ok name -> Ok (`Assoc [ ("created", `String name) ])
225
+
| Ok name ->
226
+
Ok (Simdjsont.Json.Object [ ("created", Simdjsont.Json.String name) ])
219
227
| Error e -> Error e
220
228
in
221
229
let server = Server.create () |> Server.procedure ~nsid ~handler in
···
225
233
in
226
234
let response = Server.handle server request in
227
235
Alcotest.(check int) "status" 200 response.status;
228
-
let body = Yojson.Basic.from_string response.body in
236
+
let body =
237
+
match Simdjsont.decode Simdjsont.Codec.value response.body with
238
+
| Ok json -> json
239
+
| Error msg -> Alcotest.fail msg
240
+
in
241
+
229
242
match body with
230
-
| `Assoc pairs ->
243
+
| Simdjsont.Json.Object pairs ->
231
244
Alcotest.(check (option string))
232
245
"created" (Some "test")
233
246
(match List.assoc_opt "created" pairs with
234
-
| Some (`String s) -> Some s
247
+
| Some (Simdjsont.Json.String s) -> Some s
235
248
| _ -> None)
236
249
| _ -> Alcotest.fail "expected object"
237
250
···
243
256
244
257
let test_server_method_not_allowed () =
245
258
let nsid = Nsid.of_string "com.example.query" |> Result.get_ok in
246
-
let handler _ctx = Ok (`Assoc []) in
259
+
let handler _ctx = Ok (Simdjsont.Json.Object []) in
247
260
let server = Server.create () |> Server.query ~nsid ~handler in
248
261
(* Try POST on a query endpoint *)
249
262
let request = server_request ~meth:`POST "/xrpc/com.example.query" in
···
260
273
let nsid = Nsid.of_string "com.example.getUser" |> Result.get_ok in
261
274
let handler ctx =
262
275
match Server.require_param ctx "id" with
263
-
| Ok id -> Ok (`Assoc [ ("userId", `String id) ])
276
+
| Ok id ->
277
+
Ok (Simdjsont.Json.Object [ ("userId", Simdjsont.Json.String id) ])
264
278
| Error e -> Error e
265
279
in
266
280
let server = Server.create () |> Server.query ~nsid ~handler in
267
281
let request = server_request "/xrpc/com.example.getUser?id=12345" in
268
282
let response = Server.handle server request in
269
283
Alcotest.(check int) "status" 200 response.status;
270
-
let body = Yojson.Basic.from_string response.body in
284
+
let body =
285
+
match Simdjsont.decode Simdjsont.Codec.value response.body with
286
+
| Ok json -> json
287
+
| Error msg -> Alcotest.fail msg
288
+
in
289
+
271
290
match body with
272
-
| `Assoc pairs ->
291
+
| Simdjsont.Json.Object pairs ->
273
292
Alcotest.(check (option string))
274
293
"userId" (Some "12345")
275
294
(match List.assoc_opt "userId" pairs with
276
-
| Some (`String s) -> Some s
295
+
| Some (Simdjsont.Json.String s) -> Some s
277
296
| _ -> None)
278
297
| _ -> Alcotest.fail "expected object"
279
298
···
281
300
let nsid = Nsid.of_string "com.example.getUser" |> Result.get_ok in
282
301
let handler ctx =
283
302
match Server.require_param ctx "id" with
284
-
| Ok id -> Ok (`Assoc [ ("userId", `String id) ])
303
+
| Ok id ->
304
+
Ok (Simdjsont.Json.Object [ ("userId", Simdjsont.Json.String id) ])
285
305
| Error e -> Error e
286
306
in
287
307
let server = Server.create () |> Server.query ~nsid ~handler in
···
291
311
292
312
let test_server_auth_required () =
293
313
let nsid = Nsid.of_string "com.example.private" |> Result.get_ok in
294
-
let handler _ctx = Ok (`Assoc [ ("secret", `String "data") ]) in
314
+
let handler _ctx =
315
+
Ok (Simdjsont.Json.Object [ ("secret", Simdjsont.Json.String "data") ])
316
+
in
295
317
let server =
296
318
Server.create () |> Server.query ~require_auth:true ~nsid ~handler
297
319
in
···
304
326
let nsid = Nsid.of_string "com.example.private" |> Result.get_ok in
305
327
let handler ctx =
306
328
match ctx.Server.auth with
307
-
| Some auth -> Ok (`Assoc [ ("did", `String auth.did) ])
329
+
| Some auth ->
330
+
Ok (Simdjsont.Json.Object [ ("did", Simdjsont.Json.String auth.did) ])
308
331
| None -> Error (Server.auth_required ())
309
332
in
310
333
let auth_handler (request : Server.request) =
···
332
355
let request = server_request "/xrpc/com.example.fail" in
333
356
let response = Server.handle server request in
334
357
Alcotest.(check int) "status" 400 response.status;
335
-
let body = Yojson.Basic.from_string response.body in
358
+
let body =
359
+
match Simdjsont.decode Simdjsont.Codec.value response.body with
360
+
| Ok json -> json
361
+
| Error msg -> Alcotest.fail msg
362
+
in
363
+
336
364
match body with
337
-
| `Assoc pairs ->
365
+
| Simdjsont.Json.Object pairs ->
338
366
Alcotest.(check (option string))
339
367
"error" (Some "InvalidRequest")
340
368
(match List.assoc_opt "error" pairs with
341
-
| Some (`String s) -> Some s
369
+
| Some (Simdjsont.Json.String s) -> Some s
342
370
| _ -> None)
343
371
| _ -> Alcotest.fail "expected object"
344
372
···
361
389
(Server.extract_bearer_token headers4)
362
390
363
391
let test_json_response () =
364
-
match Server.json_response (`Assoc [ ("ok", `Bool true) ]) with
392
+
match
393
+
Server.json_response
394
+
(Simdjsont.Json.Object [ ("ok", Simdjsont.Json.Bool true) ])
395
+
with
365
396
| Ok json -> (
366
397
match json with
367
-
| `Assoc pairs ->
398
+
| Simdjsont.Json.Object pairs ->
368
399
Alcotest.(check bool) "has ok" true (List.mem_assoc "ok" pairs)
369
400
| _ -> Alcotest.fail "expected object")
370
401
| Error _ -> Alcotest.fail "expected Ok"
···
452
483
453
484
let test_parse_authorization_server () =
454
485
let json =
455
-
`Assoc
486
+
Simdjsont.Json.Object
456
487
[
457
-
("issuer", `String "https://auth.example.com");
458
-
("authorization_endpoint", `String "https://auth.example.com/authorize");
459
-
("token_endpoint", `String "https://auth.example.com/token");
460
-
("dpop_signing_alg_values_supported", `List [ `String "ES256" ]);
488
+
("issuer", Simdjsont.Json.String "https://auth.example.com");
489
+
( "authorization_endpoint",
490
+
Simdjsont.Json.String "https://auth.example.com/authorize" );
491
+
( "token_endpoint",
492
+
Simdjsont.Json.String "https://auth.example.com/token" );
493
+
( "dpop_signing_alg_values_supported",
494
+
Simdjsont.Json.Array [ Simdjsont.Json.String "ES256" ] );
461
495
( "scopes_supported",
462
-
`List [ `String "atproto"; `String "transition:generic" ] );
496
+
Simdjsont.Json.Array
497
+
[
498
+
Simdjsont.Json.String "atproto";
499
+
Simdjsont.Json.String "transition:generic";
500
+
] );
463
501
]
464
502
in
465
503
match OAuth.parse_authorization_server json with
···
471
509
| Error e -> Alcotest.fail (OAuth.error_to_string e)
472
510
473
511
let test_parse_authorization_server_missing_fields () =
474
-
let json = `Assoc [ ("issuer", `String "https://auth.example.com") ] in
512
+
let json =
513
+
Simdjsont.Json.Object
514
+
[ ("issuer", Simdjsont.Json.String "https://auth.example.com") ]
515
+
in
475
516
match OAuth.parse_authorization_server json with
476
517
| Error (OAuth.Invalid_response _) -> ()
477
518
| Error e -> Alcotest.fail ("Wrong error: " ^ OAuth.error_to_string e)
···
528
569
529
570
let test_parse_tokens () =
530
571
let json =
531
-
`Assoc
572
+
Simdjsont.Json.Object
532
573
[
533
-
("access_token", `String "eyJhbGciOiJFUzI1NiJ9...");
534
-
("refresh_token", `String "dGVzdF9yZWZyZXNo");
535
-
("token_type", `String "DPoP");
536
-
("expires_in", `Int 3600);
537
-
("scope", `String "atproto transition:generic");
574
+
("access_token", Simdjsont.Json.String "eyJhbGciOiJFUzI1NiJ9...");
575
+
("refresh_token", Simdjsont.Json.String "dGVzdF9yZWZyZXNo");
576
+
("token_type", Simdjsont.Json.String "DPoP");
577
+
("expires_in", Simdjsont.Json.Int 3600L);
578
+
("scope", Simdjsont.Json.String "atproto transition:generic");
538
579
]
539
580
in
540
581
match OAuth.parse_tokens json with
···
549
590
| Error e -> Alcotest.fail (OAuth.error_to_string e)
550
591
551
592
let test_parse_tokens_minimal () =
552
-
let json = `Assoc [ ("access_token", `String "test_token") ] in
593
+
let json =
594
+
Simdjsont.Json.Object
595
+
[ ("access_token", Simdjsont.Json.String "test_token") ]
596
+
in
553
597
match OAuth.parse_tokens json with
554
598
| Ok tokens ->
555
599
Alcotest.(check string) "access_token" "test_token" tokens.access_token;
···
558
602
| Error e -> Alcotest.fail (OAuth.error_to_string e)
559
603
560
604
let test_parse_tokens_missing_access () =
561
-
let json = `Assoc [ ("refresh_token", `String "test") ] in
605
+
let json =
606
+
Simdjsont.Json.Object [ ("refresh_token", Simdjsont.Json.String "test") ]
607
+
in
562
608
match OAuth.parse_tokens json with
563
609
| Error (OAuth.Invalid_response _) -> ()
564
610
| Error e -> Alcotest.fail ("Wrong error: " ^ OAuth.error_to_string e)