atproto libraries implementation in ocaml

Compare changes

Choose any two refs to compare.

+1 -1
.beads/.local_version
··· 1 - 0.29.0 1 + 0.43.0
+38 -38
.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":"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"}]} 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"}]} 35 - {"id":"atproto-8pf","title":"Migrate lib/api JSON parsing from yojson to simdjson","description":"","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","description":"","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"} 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"} 37 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"]} 38 - {"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"} 39 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"]} 40 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"]} 41 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"]} 42 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"]} 43 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"]} 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"}]} 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 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"} 46 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"]} 47 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"]} 48 - {"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"} 49 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"]} 50 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"]} 51 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"]} 52 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"]} 53 - {"id":"atproto-w30","title":"Migrate test fixture loading from Yojson to Atproto_json","description":"","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"}]} 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":"{}"}]} 55 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"]} 56 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
+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
+3 -2
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" ··· 12 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} ··· 36 37 "@doc" {with-doc} 37 38 ] 38 39 ] 39 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 40 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 40 41 x-maintenance-intent: ["(latest)"]
+4 -2
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" ··· 12 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"} ··· 35 37 "@doc" {with-doc} 36 38 ] 37 39 ] 38 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 40 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 39 41 x-maintenance-intent: ["(latest)"]
+3 -2
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." ··· 12 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: "https://tangled.org/gdiazlo.tngl.sh/atproto" 36 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 36 37 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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} ··· 35 36 "@doc" {with-doc} 36 37 ] 37 38 ] 38 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 39 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 39 40 x-maintenance-intent: ["(latest)"]
+4 -2
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" ··· 12 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"} ··· 35 37 "@doc" {with-doc} 36 38 ] 37 39 ] 38 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 40 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 39 41 x-maintenance-intent: ["(latest)"]
+3 -2
atproto-json.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: "JSON utilities for AT Protocol" 4 5 description: 5 6 "JSON wrapper used across AT Protocol packages (currently backed by simdjsont)" ··· 12 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 "simdjsont" {>= "0.1.0"} 17 18 "alcotest" {with-test} 18 19 "odoc" {with-doc} ··· 31 32 "@doc" {with-doc} 32 33 ] 33 34 ] 34 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 35 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 35 36 x-maintenance-intent: ["(latest)"]
+3 -2
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 6 maintainer: ["Gabriel Dรญaz"] ··· 11 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 17 "atproto-json" {= version} 17 18 "alcotest" {with-test} ··· 31 32 "@doc" {with-doc} 32 33 ] 33 34 ] 34 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 35 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 35 36 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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} ··· 32 33 "@doc" {with-doc} 33 34 ] 34 35 ] 35 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 36 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 36 37 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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: "https://tangled.org/gdiazlo.tngl.sh/atproto" 34 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 34 35 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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} ··· 35 36 "@doc" {with-doc} 36 37 ] 37 38 ] 38 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 39 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 39 40 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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: "https://tangled.org/gdiazlo.tngl.sh/atproto" 38 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 38 39 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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: "https://tangled.org/gdiazlo.tngl.sh/atproto" 35 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 35 36 x-maintenance-intent: ["(latest)"]
+3 -2
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" ··· 12 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} ··· 35 36 "@doc" {with-doc} 36 37 ] 37 38 ] 38 - dev-repo: "https://tangled.org/gdiazlo.tngl.sh/atproto" 39 + dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/atproto" 39 40 x-maintenance-intent: ["(latest)"]
+13 -3
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" ··· 12 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: "https://tangled.org/gdiazlo.tngl.sh/atproto" 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))
+31 -18
dune-project
··· 1 1 (lang dune 3.20) 2 2 3 - (name hcs) 3 + (name atproto) 4 + 5 + (version 0.1.2) 4 6 5 7 (generate_opam_files true) 6 8 7 9 (source 8 - ; (tangled @gdiazlo.tngl.sh/hcs) 9 - (uri https://tangled.org/gdiazlo.tngl.sh/atproto)) 10 + ; (tangled @gdiazlo.tngl.sh/atproto) 11 + (uri git+https://tangled.org/gdiazlo.tngl.sh/atproto)) 10 12 11 13 (authors "Gabriel Dรญaz") 12 14 ··· 23 25 (synopsis "Base encoding utilities for AT Protocol") 24 26 (description "Multibase encoding/decoding including base32-sortable for TIDs and base58btc for did:key") 25 27 (depends 26 - (ocaml (>= 5.1)) 28 + (ocaml (>= 5.4)) 27 29 (alcotest :with-test)) 28 30 (tags (atproto encoding multibase base32 base58))) 29 31 ··· 32 34 (synopsis "Syntax validation for AT Protocol identifiers") 33 35 (description "Parser-based validation for handles, DIDs, NSIDs, TIDs, AT-URIs, and other AT Protocol syntax") 34 36 (depends 35 - (ocaml (>= 5.1)) 37 + (ocaml (>= 5.4)) 36 38 (atproto-multibase (= :version)) 37 39 (alcotest :with-test)) 38 40 (tags (atproto syntax parser validation))) ··· 42 44 (synopsis "Cryptographic operations for AT Protocol") 43 45 (description "P-256 and K-256 elliptic curve support with low-S normalization, did:key encoding") 44 46 (depends 45 - (ocaml (>= 5.1)) 47 + (ocaml (>= 5.4)) 46 48 (atproto-multibase (= :version)) 49 + (atproto-json (= :version)) 47 50 (mirage-crypto-ec (>= 2.0.2)) 48 51 (mirage-crypto-rng (>= 2.0.2)) 49 52 (digestif (>= 1.0)) ··· 57 60 (synopsis "IPLD support for AT Protocol") 58 61 (description "Content Identifiers (CID) and DAG-CBOR encoding for AT Protocol") 59 62 (depends 60 - (ocaml (>= 5.1)) 63 + (ocaml (>= 5.4)) 61 64 (atproto-multibase (= :version)) 65 + (atproto-json (= :version)) 62 66 (digestif (>= 1.0)) 63 67 (zarith (>= 1.12)) 64 68 (cbor (>= 0.5)) ··· 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 81 (alcotest :with-test)) ··· 82 86 (synopsis "Repository support for AT Protocol") 83 87 (description "Repository structure, commits, and record operations for AT Protocol") 84 88 (depends 85 - (ocaml (>= 5.1)) 89 + (ocaml (>= 5.4)) 86 90 (atproto-syntax (= :version)) 87 91 (atproto-crypto (= :version)) 88 92 (atproto-ipld (= :version)) ··· 96 100 (synopsis "Lexicon schema support for AT Protocol") 97 101 (description "Lexicon schema parsing and validation for AT Protocol") 98 102 (depends 99 - (ocaml (>= 5.1)) 103 + (ocaml (>= 5.4)) 100 104 (atproto-syntax (= :version)) 101 105 (atproto-json (= :version)) 102 106 (alcotest :with-test)) ··· 108 112 (synopsis "JSON utilities for AT Protocol") 109 113 (description "JSON wrapper used across AT Protocol packages (currently backed by simdjsont)") 110 114 (depends 111 - (ocaml (>= 5.1)) 115 + (ocaml (>= 5.4)) 112 116 (simdjsont (>= 0.1.0)) 113 117 (alcotest :with-test)) 114 118 (tags (atproto json simdjson))) ··· 119 123 (synopsis "XRPC client/server for AT Protocol") 120 124 (description "XRPC HTTP API protocol implementation for AT Protocol client-server communication") 121 125 (depends 122 - (ocaml (>= 5.1)) 126 + (ocaml (>= 5.4)) 123 127 (atproto-effects (= :version)) 124 128 (atproto-syntax (= :version)) 125 129 (atproto-lexicon (= :version)) ··· 134 138 (synopsis "DID and Handle resolution for AT Protocol") 135 139 (description "DID and Handle resolution including did:plc, did:web, and DNS/HTTPS handle resolution") 136 140 (depends 137 - (ocaml (>= 5.1)) 141 + (ocaml (>= 5.4)) 138 142 (atproto-effects (= :version)) 139 143 (atproto-syntax (= :version)) 140 144 (atproto-crypto (= :version)) ··· 149 153 (synopsis "Repository sync and event streams for AT Protocol") 150 154 (description "Firehose event stream client and repository synchronization for AT Protocol") 151 155 (depends 152 - (ocaml (>= 5.1)) 156 + (ocaml (>= 5.4)) 153 157 (atproto-effects (= :version)) 154 158 (atproto-syntax (= :version)) 155 159 (atproto-ipld (= :version)) ··· 163 167 (synopsis "High-level API client for AT Protocol") 164 168 (description "User-friendly API client for AT Protocol with session management, posting, and social actions") 165 169 (depends 166 - (ocaml (>= 5.1)) 170 + (ocaml (>= 5.4)) 167 171 (atproto-syntax (= :version)) 168 172 (atproto-xrpc (= :version)) 169 173 (atproto-identity (= :version)) ··· 179 183 (synopsis "Effects-based I/O abstraction for AT Protocol") 180 184 (description "Unified effect types for HTTP, DNS, WebSocket, time, and random operations. Allows libraries to be runtime-agnostic.") 181 185 (depends 182 - (ocaml (>= 5.1)) 186 + (ocaml (>= 5.4)) 183 187 (uri (>= 4.0)) 184 188 (ptime (>= 1.0)) 185 189 (alcotest :with-test)) ··· 191 195 (synopsis "AT Protocol implementation in OCaml") 192 196 (description "Complete AT Protocol implementation including syntax validation, cryptography, IPLD, and identity resolution") 193 197 (depends 194 - (ocaml (>= 5.1)) 198 + (ocaml (>= 5.4)) 199 + (atproto-multibase (= :version)) 195 200 (atproto-syntax (= :version)) 196 201 (atproto-crypto (= :version)) 197 - (atproto-multibase (= :version)) 198 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)) 199 212 (odoc :with-doc)) 200 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))
+35 -243
examples/feed_generator/feed_generator.ml
··· 178 178 | Firehose.Tombstone _ -> List.mem Tombstones filters 179 179 | Firehose.Info _ | Firehose.StreamError _ -> true) 180 180 181 - (** {1 Keyword Filtering} *) 182 - 183 181 let contains_keyword keyword text = 184 182 let kw = String.lowercase_ascii keyword in 185 183 let txt = String.lowercase_ascii text in ··· 216 214 && text_matches_keyword kw blocks op) 217 215 evt.ops 218 216 | _ -> false) 219 - 220 - (** {1 Feed Skeleton} *) 221 217 222 218 let max_feed_size = 1000 223 219 let feed_posts : string Queue.t = Queue.create () ··· 264 260 Printf.printf " ]\n}\n" 265 261 end 266 262 267 - (** {1 WebSocket Client} *) 268 - 269 - module Ws = struct 270 - type conn = { 271 - socket : Tls_eio.t; 272 - buf : bytes; 273 - mutable buf_len : int; 274 - frag_buf : Buffer.t; 275 - mutable frag_opcode : int; 276 - } 277 - 278 - let ws_nonce () = 279 - let b = Bytes.create 16 in 280 - for i = 0 to 15 do 281 - Bytes.set b i (Char.chr (Random.int 256)) 282 - done; 283 - Base64.encode_exn (Bytes.to_string b) 284 - 285 - let parse_frame_header buf off len = 286 - if len < 2 then None 287 - else 288 - let b0, b1 = 289 - (Char.code (Bytes.get buf off), Char.code (Bytes.get buf (off + 1))) 290 - in 291 - let fin, opcode = (b0 land 0x80 <> 0, b0 land 0x0f) in 292 - let masked, plen = (b1 land 0x80 <> 0, b1 land 0x7f) in 293 - let hlen, plen = 294 - if plen = 126 then 295 - if len < 4 then (0, -1) 296 - else 297 - ( 4, 298 - (Char.code (Bytes.get buf (off + 2)) lsl 8) 299 - lor Char.code (Bytes.get buf (off + 3)) ) 300 - else if plen = 127 then 301 - if len < 10 then (0, -1) 302 - else 303 - ( 10, 304 - (Char.code (Bytes.get buf (off + 6)) lsl 24) 305 - lor (Char.code (Bytes.get buf (off + 7)) lsl 16) 306 - lor (Char.code (Bytes.get buf (off + 8)) lsl 8) 307 - lor Char.code (Bytes.get buf (off + 9)) ) 308 - else (2, plen) 309 - in 310 - if plen < 0 then None 311 - else 312 - let mlen = if masked then 4 else 0 in 313 - if len < hlen + mlen then None 314 - else 315 - Some 316 - ( fin, 317 - opcode, 318 - plen, 319 - (if masked then Some (Bytes.sub buf (off + hlen) 4) else None), 320 - hlen + mlen ) 321 - 322 - let make_frame opcode payload = 323 - let plen = String.length payload in 324 - let mask = Bytes.init 4 (fun _ -> Char.chr (Random.int 256)) in 325 - let hdr = 326 - if plen < 126 then ( 327 - let h = Bytes.create 6 in 328 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 329 - Bytes.set h 1 (Char.chr (0x80 lor plen)); 330 - Bytes.blit mask 0 h 2 4; 331 - Bytes.to_string h) 332 - else if plen < 65536 then ( 333 - let h = Bytes.create 8 in 334 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 335 - Bytes.set h 1 (Char.chr (0x80 lor 126)); 336 - Bytes.set h 2 (Char.chr ((plen lsr 8) land 0xff)); 337 - Bytes.set h 3 (Char.chr (plen land 0xff)); 338 - Bytes.blit mask 0 h 4 4; 339 - Bytes.to_string h) 340 - else 341 - let h = Bytes.create 14 in 342 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 343 - Bytes.set h 1 (Char.chr (0x80 lor 127)); 344 - for i = 2 to 5 do 345 - Bytes.set h i '\000' 346 - done; 347 - Bytes.set h 6 (Char.chr ((plen lsr 24) land 0xff)); 348 - Bytes.set h 7 (Char.chr ((plen lsr 16) land 0xff)); 349 - Bytes.set h 8 (Char.chr ((plen lsr 8) land 0xff)); 350 - Bytes.set h 9 (Char.chr (plen land 0xff)); 351 - Bytes.blit mask 0 h 10 4; 352 - Bytes.to_string h 353 - in 354 - let masked = 355 - Bytes.mapi 356 - (fun i c -> 357 - Char.chr (Char.code c lxor Char.code (Bytes.get mask (i mod 4)))) 358 - (Bytes.of_string payload) 359 - in 360 - hdr ^ Bytes.to_string masked 361 - 362 - let connect ~net ~sw uri = 363 - let host = Uri.host uri |> Option.value ~default:"localhost" in 364 - let port = Uri.port uri |> Option.value ~default:443 in 365 - let auth = 366 - match Ca_certs_nss.authenticator () with 367 - | Ok a -> a 368 - | Error (`Msg m) -> failwith m 369 - in 370 - let tls_config = 371 - match 372 - Tls.Config.client ~authenticator:auth ~alpn_protocols:[ "http/1.1" ] () 373 - with 374 - | Ok c -> c 375 - | Error (`Msg m) -> failwith m 376 - in 377 - let hostname = 378 - match Domain_name.of_string host with 379 - | Error _ -> None 380 - | Ok dn -> ( 381 - match Domain_name.host dn with Ok h -> Some h | Error _ -> None) 382 - in 383 - let addr = 384 - match 385 - Eio.Net.getaddrinfo_stream net host ~service:(string_of_int port) 386 - with 387 - | [] -> failwith ("DNS failed: " ^ host) 388 - | a :: _ -> a 389 - in 390 - let socket = Eio.Net.connect ~sw net addr in 391 - let tls_socket = Tls_eio.client_of_flow tls_config ?host:hostname socket in 392 - let nonce = ws_nonce () in 393 - Eio.Flow.copy_string 394 - (Printf.sprintf 395 - "GET %s HTTP/1.1\r\n\ 396 - Host: %s\r\n\ 397 - Upgrade: websocket\r\n\ 398 - Connection: Upgrade\r\n\ 399 - Sec-WebSocket-Key: %s\r\n\ 400 - Sec-WebSocket-Version: 13\r\n\ 401 - \r\n" 402 - (Uri.path_and_query uri) host nonce) 403 - tls_socket; 404 - let resp_buf = Cstruct.create 4096 in 405 - let n = Eio.Flow.single_read tls_socket resp_buf in 406 - let resp = Cstruct.to_string (Cstruct.sub resp_buf 0 n) in 407 - if not (String.length resp >= 12 && String.sub resp 0 12 = "HTTP/1.1 101") 408 - then 409 - failwith 410 - ("WebSocket upgrade failed: " 411 - ^ String.sub resp 0 (min 50 (String.length resp))); 412 - { 413 - socket = tls_socket; 414 - buf = Bytes.create 1048576; 415 - buf_len = 0; 416 - frag_buf = Buffer.create 65536; 417 - frag_opcode = 0; 418 - } 419 - 420 - let read_more conn = 421 - let cs = Cstruct.create 65536 in 422 - let n = Eio.Flow.single_read conn.socket cs in 423 - Cstruct.blit_to_bytes cs 0 conn.buf conn.buf_len n; 424 - conn.buf_len <- conn.buf_len + n 425 - 426 - let recv conn = 427 - let rec read_frame () = 428 - if conn.buf_len < 2 then ( 429 - read_more conn; 430 - read_frame ()) 431 - else 432 - match parse_frame_header conn.buf 0 conn.buf_len with 433 - | None -> 434 - read_more conn; 435 - read_frame () 436 - | Some (fin, opcode, plen, mask, hlen) -> ( 437 - let total = hlen + plen in 438 - while conn.buf_len < total do 439 - read_more conn 440 - done; 441 - let payload = Bytes.sub conn.buf hlen plen in 442 - (match mask with 443 - | Some k -> 444 - for i = 0 to plen - 1 do 445 - Bytes.set payload i 446 - (Char.chr 447 - (Char.code (Bytes.get payload i) 448 - lxor Char.code (Bytes.get k (i mod 4)))) 449 - done 450 - | None -> ()); 451 - let rem = conn.buf_len - total in 452 - if rem > 0 then Bytes.blit conn.buf total conn.buf 0 rem; 453 - conn.buf_len <- rem; 454 - match opcode with 455 - | 0x0 -> 456 - Buffer.add_bytes conn.frag_buf payload; 457 - if fin then ( 458 - let data = Buffer.contents conn.frag_buf in 459 - Buffer.clear conn.frag_buf; 460 - if conn.frag_opcode = 0x2 then Ok data else read_frame ()) 461 - else read_frame () 462 - | 0x1 -> 463 - if not fin then ( 464 - Buffer.clear conn.frag_buf; 465 - Buffer.add_bytes conn.frag_buf payload; 466 - conn.frag_opcode <- 0x1); 467 - read_frame () 468 - | 0x2 -> 469 - if fin then Ok (Bytes.to_string payload) 470 - else ( 471 - Buffer.clear conn.frag_buf; 472 - Buffer.add_bytes conn.frag_buf payload; 473 - conn.frag_opcode <- 0x2; 474 - read_frame ()) 475 - | 0x8 -> Error "Connection closed" 476 - | 0x9 -> 477 - Eio.Flow.copy_string 478 - (make_frame 0xA (Bytes.to_string payload)) 479 - conn.socket; 480 - read_frame () 481 - | _ -> read_frame ()) 482 - in 483 - try read_frame () with exn -> Error (Printexc.to_string exn) 484 - 485 - let close _conn = () 486 - end 487 - 488 - let with_websocket ~net ~sw f = 263 + let with_websocket ~sw ~net f = 489 264 let open Effect.Deep in 490 265 try_with f () 491 266 { ··· 495 270 | Firehose.Ws_connect uri -> 496 271 Some 497 272 (fun (k : (a, _) continuation) -> 498 - try 499 - continue k 500 - (Ok 501 - (Obj.magic (Ws.connect ~net ~sw uri) 502 - : Firehose.websocket)) 503 - 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)) 504 287 | Firehose.Ws_recv ws -> 505 - 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))) 506 307 | Firehose.Ws_close ws -> 507 308 Some 508 309 (fun k -> 509 - Ws.close (Obj.magic ws); 310 + Hcs.Websocket.close (Obj.magic ws); 510 311 continue k ()) 511 312 | _ -> None); 512 313 } 513 314 514 - (** {1 Configuration} *) 515 - 516 315 type config = { 517 316 cursor : int64 option; 518 317 limit : int option; ··· 563 362 Climate.Command.singleton 564 363 ~doc:"AT Protocol firehose client and feed generator" config_parser 565 364 566 - (** {1 Stats} *) 567 - 568 365 type stats = { 569 366 mutable total : int; 570 367 mutable matched : int; ··· 582 379 let stats = { total = 0; matched = 0; last_seq = 0L; start = 0. } 583 380 let interrupted = ref false 584 381 585 - (** {1 Main} *) 586 - 587 382 let run ~net ~sw config = 588 383 let uri = 589 384 Uri.of_string "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos" ··· 601 396 (match Firehose.event_seq event with 602 397 | Some s -> stats.last_seq <- s 603 398 | None -> ()); 604 - (* Collect posts for skeleton if enabled *) 605 399 (if config.skeleton then 606 400 match event with 607 401 | Firehose.Commit evt -> collect_post_uris evt config.keyword 608 402 | _ -> ()); 609 - (* Check filters *) 610 403 let type_match = event_matches config.filters event in 611 404 let keyword_match = event_matches_keyword config.keyword event in 612 405 if type_match && keyword_match then begin ··· 642 435 Sys.set_signal Sys.sigint (Sys.Signal_handle (fun _ -> interrupted := true)); 643 436 stats.start <- Unix.gettimeofday (); 644 437 Mirage_crypto_rng_unix.use_default (); 645 - Random.self_init (); 646 438 (try 647 439 Eio_main.run @@ fun env -> 648 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
+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 atproto_json uri unix)) 4 + (libraries 5 + atproto_syntax 6 + atproto_xrpc 7 + atproto_identity 8 + atproto_ipld 9 + atproto_json 10 + uri 11 + unix))
+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 atproto_json) 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))
+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 atproto_json 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))
+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
+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))
+6 -1
test/xrpc/dune
··· 1 1 (test 2 2 (name test_xrpc) 3 - (libraries atproto_xrpc atproto_syntax atproto_json alcotest mirage-crypto-rng.unix)) 3 + (libraries 4 + atproto_xrpc 5 + atproto_syntax 6 + atproto_json 7 + alcotest 8 + mirage-crypto-rng.unix))