atproto libraries implementation in ocaml

Compare changes

Choose any two refs to compare.

Changed files
+19753 -2181
.beads
bin
examples
bsky_bot
feed_generator
identity_tool
repo_inspector
lexicons
app
bsky
actor
ageassurance
bookmark
contact
embed
feed
graph
labeler
notification
richtext
unspecced
video
chat
com
atproto
admin
identity
label
lexicon
moderation
repo
server
sync
temp
tools
lib
test
+1 -1
.beads/.local_version
··· 1 - 0.29.0 1 + 0.43.0
+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
··· 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 # AT Protocol Compliance Report 2 2 3 - **Generated:** 2025-12-28T15:06:07Z 3 + **Generated:** 2026-01-01T22:04:38Z 4 4 **Repository:** [https://github.com/gdiazlo/atproto](https://github.com/gdiazlo/atproto) 5 5 6 6 ## Summary
+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
··· 294 294 295 295 ## License 296 296 297 - MIT License - see [LICENSE](LICENSE) file. 297 + ISC License - see [LICENSE](LICENSE) file. 298 298 299 299 ## Contributing 300 300
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 1 (executable 2 - (public_name atproto) 3 - (package atproto) 4 2 (name main) 5 3 (libraries atproto))
+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
··· 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
··· 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
··· 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 1 (executable 2 2 (name bsky_bot) 3 - (public_name bsky_bot) 4 - (package atproto) 5 3 (libraries 6 4 atproto-api 7 5 atproto-xrpc 8 6 atproto-effects 9 7 climate 10 8 eio_main 11 - cohttp-eio 12 - tls-eio 9 + hcs 13 10 mirage-crypto-rng.unix 14 11 uri))
+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
··· 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 1 (executable 2 2 (name identity_tool) 3 - (public_name identity_tool) 4 - (package atproto) 5 3 (libraries 6 4 atproto-identity 7 5 atproto-syntax 8 6 climate 9 7 eio_main 10 - cohttp-eio 11 - tls-eio 8 + hcs 12 9 mirage-crypto-rng.unix 13 10 uri))
+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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "chat.bsky.actor.deleteAccount", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "output": { 8 + "encoding": "application/json", 9 + "schema": { 10 + "type": "object", 11 + "properties": {} 12 + } 13 + } 14 + } 15 + } 16 + }
+12
lexicons/chat/bsky/actor/exportAccountData.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "chat.bsky.actor.exportAccountData", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "output": { 8 + "encoding": "application/jsonl" 9 + } 10 + } 11 + } 12 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.identity.requestPlcOperationSignature", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Request an email with a code to in order to request a signed PLC operation. Requires Auth." 8 + } 9 + } 10 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.defs", 4 + "defs": { 5 + "commitMeta": { 6 + "type": "object", 7 + "required": ["cid", "rev"], 8 + "properties": { 9 + "cid": { "type": "string", "format": "cid" }, 10 + "rev": { "type": "string", "format": "tid" } 11 + } 12 + } 13 + } 14 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.server.requestAccountDelete", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Initiate a user account deletion via email." 8 + } 9 + } 10 + }
+10
lexicons/com/atproto/server/requestEmailConfirmation.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.server.requestEmailConfirmation", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Request an email with a code to confirm ownership of email." 8 + } 9 + } 10 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.sync.defs", 4 + "defs": { 5 + "hostStatus": { 6 + "type": "string", 7 + "knownValues": ["active", "idle", "offline", "throttled", "banned"] 8 + } 9 + } 10 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.ozone.signature.defs", 4 + "defs": { 5 + "sigDetail": { 6 + "type": "object", 7 + "required": ["property", "value"], 8 + "properties": { 9 + "property": { "type": "string" }, 10 + "value": { "type": "string" } 11 + } 12 + } 13 + } 14 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 1 (library 2 2 (name atproto_api) 3 3 (public_name atproto-api) 4 - (libraries atproto_syntax atproto_xrpc atproto_identity atproto_ipld yojson uri unix)) 4 + (libraries 5 + atproto_syntax 6 + atproto_xrpc 7 + atproto_identity 8 + atproto_ipld 9 + atproto_json 10 + uri 11 + unix))
+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
··· 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
··· 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
··· 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 (library 2 2 (name atproto_identity) 3 3 (public_name atproto-identity) 4 - (libraries atproto_effects atproto_syntax atproto_crypto yojson uri)) 4 + (libraries atproto_effects atproto_syntax atproto_crypto atproto_json uri))
+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
··· 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 (library 2 2 (name atproto_ipld) 3 3 (public_name atproto-ipld) 4 - (libraries atproto_multibase digestif zarith cbor base64 yojson)) 4 + (libraries atproto_multibase atproto_json digestif zarith cbor base64))
+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
··· 1 + (library 2 + (name atproto_json) 3 + (public_name atproto-json) 4 + (libraries simdjsont) 5 + (inline_tests) 6 + (preprocess 7 + (pps ppx_inline_test)))
+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
··· 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 (library 2 2 (name atproto_lexicon) 3 3 (public_name atproto-lexicon) 4 - (libraries atproto_syntax simdjsont) 4 + (libraries atproto_syntax atproto_json) 5 5 (preprocess no_preprocessing))
+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
··· 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 (library 2 2 (name atproto_sync) 3 3 (public_name atproto-sync) 4 - (libraries atproto_effects atproto_syntax atproto_ipld uri)) 4 + (libraries atproto_effects atproto_syntax atproto_ipld atproto_json uri))
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 2 2 (name test_crypto) 3 3 (package atproto-crypto) 4 - (libraries atproto_crypto alcotest yojson mirage-crypto-rng.unix) 4 + (libraries atproto_crypto atproto_json alcotest mirage-crypto-rng.unix) 5 5 (deps 6 6 (source_tree ../fixtures/crypto)) 7 7 (preprocess no_preprocessing))
+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
··· 4 4 (deps 5 5 (source_tree ../fixtures/syntax) 6 6 (source_tree ../fixtures/data-model)) 7 - (libraries atproto_ipld alcotest yojson base64)) 7 + (libraries atproto_ipld alcotest atproto_json base64))
+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
··· 3 3 (package atproto-lexicon) 4 4 (deps 5 5 (source_tree ../fixtures/lexicon)) 6 - (libraries atproto-lexicon yojson alcotest)) 6 + (libraries atproto-lexicon alcotest))
+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
··· 3 3 (package atproto-mst) 4 4 (deps 5 5 (source_tree ../fixtures/mst)) 6 - (libraries atproto_mst atproto_ipld alcotest yojson)) 6 + (libraries atproto_mst atproto_ipld atproto_json alcotest)) 7 7 8 8 (executable 9 9 (name debug_mst)
+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
··· 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 2 2 (name test_sync) 3 - (libraries atproto-sync atproto-ipld atproto-mst yojson alcotest)) 3 + (libraries atproto-sync atproto-ipld atproto-mst atproto-json alcotest))
+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
··· 1 1 (test 2 2 (name test_xrpc) 3 - (libraries atproto_xrpc atproto_syntax alcotest mirage-crypto-rng.unix)) 3 + (libraries 4 + atproto_xrpc 5 + atproto_syntax 6 + atproto_json 7 + alcotest 8 + mirage-crypto-rng.unix))
+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)