+38
-38
.beads/issues.jsonl
+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
+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
+15
LICENSE
···
1
+
ISC License
2
+
3
+
Copyright (c) 2026 Gabriel Dรญaz Lรณpez de la Llave
4
+
5
+
Permission to use, copy, modify, and/or distribute this software for any
6
+
purpose with or without fee is hereby granted, provided that the above
7
+
copyright notice and this permission notice appear in all copies.
8
+
9
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+
PERFORMANCE OF THIS SOFTWARE.
+1
-1
README.md
+1
-1
README.md
+3
-2
atproto-api.opam
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-2
bin/dune
+31
-18
dune-project
+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
+62
-77
examples/bsky_bot/bsky_bot.ml
···
5
5
module Richtext = Atproto_api.Richtext
6
6
module Client = Atproto_xrpc.Client
7
7
8
-
(** {1 HTTP Client with cohttp-eio} *)
9
-
10
-
let http_request ~sw ~client (req : Client.request) : Client.response =
11
-
let headers = Cohttp.Header.of_list req.headers in
12
-
let body =
13
-
match req.body with
14
-
| Some b -> Cohttp_eio.Body.of_string b
15
-
| None -> Cohttp_eio.Body.of_string ""
8
+
let http_request ~hcs_client (req : Client.request) : Client.response =
9
+
let url = Uri.to_string req.uri in
10
+
let result =
11
+
match req.meth with
12
+
| `GET -> Hcs.Client.request hcs_client url
13
+
| `POST ->
14
+
let body = Option.value ~default:"" req.body in
15
+
Hcs.Client.request_post hcs_client url ~body
16
16
in
17
-
let meth = match req.meth with `GET -> `GET | `POST -> `POST in
18
-
try
19
-
let resp, resp_body =
20
-
Cohttp_eio.Client.call ~sw client meth req.uri ~headers ~body
21
-
in
22
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
23
-
let headers = Cohttp.Response.headers resp |> Cohttp.Header.to_list in
24
-
let body =
25
-
Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all)
26
-
in
27
-
{ Client.status; headers; body }
28
-
with e -> { Client.status = 0; headers = []; body = Printexc.to_string e }
29
-
30
-
(** {1 Effect Handler} *)
17
+
match result with
18
+
| Ok resp ->
19
+
{ Client.status = resp.status; headers = resp.headers; body = resp.body }
20
+
| Error e ->
21
+
let msg =
22
+
match e with
23
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
24
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
25
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
26
+
| Hcs.Client.Timeout -> "Timeout"
27
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
28
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
29
+
in
30
+
{ Client.status = 0; headers = []; body = msg }
31
31
32
-
let run_with_eio ~sw ~client f =
32
+
let run_with_hcs ~hcs_client f =
33
33
Effect.Deep.try_with f ()
34
34
{
35
35
effc =
···
38
38
| Client.Http_request req ->
39
39
Some
40
40
(fun (k : (a, _) Effect.Deep.continuation) ->
41
-
Effect.Deep.continue k (http_request ~sw ~client req))
41
+
Effect.Deep.continue k (http_request ~hcs_client req))
42
42
| _ -> None);
43
43
}
44
44
45
-
(** {1 Commands} *)
46
-
47
-
let login ~sw ~client ~pds ~identifier ~password =
48
-
run_with_eio ~sw ~client (fun () ->
45
+
let login ~hcs_client ~pds ~identifier ~password =
46
+
run_with_hcs ~hcs_client (fun () ->
49
47
let agent = Agent.create_from_url ~url:pds in
50
48
match Agent.login agent ~identifier ~password with
51
49
| Error e ->
···
56
54
(Option.value ~default:"?" (Agent.handle agent));
57
55
Some agent)
58
56
59
-
let post ~sw ~client agent text =
60
-
run_with_eio ~sw ~client (fun () ->
57
+
let post ~hcs_client agent text =
58
+
run_with_hcs ~hcs_client (fun () ->
61
59
let rt = Richtext.detect_facets text in
62
60
match Agent.create_post_richtext agent ~richtext:rt () with
63
61
| Error e ->
···
67
65
Printf.printf "Posted: %s\n" r.uri;
68
66
0)
69
67
70
-
let timeline ~sw ~client agent limit =
71
-
run_with_eio ~sw ~client (fun () ->
68
+
let timeline ~hcs_client agent limit =
69
+
run_with_hcs ~hcs_client (fun () ->
72
70
match Agent.get_timeline agent ~limit () with
73
71
| Error e ->
74
72
Printf.printf "Timeline failed: %s\n" (Agent.error_to_string e);
···
81
79
feed.items;
82
80
0)
83
81
84
-
let profile ~sw ~client agent actor =
85
-
run_with_eio ~sw ~client (fun () ->
82
+
let profile ~hcs_client agent actor =
83
+
run_with_hcs ~hcs_client (fun () ->
86
84
match Agent.get_profile agent ~actor with
87
85
| Error e ->
88
86
Printf.printf "Profile failed: %s\n" (Agent.error_to_string e);
···
95
93
p.followers_count p.follows_count p.posts_count;
96
94
0)
97
95
98
-
let follow ~sw ~client agent did =
99
-
run_with_eio ~sw ~client (fun () ->
96
+
let follow ~hcs_client agent did =
97
+
run_with_hcs ~hcs_client (fun () ->
100
98
match Agent.follow agent ~did with
101
99
| Error e ->
102
100
Printf.printf "Follow failed: %s\n" (Agent.error_to_string e);
···
105
103
Printf.printf "Followed: %s\n" r.uri;
106
104
0)
107
105
108
-
(** {1 CLI} *)
109
-
110
106
type cmd =
111
107
| Post of string
112
108
| Timeline of int
···
141
137
Mirage_crypto_rng_unix.use_default ();
142
138
Eio_main.run @@ fun env ->
143
139
Eio.Switch.run @@ fun sw ->
144
-
let https_config =
145
-
match
146
-
Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) ()
147
-
with
148
-
| Ok c -> c
149
-
| Error (`Msg m) -> failwith m
140
+
let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in
141
+
let hcs_client =
142
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
143
+
~clock:(Eio.Stdenv.clock env) ~config ()
150
144
in
151
-
let https uri socket =
152
-
let tls_host =
153
-
match Uri.host uri with
154
-
| Some h -> (
155
-
match Domain_name.of_string h with
156
-
| Error _ -> Option.None
157
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
158
-
| Option.None -> Option.None
159
-
in
160
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
161
-
in
162
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
163
145
let pds, identifier, password, cmd =
164
146
Climate.Command.run ~program_name:(Climate.Program_name.Literal "bsky_bot")
165
147
(Climate.Command.singleton ~doc:"Bluesky bot - post, timeline, follow" cli)
166
148
in
167
-
match cmd with
168
-
| None ->
169
-
Printf.printf
170
-
"Usage: bsky_bot --user USER --password PASS [--post \
171
-
TEXT|--timeline|--profile ACTOR|--follow DID]\n";
172
-
exit 0
173
-
| _ -> (
174
-
match (identifier, password) with
175
-
| Some id, Some pw -> (
176
-
match login ~sw ~client ~pds ~identifier:id ~password:pw with
177
-
| None -> exit 1
178
-
| Some agent ->
179
-
exit
180
-
(match cmd with
181
-
| Post t -> post ~sw ~client agent t
182
-
| Timeline n -> timeline ~sw ~client agent n
183
-
| Profile a -> profile ~sw ~client agent a
184
-
| Follow d -> follow ~sw ~client agent d
149
+
let result =
150
+
match cmd with
151
+
| None ->
152
+
Printf.printf
153
+
"Usage: bsky_bot --user USER --password PASS [--post \
154
+
TEXT|--timeline|--profile ACTOR|--follow DID]\n";
155
+
0
156
+
| _ -> (
157
+
match (identifier, password) with
158
+
| Some id, Some pw -> (
159
+
match login ~hcs_client ~pds ~identifier:id ~password:pw with
160
+
| None -> 1
161
+
| Some agent -> (
162
+
match cmd with
163
+
| Post t -> post ~hcs_client agent t
164
+
| Timeline n -> timeline ~hcs_client agent n
165
+
| Profile a -> profile ~hcs_client agent a
166
+
| Follow d -> follow ~hcs_client agent d
185
167
| None -> 0))
186
-
| _ ->
187
-
Printf.printf "Error: --user and --password required\n";
188
-
exit 1)
168
+
| _ ->
169
+
Printf.printf "Error: --user and --password required\n";
170
+
1)
171
+
in
172
+
Hcs.Client.close hcs_client;
173
+
exit result
+1
-4
examples/bsky_bot/dune
+1
-4
examples/bsky_bot/dune
+2
-8
examples/feed_generator/dune
+2
-8
examples/feed_generator/dune
···
1
1
(executable
2
2
(name feed_generator)
3
-
(public_name feed_generator)
4
-
(package atproto)
5
3
(libraries
6
4
atproto-sync
7
5
atproto-ipld
···
9
7
uri
10
8
eio
11
9
eio_main
12
-
tls-eio
13
-
ca-certs-nss
14
-
mirage-crypto-rng.unix
15
-
base64
16
-
cstruct))
17
-
10
+
hcs
11
+
mirage-crypto-rng.unix))
+35
-243
examples/feed_generator/feed_generator.ml
+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
-4
examples/identity_tool/dune
+36
-49
examples/identity_tool/identity_tool.ml
+36
-49
examples/identity_tool/identity_tool.ml
···
5
5
open Atproto_syntax
6
6
open Atproto_identity
7
7
8
-
(** {1 HTTP Client with cohttp-eio} *)
8
+
let http_get ~hcs_client uri =
9
+
let url = Uri.to_string uri in
10
+
match Hcs.Client.request hcs_client url with
11
+
| Ok resp -> Did_resolver.{ status = resp.status; body = resp.body }
12
+
| Error e ->
13
+
let msg =
14
+
match e with
15
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
16
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
17
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
18
+
| Hcs.Client.Timeout -> "Timeout"
19
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
20
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
21
+
in
22
+
Did_resolver.{ status = 0; body = msg }
9
23
10
-
let http_get ~sw ~client uri =
11
-
try
12
-
let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in
13
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
14
-
let body =
15
-
Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all)
16
-
in
17
-
Did_resolver.{ status; body }
18
-
with e -> Did_resolver.{ status = 0; body = Printexc.to_string e }
19
-
20
-
(** {1 Effect Handler} *)
21
-
22
-
let run_with_eio ~sw ~client f =
24
+
let run_with_hcs ~hcs_client f =
23
25
Effect.Deep.try_with f ()
24
26
{
25
27
effc =
···
28
30
| Did_resolver.Http_get uri ->
29
31
Some
30
32
(fun (k : (a, _) Effect.Deep.continuation) ->
31
-
Effect.Deep.continue k (http_get ~sw ~client uri))
33
+
Effect.Deep.continue k (http_get ~hcs_client uri))
32
34
| Handle_resolver.Http_get uri ->
33
35
Some
34
36
(fun k ->
35
-
let r = http_get ~sw ~client uri in
37
+
let r = http_get ~hcs_client uri in
36
38
Effect.Deep.continue k
37
39
Handle_resolver.{ status = r.status; body = r.body })
38
40
| Handle_resolver.Dns_txt _ ->
···
41
43
| _ -> None);
42
44
}
43
45
44
-
(** {1 Commands} *)
45
-
46
-
let resolve_handle ~sw ~client h =
46
+
let resolve_handle ~hcs_client h =
47
47
match Handle.of_string h with
48
48
| Error _ ->
49
49
Printf.printf "Error: Invalid handle\n";
50
50
1
51
51
| Ok handle ->
52
-
run_with_eio ~sw ~client (fun () ->
52
+
run_with_hcs ~hcs_client (fun () ->
53
53
match Handle_resolver.resolve handle with
54
54
| Error e ->
55
55
Printf.printf "Error: %s\n" (Handle_resolver.error_to_string e);
···
59
59
(Did.to_string did);
60
60
0)
61
61
62
-
let resolve_did ~sw ~client d =
62
+
let resolve_did ~hcs_client d =
63
63
match Did.of_string d with
64
64
| Error _ ->
65
65
Printf.printf "Error: Invalid DID\n";
66
66
1
67
67
| Ok did ->
68
-
run_with_eio ~sw ~client (fun () ->
68
+
run_with_hcs ~hcs_client (fun () ->
69
69
match Did_resolver.resolve_did did with
70
70
| Error e ->
71
71
Printf.printf "Error: %s\n" (Did_resolver.error_to_string e);
···
87
87
doc.service;
88
88
0)
89
89
90
-
let verify ~sw ~client id =
90
+
let verify ~hcs_client id =
91
91
let is_did = String.length id > 4 && String.sub id 0 4 = "did:" in
92
-
run_with_eio ~sw ~client (fun () ->
92
+
run_with_hcs ~hcs_client (fun () ->
93
93
let result =
94
94
if is_did then
95
95
match Did.of_string id with
···
116
116
v.pds_endpoint;
117
117
0)
118
118
119
-
(** {1 CLI} *)
120
-
121
119
type mode = Resolve_handle | Resolve_did | Verify
122
120
123
121
let cli =
···
136
134
Mirage_crypto_rng_unix.use_default ();
137
135
Eio_main.run @@ fun env ->
138
136
Eio.Switch.run @@ fun sw ->
139
-
let https_config =
140
-
match
141
-
Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) ()
142
-
with
143
-
| Ok c -> c
144
-
| Error (`Msg m) -> failwith m
145
-
in
146
-
let https uri socket =
147
-
let tls_host =
148
-
match Uri.host uri with
149
-
| Some h -> (
150
-
match Domain_name.of_string h with
151
-
| Error _ -> None
152
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
153
-
| None -> None
154
-
in
155
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
137
+
let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in
138
+
let hcs_client =
139
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
140
+
~clock:(Eio.Stdenv.clock env) ~config ()
156
141
in
157
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
158
142
let mode, id =
159
143
Climate.Command.run
160
144
~program_name:(Climate.Program_name.Literal "identity_tool")
161
145
(Climate.Command.singleton ~doc:"AT Protocol identity lookup tool" cli)
162
146
in
163
-
exit
164
-
(match mode with
165
-
| Resolve_handle -> resolve_handle ~sw ~client id
166
-
| Resolve_did -> resolve_did ~sw ~client id
167
-
| Verify -> verify ~sw ~client id)
147
+
let result =
148
+
match mode with
149
+
| Resolve_handle -> resolve_handle ~hcs_client id
150
+
| Resolve_did -> resolve_did ~hcs_client id
151
+
| Verify -> verify ~hcs_client id
152
+
in
153
+
Hcs.Client.close hcs_client;
154
+
exit result
+1
-5
examples/repo_inspector/dune
+1
-5
examples/repo_inspector/dune
···
1
1
(executable
2
2
(name repo_inspector)
3
-
(public_name repo_inspector)
4
-
(package atproto)
5
3
(libraries
6
4
atproto-repo
7
5
atproto-mst
···
11
9
atproto-crypto
12
10
climate
13
11
eio_main
14
-
cohttp-eio
15
-
tls-eio
16
-
ca-certs-nss
12
+
hcs
17
13
base64
18
14
mirage-crypto-rng.unix
19
15
uri))
+35
-57
examples/repo_inspector/repo_inspector.ml
+35
-57
examples/repo_inspector/repo_inspector.ml
···
18
18
module Did = Atproto_syntax.Did
19
19
module Did_resolver = Atproto_identity.Did_resolver
20
20
21
-
(** {1 HTTP Client with cohttp-eio} *)
22
-
23
-
let http_get ~sw ~client uri =
24
-
try
25
-
let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in
26
-
let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in
27
-
let body =
28
-
Eio.Buf_read.(of_flow ~max_size:(100 * 1024 * 1024) resp_body |> take_all)
29
-
in
30
-
(status, body)
31
-
with e -> (0, Printexc.to_string e)
32
-
33
-
(** {1 DID Resolution Effect Handler} *)
21
+
let http_get ~hcs_client uri =
22
+
let url = Uri.to_string uri in
23
+
match Hcs.Client.request hcs_client url with
24
+
| Ok resp -> (resp.status, resp.body)
25
+
| Error e ->
26
+
let msg =
27
+
match e with
28
+
| Hcs.Client.Connection_failed s -> "Connection failed: " ^ s
29
+
| Hcs.Client.Tls_error s -> "TLS error: " ^ s
30
+
| Hcs.Client.Protocol_error s -> "Protocol error: " ^ s
31
+
| Hcs.Client.Timeout -> "Timeout"
32
+
| Hcs.Client.Invalid_response s -> "Invalid response: " ^ s
33
+
| Hcs.Client.Too_many_redirects -> "Too many redirects"
34
+
in
35
+
(0, msg)
34
36
35
-
let run_with_resolver ~sw ~client f =
37
+
let run_with_resolver ~hcs_client f =
36
38
Effect.Deep.try_with f ()
37
39
{
38
40
effc =
···
41
43
| Did_resolver.Http_get uri ->
42
44
Some
43
45
(fun (k : (a, _) Effect.Deep.continuation) ->
44
-
let status, body = http_get ~sw ~client uri in
46
+
let status, body = http_get ~hcs_client uri in
45
47
Effect.Deep.continue k Did_resolver.{ status; body })
46
48
| _ -> None);
47
49
}
48
50
49
-
(** {1 PDS Resolution} *)
50
-
51
-
let resolve_pds ~sw ~client did_str =
51
+
let resolve_pds ~hcs_client did_str =
52
52
match Did.of_string did_str with
53
53
| Error _ -> Error "Invalid DID format"
54
54
| Ok did ->
55
-
run_with_resolver ~sw ~client (fun () ->
55
+
run_with_resolver ~hcs_client (fun () ->
56
56
match Did_resolver.resolve_did did with
57
57
| Error e -> Error (Did_resolver.error_to_string e)
58
58
| Ok doc -> (
59
-
(* Find AtprotoPersonalDataServer service *)
60
59
match
61
60
List.find_opt
62
61
(fun (s : Did_resolver.service) ->
···
66
65
| Some s -> Ok s.service_endpoint
67
66
| None -> Error "No PDS service found in DID document"))
68
67
69
-
(** {1 Repository Fetching} *)
70
-
71
-
let fetch_repo ~sw ~client ~pds_url did =
68
+
let fetch_repo ~hcs_client ~pds_url did =
72
69
let uri =
73
70
Uri.of_string (pds_url ^ "/xrpc/com.atproto.sync.getRepo")
74
71
|> Fun.flip Uri.add_query_param ("did", [ did ])
75
72
in
76
73
Printf.printf "Fetching %s...\n%!" (Uri.to_string uri);
77
-
let status, body = http_get ~sw ~client uri in
74
+
let status, body = http_get ~hcs_client uri in
78
75
if status = 200 then Ok body
79
76
else
80
77
Error
81
78
(Printf.sprintf "HTTP %d: %s" status
82
79
(String.sub body 0 (min 200 (String.length body))))
83
80
84
-
(** {1 Repository Analysis} *)
85
-
86
81
let truncate n s =
87
82
if String.length s <= n then s else String.sub s 0 (n - 3) ^ "..."
88
83
···
110
105
(fun r -> Printf.printf " - %s\n" (Cid.to_string r))
111
106
header.roots;
112
107
Printf.printf "Blocks: %d\n\n" (List.length blocks);
113
-
(* Build blockstore *)
114
108
let store = Blockstore.create () in
115
109
List.iter
116
110
(fun (b : Car.block) -> Blockstore.put store b.cid b.data)
117
111
blocks;
118
-
(* Find commit *)
119
112
let commit_cid = List.hd header.roots in
120
113
let commit_data = Blockstore.get store commit_cid in
121
114
match commit_data with
···
181
174
(fun (key, cid) ->
182
175
if !shown < limit then begin
183
176
Printf.printf "%s\n CID: %s\n" key (Cid.to_string cid);
184
-
(* Try to get and show content preview *)
185
177
(match Blockstore.get store cid with
186
178
| Some data -> (
187
179
match Dag_cbor.decode data with
···
207
199
else "(none)"
208
200
in
209
201
Printf.printf "Signature: %s\n" sig_b64;
210
-
(* Get signing key from DID - would need identity resolution *)
211
202
Printf.printf
212
203
"Status: Signature present but key resolution not implemented\n";
213
204
Printf.printf " (would need to resolve DID document to verify)\n"
214
205
215
-
(** {1 CLI} *)
216
-
217
206
type mode = Summary | Collections | Records of string option | Verify
218
207
219
208
let cli =
···
239
228
Mirage_crypto_rng_unix.use_default ();
240
229
Eio_main.run @@ fun env ->
241
230
Eio.Switch.run @@ fun sw ->
242
-
let auth =
243
-
match Ca_certs_nss.authenticator () with
244
-
| Ok a -> a
245
-
| Error (`Msg m) -> failwith m
231
+
let config =
232
+
Hcs.Client.default_config |> Hcs.Client.with_max_response_body 104857600L
246
233
in
247
-
let https_config =
248
-
match Tls.Config.client ~authenticator:auth () with
249
-
| Ok c -> c
250
-
| Error (`Msg m) -> failwith m
251
-
in
252
-
let https uri socket =
253
-
let tls_host =
254
-
match Uri.host uri with
255
-
| Some h -> (
256
-
match Domain_name.of_string h with
257
-
| Error _ -> None
258
-
| Ok dn -> Domain_name.host dn |> Result.to_option)
259
-
| None -> None
260
-
in
261
-
Tls_eio.client_of_flow https_config ?host:tls_host socket
234
+
let hcs_client =
235
+
Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env)
236
+
~clock:(Eio.Stdenv.clock env) ~config ()
262
237
in
263
-
let client = Cohttp_eio.Client.make ~https:(Some https) env#net in
264
238
let did, pds_opt, mode, limit =
265
239
Climate.Command.run
266
240
~program_name:(Climate.Program_name.Literal "repo_inspector")
267
241
(Climate.Command.singleton ~doc:"AT Protocol repository inspector" cli)
268
242
in
269
243
Printf.printf "Repository Inspector\n====================\n\n";
270
-
(* Resolve PDS if not provided *)
271
244
let pds_url =
272
245
match pds_opt with
273
246
| Some url -> url
274
247
| None -> (
275
248
Printf.printf "Resolving PDS for %s...\n%!" did;
276
-
match resolve_pds ~sw ~client did with
249
+
match resolve_pds ~hcs_client did with
277
250
| Ok url ->
278
251
Printf.printf "PDS: %s\n\n%!" url;
279
252
url
280
253
| Error e ->
281
254
Printf.printf "Error resolving PDS: %s\n" e;
255
+
Hcs.Client.close hcs_client;
282
256
exit 1)
283
257
in
284
-
match fetch_repo ~sw ~client ~pds_url did with
258
+
(match fetch_repo ~hcs_client ~pds_url did with
285
259
| Error e ->
286
260
Printf.printf "Error: %s\n" e;
261
+
Hcs.Client.close hcs_client;
287
262
exit 1
288
263
| Ok car_data -> (
289
264
match analyze_repo car_data with
290
-
| None -> exit 1
265
+
| None ->
266
+
Hcs.Client.close hcs_client;
267
+
exit 1
291
268
| Some (store, commit) -> (
292
269
show_commit commit;
293
270
match mode with
294
271
| Summary -> show_collections store commit
295
272
| Collections -> show_collections store commit
296
273
| Records coll -> show_records ~limit ~collection:coll store commit
297
-
| Verify -> verify_commit store commit))
274
+
| Verify -> verify_commit store commit)));
275
+
Hcs.Client.close hcs_client
+8
-1
lib/api/dune
+8
-1
lib/api/dune
+8
-1
lib/crypto/dune
+8
-1
lib/crypto/dune
···
1
1
(library
2
2
(name atproto_crypto)
3
3
(public_name atproto-crypto)
4
-
(libraries atproto_multibase mirage-crypto-ec mirage-crypto-rng digestif zarith base64 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
+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
+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
+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))