Highly ambitious ATProtocol AppView service and sdks

wip

+16
.editorconfig
··· 1 + root = true 2 + 3 + [*] 4 + end_of_line = lf 5 + charset = utf-8 6 + trim_trailing_whitespace = true 7 + insert_final_newline = true 8 + indent_style = space 9 + indent_size = 2 10 + 11 + [*.txt] 12 + indent_style = tab 13 + indent_size = 4 14 + 15 + [*.{diff,md}] 16 + trim_trailing_whitespace = false
+1
api/.env
··· 1 + DATABASE_URL=postgresql://slice:slice@localhost:5432/slice
+1
api/.gitignore
··· 1 + /target
+59
api/CLAUDE.md
··· 1 + ### OAuth 2.0 Endpoints with AIP 2 + 3 + The AIP server implements the following OAuth 2.0 endpoints: 4 + 5 + - `GET ${AIP_BASE_URL}/oauth/authorize` - Authorization endpoint for OAuth flows 6 + - `POST ${AIP_BASE_URL}/oauth/token` - Token endpoint for exchanging 7 + authorization codes for access tokens 8 + - `POST ${AIP_BASE_URL}/oauth/par` - Pushed Authorization Request endpoint 9 + (RFC 9126) 10 + - `POST ${AIP_BASE_URL}/oauth/clients/register` - Dynamic Client Registration 11 + endpoint (RFC 7591) 12 + - `GET ${AIP_BASE_URL}/oauth/atp/callback` - ATProtocol OAuth callback handler 13 + - `GET ${AIP_BASE_URL}/.well-known/oauth-authorization-server` - OAuth server 14 + metadata discovery (RFC 8414) 15 + - `GET ${AIP_BASE_URL}/.well-known/oauth-protected-resource` - Protected 16 + resource metadata 17 + - `GET ${AIP_BASE_URL}/.well-known/jwks.json` - JSON Web Key Set for token 18 + verification 19 + - `GET ${AIP_BASE_URL}/oauth/userinfo` - introspection endpoint returning claims 20 + info where sub is the user's atproto did 21 + - `GET ${AIP_BASE_URL}/api/atproto/session` - returns atproto session data 22 + 23 + ## Error Handling 24 + 25 + All error strings must use this format: 26 + 27 + error-aip-<domain>-<number> <message>: <details> 28 + 29 + Example errors: 30 + 31 + - error-slice-resolve-1 Multiple DIDs resolved for method 32 + - error-slice-plc-1 HTTP request failed: https://google.com/ Not Found 33 + - error-slice-key-1 Error decoding key: invalid 34 + 35 + Errors should be represented as enums using the `thiserror` library when 36 + possible using `src/errors.rs` as a reference and example. 37 + 38 + Avoid creating new errors with the `anyhow!(...)` macro. 39 + 40 + ## Time, Date, and Duration 41 + 42 + Use the `chrono` crate for time, date, and duration logic. 43 + 44 + Use the `duration_str` crate for parsing string duration values. 45 + 46 + All stored dates and times must be in UTC. UTC should be used whenever 47 + determining the current time and computing values like expiration. 48 + 49 + ## HTTP Handler Organization 50 + 51 + HTTP handlers should be organized as Rust source files in the `src/http` 52 + directory and should have the `handler_` prefix. Each handler should have it's 53 + own request and response types and helper functionality. 54 + 55 + Example handler: `handler_index.rs` 56 + 57 + - After updating, run `cargo check` to fix errors and warnings 58 + - Don't use dead code, if it's not used remove it 59 + - Ise htmx and hyperscript when possible, if not javascript in script tag is ok
+3438
api/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.24.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.1" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 + 20 + [[package]] 21 + name = "aes" 22 + version = "0.8.4" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 25 + dependencies = [ 26 + "cfg-if", 27 + "cipher", 28 + "cpufeatures", 29 + ] 30 + 31 + [[package]] 32 + name = "aho-corasick" 33 + version = "1.1.3" 34 + source = "registry+https://github.com/rust-lang/crates.io-index" 35 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 36 + dependencies = [ 37 + "memchr", 38 + ] 39 + 40 + [[package]] 41 + name = "allocator-api2" 42 + version = "0.2.21" 43 + source = "registry+https://github.com/rust-lang/crates.io-index" 44 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 45 + 46 + [[package]] 47 + name = "android-tzdata" 48 + version = "0.1.1" 49 + source = "registry+https://github.com/rust-lang/crates.io-index" 50 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 51 + 52 + [[package]] 53 + name = "android_system_properties" 54 + version = "0.1.5" 55 + source = "registry+https://github.com/rust-lang/crates.io-index" 56 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 57 + dependencies = [ 58 + "libc", 59 + ] 60 + 61 + [[package]] 62 + name = "arbitrary" 63 + version = "1.4.2" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 66 + dependencies = [ 67 + "derive_arbitrary", 68 + ] 69 + 70 + [[package]] 71 + name = "async-trait" 72 + version = "0.1.89" 73 + source = "registry+https://github.com/rust-lang/crates.io-index" 74 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 75 + dependencies = [ 76 + "proc-macro2", 77 + "quote", 78 + "syn", 79 + ] 80 + 81 + [[package]] 82 + name = "atoi" 83 + version = "2.0.0" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 86 + dependencies = [ 87 + "num-traits", 88 + ] 89 + 90 + [[package]] 91 + name = "atomic-waker" 92 + version = "1.1.2" 93 + source = "registry+https://github.com/rust-lang/crates.io-index" 94 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 95 + 96 + [[package]] 97 + name = "autocfg" 98 + version = "1.5.0" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 101 + 102 + [[package]] 103 + name = "axum" 104 + version = "0.7.9" 105 + source = "registry+https://github.com/rust-lang/crates.io-index" 106 + checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 107 + dependencies = [ 108 + "async-trait", 109 + "axum-core", 110 + "axum-macros", 111 + "base64", 112 + "bytes", 113 + "futures-util", 114 + "http", 115 + "http-body", 116 + "http-body-util", 117 + "hyper", 118 + "hyper-util", 119 + "itoa", 120 + "matchit", 121 + "memchr", 122 + "mime", 123 + "percent-encoding", 124 + "pin-project-lite", 125 + "rustversion", 126 + "serde", 127 + "serde_json", 128 + "serde_path_to_error", 129 + "serde_urlencoded", 130 + "sha1", 131 + "sync_wrapper", 132 + "tokio", 133 + "tokio-tungstenite", 134 + "tower", 135 + "tower-layer", 136 + "tower-service", 137 + "tracing", 138 + ] 139 + 140 + [[package]] 141 + name = "axum-core" 142 + version = "0.4.5" 143 + source = "registry+https://github.com/rust-lang/crates.io-index" 144 + checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 145 + dependencies = [ 146 + "async-trait", 147 + "bytes", 148 + "futures-util", 149 + "http", 150 + "http-body", 151 + "http-body-util", 152 + "mime", 153 + "pin-project-lite", 154 + "rustversion", 155 + "sync_wrapper", 156 + "tower-layer", 157 + "tower-service", 158 + "tracing", 159 + ] 160 + 161 + [[package]] 162 + name = "axum-extra" 163 + version = "0.9.6" 164 + source = "registry+https://github.com/rust-lang/crates.io-index" 165 + checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" 166 + dependencies = [ 167 + "axum", 168 + "axum-core", 169 + "bytes", 170 + "fastrand", 171 + "futures-util", 172 + "http", 173 + "http-body", 174 + "http-body-util", 175 + "mime", 176 + "multer", 177 + "pin-project-lite", 178 + "serde", 179 + "serde_html_form", 180 + "tower", 181 + "tower-layer", 182 + "tower-service", 183 + ] 184 + 185 + [[package]] 186 + name = "axum-macros" 187 + version = "0.4.2" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" 190 + dependencies = [ 191 + "proc-macro2", 192 + "quote", 193 + "syn", 194 + ] 195 + 196 + [[package]] 197 + name = "backtrace" 198 + version = "0.3.75" 199 + source = "registry+https://github.com/rust-lang/crates.io-index" 200 + checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 201 + dependencies = [ 202 + "addr2line", 203 + "cfg-if", 204 + "libc", 205 + "miniz_oxide", 206 + "object", 207 + "rustc-demangle", 208 + "windows-targets 0.52.6", 209 + ] 210 + 211 + [[package]] 212 + name = "base64" 213 + version = "0.22.1" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 216 + 217 + [[package]] 218 + name = "base64ct" 219 + version = "1.8.0" 220 + source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 222 + 223 + [[package]] 224 + name = "bitflags" 225 + version = "2.9.1" 226 + source = "registry+https://github.com/rust-lang/crates.io-index" 227 + checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 228 + dependencies = [ 229 + "serde", 230 + ] 231 + 232 + [[package]] 233 + name = "block-buffer" 234 + version = "0.10.4" 235 + source = "registry+https://github.com/rust-lang/crates.io-index" 236 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 237 + dependencies = [ 238 + "generic-array", 239 + ] 240 + 241 + [[package]] 242 + name = "bumpalo" 243 + version = "3.19.0" 244 + source = "registry+https://github.com/rust-lang/crates.io-index" 245 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 246 + 247 + [[package]] 248 + name = "byteorder" 249 + version = "1.5.0" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 252 + 253 + [[package]] 254 + name = "bytes" 255 + version = "1.10.1" 256 + source = "registry+https://github.com/rust-lang/crates.io-index" 257 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 258 + 259 + [[package]] 260 + name = "bzip2" 261 + version = "0.6.0" 262 + source = "registry+https://github.com/rust-lang/crates.io-index" 263 + checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" 264 + dependencies = [ 265 + "libbz2-rs-sys", 266 + ] 267 + 268 + [[package]] 269 + name = "cc" 270 + version = "1.2.33" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" 273 + dependencies = [ 274 + "jobserver", 275 + "libc", 276 + "shlex", 277 + ] 278 + 279 + [[package]] 280 + name = "cfg-if" 281 + version = "1.0.1" 282 + source = "registry+https://github.com/rust-lang/crates.io-index" 283 + checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 284 + 285 + [[package]] 286 + name = "chrono" 287 + version = "0.4.41" 288 + source = "registry+https://github.com/rust-lang/crates.io-index" 289 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 290 + dependencies = [ 291 + "android-tzdata", 292 + "iana-time-zone", 293 + "js-sys", 294 + "num-traits", 295 + "serde", 296 + "wasm-bindgen", 297 + "windows-link", 298 + ] 299 + 300 + [[package]] 301 + name = "cipher" 302 + version = "0.4.4" 303 + source = "registry+https://github.com/rust-lang/crates.io-index" 304 + checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 305 + dependencies = [ 306 + "crypto-common", 307 + "inout", 308 + ] 309 + 310 + [[package]] 311 + name = "concurrent-queue" 312 + version = "2.5.0" 313 + source = "registry+https://github.com/rust-lang/crates.io-index" 314 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 315 + dependencies = [ 316 + "crossbeam-utils", 317 + ] 318 + 319 + [[package]] 320 + name = "const-oid" 321 + version = "0.9.6" 322 + source = "registry+https://github.com/rust-lang/crates.io-index" 323 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 324 + 325 + [[package]] 326 + name = "constant_time_eq" 327 + version = "0.3.1" 328 + source = "registry+https://github.com/rust-lang/crates.io-index" 329 + checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" 330 + 331 + [[package]] 332 + name = "core-foundation" 333 + version = "0.9.4" 334 + source = "registry+https://github.com/rust-lang/crates.io-index" 335 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 336 + dependencies = [ 337 + "core-foundation-sys", 338 + "libc", 339 + ] 340 + 341 + [[package]] 342 + name = "core-foundation-sys" 343 + version = "0.8.7" 344 + source = "registry+https://github.com/rust-lang/crates.io-index" 345 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 346 + 347 + [[package]] 348 + name = "cpufeatures" 349 + version = "0.2.17" 350 + source = "registry+https://github.com/rust-lang/crates.io-index" 351 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 352 + dependencies = [ 353 + "libc", 354 + ] 355 + 356 + [[package]] 357 + name = "crc" 358 + version = "3.3.0" 359 + source = "registry+https://github.com/rust-lang/crates.io-index" 360 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 361 + dependencies = [ 362 + "crc-catalog", 363 + ] 364 + 365 + [[package]] 366 + name = "crc-catalog" 367 + version = "2.4.0" 368 + source = "registry+https://github.com/rust-lang/crates.io-index" 369 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 370 + 371 + [[package]] 372 + name = "crc32fast" 373 + version = "1.5.0" 374 + source = "registry+https://github.com/rust-lang/crates.io-index" 375 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 376 + dependencies = [ 377 + "cfg-if", 378 + ] 379 + 380 + [[package]] 381 + name = "crossbeam-queue" 382 + version = "0.3.12" 383 + source = "registry+https://github.com/rust-lang/crates.io-index" 384 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 385 + dependencies = [ 386 + "crossbeam-utils", 387 + ] 388 + 389 + [[package]] 390 + name = "crossbeam-utils" 391 + version = "0.8.21" 392 + source = "registry+https://github.com/rust-lang/crates.io-index" 393 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 394 + 395 + [[package]] 396 + name = "crypto-common" 397 + version = "0.1.6" 398 + source = "registry+https://github.com/rust-lang/crates.io-index" 399 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 400 + dependencies = [ 401 + "generic-array", 402 + "typenum", 403 + ] 404 + 405 + [[package]] 406 + name = "data-encoding" 407 + version = "2.9.0" 408 + source = "registry+https://github.com/rust-lang/crates.io-index" 409 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 410 + 411 + [[package]] 412 + name = "deflate64" 413 + version = "0.1.9" 414 + source = "registry+https://github.com/rust-lang/crates.io-index" 415 + checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" 416 + 417 + [[package]] 418 + name = "der" 419 + version = "0.7.10" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 422 + dependencies = [ 423 + "const-oid", 424 + "pem-rfc7468", 425 + "zeroize", 426 + ] 427 + 428 + [[package]] 429 + name = "deranged" 430 + version = "0.4.0" 431 + source = "registry+https://github.com/rust-lang/crates.io-index" 432 + checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 433 + dependencies = [ 434 + "powerfmt", 435 + ] 436 + 437 + [[package]] 438 + name = "derive_arbitrary" 439 + version = "1.4.2" 440 + source = "registry+https://github.com/rust-lang/crates.io-index" 441 + checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" 442 + dependencies = [ 443 + "proc-macro2", 444 + "quote", 445 + "syn", 446 + ] 447 + 448 + [[package]] 449 + name = "digest" 450 + version = "0.10.7" 451 + source = "registry+https://github.com/rust-lang/crates.io-index" 452 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 453 + dependencies = [ 454 + "block-buffer", 455 + "const-oid", 456 + "crypto-common", 457 + "subtle", 458 + ] 459 + 460 + [[package]] 461 + name = "displaydoc" 462 + version = "0.2.5" 463 + source = "registry+https://github.com/rust-lang/crates.io-index" 464 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 465 + dependencies = [ 466 + "proc-macro2", 467 + "quote", 468 + "syn", 469 + ] 470 + 471 + [[package]] 472 + name = "dotenvy" 473 + version = "0.15.7" 474 + source = "registry+https://github.com/rust-lang/crates.io-index" 475 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 476 + 477 + [[package]] 478 + name = "either" 479 + version = "1.15.0" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 482 + dependencies = [ 483 + "serde", 484 + ] 485 + 486 + [[package]] 487 + name = "encoding_rs" 488 + version = "0.8.35" 489 + source = "registry+https://github.com/rust-lang/crates.io-index" 490 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 491 + dependencies = [ 492 + "cfg-if", 493 + ] 494 + 495 + [[package]] 496 + name = "equivalent" 497 + version = "1.0.2" 498 + source = "registry+https://github.com/rust-lang/crates.io-index" 499 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 500 + 501 + [[package]] 502 + name = "errno" 503 + version = "0.3.13" 504 + source = "registry+https://github.com/rust-lang/crates.io-index" 505 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 506 + dependencies = [ 507 + "libc", 508 + "windows-sys 0.60.2", 509 + ] 510 + 511 + [[package]] 512 + name = "etcetera" 513 + version = "0.8.0" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 516 + dependencies = [ 517 + "cfg-if", 518 + "home", 519 + "windows-sys 0.48.0", 520 + ] 521 + 522 + [[package]] 523 + name = "event-listener" 524 + version = "5.4.1" 525 + source = "registry+https://github.com/rust-lang/crates.io-index" 526 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 527 + dependencies = [ 528 + "concurrent-queue", 529 + "parking", 530 + "pin-project-lite", 531 + ] 532 + 533 + [[package]] 534 + name = "fastrand" 535 + version = "2.3.0" 536 + source = "registry+https://github.com/rust-lang/crates.io-index" 537 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 538 + 539 + [[package]] 540 + name = "flate2" 541 + version = "1.1.2" 542 + source = "registry+https://github.com/rust-lang/crates.io-index" 543 + checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 544 + dependencies = [ 545 + "crc32fast", 546 + "libz-rs-sys", 547 + "miniz_oxide", 548 + ] 549 + 550 + [[package]] 551 + name = "flume" 552 + version = "0.11.1" 553 + source = "registry+https://github.com/rust-lang/crates.io-index" 554 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 555 + dependencies = [ 556 + "futures-core", 557 + "futures-sink", 558 + "spin", 559 + ] 560 + 561 + [[package]] 562 + name = "fnv" 563 + version = "1.0.7" 564 + source = "registry+https://github.com/rust-lang/crates.io-index" 565 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 566 + 567 + [[package]] 568 + name = "foldhash" 569 + version = "0.1.5" 570 + source = "registry+https://github.com/rust-lang/crates.io-index" 571 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 572 + 573 + [[package]] 574 + name = "foreign-types" 575 + version = "0.3.2" 576 + source = "registry+https://github.com/rust-lang/crates.io-index" 577 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 578 + dependencies = [ 579 + "foreign-types-shared", 580 + ] 581 + 582 + [[package]] 583 + name = "foreign-types-shared" 584 + version = "0.1.1" 585 + source = "registry+https://github.com/rust-lang/crates.io-index" 586 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 587 + 588 + [[package]] 589 + name = "form_urlencoded" 590 + version = "1.2.1" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 593 + dependencies = [ 594 + "percent-encoding", 595 + ] 596 + 597 + [[package]] 598 + name = "futures-channel" 599 + version = "0.3.31" 600 + source = "registry+https://github.com/rust-lang/crates.io-index" 601 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 602 + dependencies = [ 603 + "futures-core", 604 + "futures-sink", 605 + ] 606 + 607 + [[package]] 608 + name = "futures-core" 609 + version = "0.3.31" 610 + source = "registry+https://github.com/rust-lang/crates.io-index" 611 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 612 + 613 + [[package]] 614 + name = "futures-executor" 615 + version = "0.3.31" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 618 + dependencies = [ 619 + "futures-core", 620 + "futures-task", 621 + "futures-util", 622 + ] 623 + 624 + [[package]] 625 + name = "futures-intrusive" 626 + version = "0.5.0" 627 + source = "registry+https://github.com/rust-lang/crates.io-index" 628 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 629 + dependencies = [ 630 + "futures-core", 631 + "lock_api", 632 + "parking_lot", 633 + ] 634 + 635 + [[package]] 636 + name = "futures-io" 637 + version = "0.3.31" 638 + source = "registry+https://github.com/rust-lang/crates.io-index" 639 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 640 + 641 + [[package]] 642 + name = "futures-macro" 643 + version = "0.3.31" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 646 + dependencies = [ 647 + "proc-macro2", 648 + "quote", 649 + "syn", 650 + ] 651 + 652 + [[package]] 653 + name = "futures-sink" 654 + version = "0.3.31" 655 + source = "registry+https://github.com/rust-lang/crates.io-index" 656 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 657 + 658 + [[package]] 659 + name = "futures-task" 660 + version = "0.3.31" 661 + source = "registry+https://github.com/rust-lang/crates.io-index" 662 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 663 + 664 + [[package]] 665 + name = "futures-util" 666 + version = "0.3.31" 667 + source = "registry+https://github.com/rust-lang/crates.io-index" 668 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 669 + dependencies = [ 670 + "futures-core", 671 + "futures-io", 672 + "futures-macro", 673 + "futures-sink", 674 + "futures-task", 675 + "memchr", 676 + "pin-project-lite", 677 + "pin-utils", 678 + "slab", 679 + ] 680 + 681 + [[package]] 682 + name = "generic-array" 683 + version = "0.14.7" 684 + source = "registry+https://github.com/rust-lang/crates.io-index" 685 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 686 + dependencies = [ 687 + "typenum", 688 + "version_check", 689 + ] 690 + 691 + [[package]] 692 + name = "getrandom" 693 + version = "0.2.16" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 696 + dependencies = [ 697 + "cfg-if", 698 + "libc", 699 + "wasi 0.11.1+wasi-snapshot-preview1", 700 + ] 701 + 702 + [[package]] 703 + name = "getrandom" 704 + version = "0.3.3" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 707 + dependencies = [ 708 + "cfg-if", 709 + "libc", 710 + "r-efi", 711 + "wasi 0.14.2+wasi-0.2.4", 712 + ] 713 + 714 + [[package]] 715 + name = "gimli" 716 + version = "0.31.1" 717 + source = "registry+https://github.com/rust-lang/crates.io-index" 718 + checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 719 + 720 + [[package]] 721 + name = "h2" 722 + version = "0.4.12" 723 + source = "registry+https://github.com/rust-lang/crates.io-index" 724 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 725 + dependencies = [ 726 + "atomic-waker", 727 + "bytes", 728 + "fnv", 729 + "futures-core", 730 + "futures-sink", 731 + "http", 732 + "indexmap", 733 + "slab", 734 + "tokio", 735 + "tokio-util", 736 + "tracing", 737 + ] 738 + 739 + [[package]] 740 + name = "hashbrown" 741 + version = "0.15.5" 742 + source = "registry+https://github.com/rust-lang/crates.io-index" 743 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 744 + dependencies = [ 745 + "allocator-api2", 746 + "equivalent", 747 + "foldhash", 748 + ] 749 + 750 + [[package]] 751 + name = "hashlink" 752 + version = "0.10.0" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 755 + dependencies = [ 756 + "hashbrown", 757 + ] 758 + 759 + [[package]] 760 + name = "heck" 761 + version = "0.5.0" 762 + source = "registry+https://github.com/rust-lang/crates.io-index" 763 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 764 + 765 + [[package]] 766 + name = "hex" 767 + version = "0.4.3" 768 + source = "registry+https://github.com/rust-lang/crates.io-index" 769 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 770 + 771 + [[package]] 772 + name = "hkdf" 773 + version = "0.12.4" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 776 + dependencies = [ 777 + "hmac", 778 + ] 779 + 780 + [[package]] 781 + name = "hmac" 782 + version = "0.12.1" 783 + source = "registry+https://github.com/rust-lang/crates.io-index" 784 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 785 + dependencies = [ 786 + "digest", 787 + ] 788 + 789 + [[package]] 790 + name = "home" 791 + version = "0.5.11" 792 + source = "registry+https://github.com/rust-lang/crates.io-index" 793 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 794 + dependencies = [ 795 + "windows-sys 0.59.0", 796 + ] 797 + 798 + [[package]] 799 + name = "http" 800 + version = "1.3.1" 801 + source = "registry+https://github.com/rust-lang/crates.io-index" 802 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 803 + dependencies = [ 804 + "bytes", 805 + "fnv", 806 + "itoa", 807 + ] 808 + 809 + [[package]] 810 + name = "http-body" 811 + version = "1.0.1" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 814 + dependencies = [ 815 + "bytes", 816 + "http", 817 + ] 818 + 819 + [[package]] 820 + name = "http-body-util" 821 + version = "0.1.3" 822 + source = "registry+https://github.com/rust-lang/crates.io-index" 823 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 824 + dependencies = [ 825 + "bytes", 826 + "futures-core", 827 + "http", 828 + "http-body", 829 + "pin-project-lite", 830 + ] 831 + 832 + [[package]] 833 + name = "httparse" 834 + version = "1.10.1" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 837 + 838 + [[package]] 839 + name = "httpdate" 840 + version = "1.0.3" 841 + source = "registry+https://github.com/rust-lang/crates.io-index" 842 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 843 + 844 + [[package]] 845 + name = "hyper" 846 + version = "1.6.0" 847 + source = "registry+https://github.com/rust-lang/crates.io-index" 848 + checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 849 + dependencies = [ 850 + "bytes", 851 + "futures-channel", 852 + "futures-util", 853 + "h2", 854 + "http", 855 + "http-body", 856 + "httparse", 857 + "httpdate", 858 + "itoa", 859 + "pin-project-lite", 860 + "smallvec", 861 + "tokio", 862 + "want", 863 + ] 864 + 865 + [[package]] 866 + name = "hyper-rustls" 867 + version = "0.27.7" 868 + source = "registry+https://github.com/rust-lang/crates.io-index" 869 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 870 + dependencies = [ 871 + "http", 872 + "hyper", 873 + "hyper-util", 874 + "rustls", 875 + "rustls-pki-types", 876 + "tokio", 877 + "tokio-rustls", 878 + "tower-service", 879 + ] 880 + 881 + [[package]] 882 + name = "hyper-tls" 883 + version = "0.6.0" 884 + source = "registry+https://github.com/rust-lang/crates.io-index" 885 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 886 + dependencies = [ 887 + "bytes", 888 + "http-body-util", 889 + "hyper", 890 + "hyper-util", 891 + "native-tls", 892 + "tokio", 893 + "tokio-native-tls", 894 + "tower-service", 895 + ] 896 + 897 + [[package]] 898 + name = "hyper-util" 899 + version = "0.1.16" 900 + source = "registry+https://github.com/rust-lang/crates.io-index" 901 + checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 902 + dependencies = [ 903 + "base64", 904 + "bytes", 905 + "futures-channel", 906 + "futures-core", 907 + "futures-util", 908 + "http", 909 + "http-body", 910 + "hyper", 911 + "ipnet", 912 + "libc", 913 + "percent-encoding", 914 + "pin-project-lite", 915 + "socket2", 916 + "system-configuration", 917 + "tokio", 918 + "tower-service", 919 + "tracing", 920 + "windows-registry", 921 + ] 922 + 923 + [[package]] 924 + name = "iana-time-zone" 925 + version = "0.1.63" 926 + source = "registry+https://github.com/rust-lang/crates.io-index" 927 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 928 + dependencies = [ 929 + "android_system_properties", 930 + "core-foundation-sys", 931 + "iana-time-zone-haiku", 932 + "js-sys", 933 + "log", 934 + "wasm-bindgen", 935 + "windows-core", 936 + ] 937 + 938 + [[package]] 939 + name = "iana-time-zone-haiku" 940 + version = "0.1.2" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 943 + dependencies = [ 944 + "cc", 945 + ] 946 + 947 + [[package]] 948 + name = "icu_collections" 949 + version = "2.0.0" 950 + source = "registry+https://github.com/rust-lang/crates.io-index" 951 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 952 + dependencies = [ 953 + "displaydoc", 954 + "potential_utf", 955 + "yoke", 956 + "zerofrom", 957 + "zerovec", 958 + ] 959 + 960 + [[package]] 961 + name = "icu_locale_core" 962 + version = "2.0.0" 963 + source = "registry+https://github.com/rust-lang/crates.io-index" 964 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 965 + dependencies = [ 966 + "displaydoc", 967 + "litemap", 968 + "tinystr", 969 + "writeable", 970 + "zerovec", 971 + ] 972 + 973 + [[package]] 974 + name = "icu_normalizer" 975 + version = "2.0.0" 976 + source = "registry+https://github.com/rust-lang/crates.io-index" 977 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 978 + dependencies = [ 979 + "displaydoc", 980 + "icu_collections", 981 + "icu_normalizer_data", 982 + "icu_properties", 983 + "icu_provider", 984 + "smallvec", 985 + "zerovec", 986 + ] 987 + 988 + [[package]] 989 + name = "icu_normalizer_data" 990 + version = "2.0.0" 991 + source = "registry+https://github.com/rust-lang/crates.io-index" 992 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 993 + 994 + [[package]] 995 + name = "icu_properties" 996 + version = "2.0.1" 997 + source = "registry+https://github.com/rust-lang/crates.io-index" 998 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 999 + dependencies = [ 1000 + "displaydoc", 1001 + "icu_collections", 1002 + "icu_locale_core", 1003 + "icu_properties_data", 1004 + "icu_provider", 1005 + "potential_utf", 1006 + "zerotrie", 1007 + "zerovec", 1008 + ] 1009 + 1010 + [[package]] 1011 + name = "icu_properties_data" 1012 + version = "2.0.1" 1013 + source = "registry+https://github.com/rust-lang/crates.io-index" 1014 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1015 + 1016 + [[package]] 1017 + name = "icu_provider" 1018 + version = "2.0.0" 1019 + source = "registry+https://github.com/rust-lang/crates.io-index" 1020 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1021 + dependencies = [ 1022 + "displaydoc", 1023 + "icu_locale_core", 1024 + "stable_deref_trait", 1025 + "tinystr", 1026 + "writeable", 1027 + "yoke", 1028 + "zerofrom", 1029 + "zerotrie", 1030 + "zerovec", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "idna" 1035 + version = "1.0.3" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1038 + dependencies = [ 1039 + "idna_adapter", 1040 + "smallvec", 1041 + "utf8_iter", 1042 + ] 1043 + 1044 + [[package]] 1045 + name = "idna_adapter" 1046 + version = "1.2.1" 1047 + source = "registry+https://github.com/rust-lang/crates.io-index" 1048 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1049 + dependencies = [ 1050 + "icu_normalizer", 1051 + "icu_properties", 1052 + ] 1053 + 1054 + [[package]] 1055 + name = "indexmap" 1056 + version = "2.10.0" 1057 + source = "registry+https://github.com/rust-lang/crates.io-index" 1058 + checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 1059 + dependencies = [ 1060 + "equivalent", 1061 + "hashbrown", 1062 + ] 1063 + 1064 + [[package]] 1065 + name = "inout" 1066 + version = "0.1.4" 1067 + source = "registry+https://github.com/rust-lang/crates.io-index" 1068 + checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 1069 + dependencies = [ 1070 + "generic-array", 1071 + ] 1072 + 1073 + [[package]] 1074 + name = "io-uring" 1075 + version = "0.7.9" 1076 + source = "registry+https://github.com/rust-lang/crates.io-index" 1077 + checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" 1078 + dependencies = [ 1079 + "bitflags", 1080 + "cfg-if", 1081 + "libc", 1082 + ] 1083 + 1084 + [[package]] 1085 + name = "ipnet" 1086 + version = "2.11.0" 1087 + source = "registry+https://github.com/rust-lang/crates.io-index" 1088 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1089 + 1090 + [[package]] 1091 + name = "iri-string" 1092 + version = "0.7.8" 1093 + source = "registry+https://github.com/rust-lang/crates.io-index" 1094 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1095 + dependencies = [ 1096 + "memchr", 1097 + "serde", 1098 + ] 1099 + 1100 + [[package]] 1101 + name = "itoa" 1102 + version = "1.0.15" 1103 + source = "registry+https://github.com/rust-lang/crates.io-index" 1104 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1105 + 1106 + [[package]] 1107 + name = "jobserver" 1108 + version = "0.1.33" 1109 + source = "registry+https://github.com/rust-lang/crates.io-index" 1110 + checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1111 + dependencies = [ 1112 + "getrandom 0.3.3", 1113 + "libc", 1114 + ] 1115 + 1116 + [[package]] 1117 + name = "js-sys" 1118 + version = "0.3.77" 1119 + source = "registry+https://github.com/rust-lang/crates.io-index" 1120 + checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1121 + dependencies = [ 1122 + "once_cell", 1123 + "wasm-bindgen", 1124 + ] 1125 + 1126 + [[package]] 1127 + name = "lazy_static" 1128 + version = "1.5.0" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1131 + dependencies = [ 1132 + "spin", 1133 + ] 1134 + 1135 + [[package]] 1136 + name = "libbz2-rs-sys" 1137 + version = "0.2.2" 1138 + source = "registry+https://github.com/rust-lang/crates.io-index" 1139 + checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" 1140 + 1141 + [[package]] 1142 + name = "libc" 1143 + version = "0.2.175" 1144 + source = "registry+https://github.com/rust-lang/crates.io-index" 1145 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1146 + 1147 + [[package]] 1148 + name = "liblzma" 1149 + version = "0.4.2" 1150 + source = "registry+https://github.com/rust-lang/crates.io-index" 1151 + checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8" 1152 + dependencies = [ 1153 + "liblzma-sys", 1154 + ] 1155 + 1156 + [[package]] 1157 + name = "liblzma-sys" 1158 + version = "0.4.4" 1159 + source = "registry+https://github.com/rust-lang/crates.io-index" 1160 + checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" 1161 + dependencies = [ 1162 + "cc", 1163 + "libc", 1164 + "pkg-config", 1165 + ] 1166 + 1167 + [[package]] 1168 + name = "libm" 1169 + version = "0.2.15" 1170 + source = "registry+https://github.com/rust-lang/crates.io-index" 1171 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1172 + 1173 + [[package]] 1174 + name = "libredox" 1175 + version = "0.1.9" 1176 + source = "registry+https://github.com/rust-lang/crates.io-index" 1177 + checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 1178 + dependencies = [ 1179 + "bitflags", 1180 + "libc", 1181 + "redox_syscall", 1182 + ] 1183 + 1184 + [[package]] 1185 + name = "libsqlite3-sys" 1186 + version = "0.30.1" 1187 + source = "registry+https://github.com/rust-lang/crates.io-index" 1188 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1189 + dependencies = [ 1190 + "pkg-config", 1191 + "vcpkg", 1192 + ] 1193 + 1194 + [[package]] 1195 + name = "libz-rs-sys" 1196 + version = "0.5.1" 1197 + source = "registry+https://github.com/rust-lang/crates.io-index" 1198 + checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" 1199 + dependencies = [ 1200 + "zlib-rs", 1201 + ] 1202 + 1203 + [[package]] 1204 + name = "linux-raw-sys" 1205 + version = "0.9.4" 1206 + source = "registry+https://github.com/rust-lang/crates.io-index" 1207 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1208 + 1209 + [[package]] 1210 + name = "litemap" 1211 + version = "0.8.0" 1212 + source = "registry+https://github.com/rust-lang/crates.io-index" 1213 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1214 + 1215 + [[package]] 1216 + name = "lock_api" 1217 + version = "0.4.13" 1218 + source = "registry+https://github.com/rust-lang/crates.io-index" 1219 + checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1220 + dependencies = [ 1221 + "autocfg", 1222 + "scopeguard", 1223 + ] 1224 + 1225 + [[package]] 1226 + name = "log" 1227 + version = "0.4.27" 1228 + source = "registry+https://github.com/rust-lang/crates.io-index" 1229 + checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1230 + 1231 + [[package]] 1232 + name = "matchers" 1233 + version = "0.1.0" 1234 + source = "registry+https://github.com/rust-lang/crates.io-index" 1235 + checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1236 + dependencies = [ 1237 + "regex-automata 0.1.10", 1238 + ] 1239 + 1240 + [[package]] 1241 + name = "matchit" 1242 + version = "0.7.3" 1243 + source = "registry+https://github.com/rust-lang/crates.io-index" 1244 + checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 1245 + 1246 + [[package]] 1247 + name = "md-5" 1248 + version = "0.10.6" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1251 + dependencies = [ 1252 + "cfg-if", 1253 + "digest", 1254 + ] 1255 + 1256 + [[package]] 1257 + name = "memchr" 1258 + version = "2.7.5" 1259 + source = "registry+https://github.com/rust-lang/crates.io-index" 1260 + checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1261 + 1262 + [[package]] 1263 + name = "memo-map" 1264 + version = "0.3.3" 1265 + source = "registry+https://github.com/rust-lang/crates.io-index" 1266 + checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" 1267 + 1268 + [[package]] 1269 + name = "mime" 1270 + version = "0.3.17" 1271 + source = "registry+https://github.com/rust-lang/crates.io-index" 1272 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1273 + 1274 + [[package]] 1275 + name = "minijinja" 1276 + version = "2.11.0" 1277 + source = "registry+https://github.com/rust-lang/crates.io-index" 1278 + checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" 1279 + dependencies = [ 1280 + "memo-map", 1281 + "self_cell", 1282 + "serde", 1283 + ] 1284 + 1285 + [[package]] 1286 + name = "miniz_oxide" 1287 + version = "0.8.9" 1288 + source = "registry+https://github.com/rust-lang/crates.io-index" 1289 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1290 + dependencies = [ 1291 + "adler2", 1292 + ] 1293 + 1294 + [[package]] 1295 + name = "mio" 1296 + version = "1.0.4" 1297 + source = "registry+https://github.com/rust-lang/crates.io-index" 1298 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1299 + dependencies = [ 1300 + "libc", 1301 + "wasi 0.11.1+wasi-snapshot-preview1", 1302 + "windows-sys 0.59.0", 1303 + ] 1304 + 1305 + [[package]] 1306 + name = "multer" 1307 + version = "3.1.0" 1308 + source = "registry+https://github.com/rust-lang/crates.io-index" 1309 + checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" 1310 + dependencies = [ 1311 + "bytes", 1312 + "encoding_rs", 1313 + "futures-util", 1314 + "http", 1315 + "httparse", 1316 + "memchr", 1317 + "mime", 1318 + "spin", 1319 + "version_check", 1320 + ] 1321 + 1322 + [[package]] 1323 + name = "native-tls" 1324 + version = "0.2.14" 1325 + source = "registry+https://github.com/rust-lang/crates.io-index" 1326 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1327 + dependencies = [ 1328 + "libc", 1329 + "log", 1330 + "openssl", 1331 + "openssl-probe", 1332 + "openssl-sys", 1333 + "schannel", 1334 + "security-framework", 1335 + "security-framework-sys", 1336 + "tempfile", 1337 + ] 1338 + 1339 + [[package]] 1340 + name = "nu-ansi-term" 1341 + version = "0.46.0" 1342 + source = "registry+https://github.com/rust-lang/crates.io-index" 1343 + checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1344 + dependencies = [ 1345 + "overload", 1346 + "winapi", 1347 + ] 1348 + 1349 + [[package]] 1350 + name = "num-bigint-dig" 1351 + version = "0.8.4" 1352 + source = "registry+https://github.com/rust-lang/crates.io-index" 1353 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 1354 + dependencies = [ 1355 + "byteorder", 1356 + "lazy_static", 1357 + "libm", 1358 + "num-integer", 1359 + "num-iter", 1360 + "num-traits", 1361 + "rand", 1362 + "smallvec", 1363 + "zeroize", 1364 + ] 1365 + 1366 + [[package]] 1367 + name = "num-conv" 1368 + version = "0.1.0" 1369 + source = "registry+https://github.com/rust-lang/crates.io-index" 1370 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1371 + 1372 + [[package]] 1373 + name = "num-integer" 1374 + version = "0.1.46" 1375 + source = "registry+https://github.com/rust-lang/crates.io-index" 1376 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1377 + dependencies = [ 1378 + "num-traits", 1379 + ] 1380 + 1381 + [[package]] 1382 + name = "num-iter" 1383 + version = "0.1.45" 1384 + source = "registry+https://github.com/rust-lang/crates.io-index" 1385 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1386 + dependencies = [ 1387 + "autocfg", 1388 + "num-integer", 1389 + "num-traits", 1390 + ] 1391 + 1392 + [[package]] 1393 + name = "num-traits" 1394 + version = "0.2.19" 1395 + source = "registry+https://github.com/rust-lang/crates.io-index" 1396 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1397 + dependencies = [ 1398 + "autocfg", 1399 + "libm", 1400 + ] 1401 + 1402 + [[package]] 1403 + name = "object" 1404 + version = "0.36.7" 1405 + source = "registry+https://github.com/rust-lang/crates.io-index" 1406 + checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1407 + dependencies = [ 1408 + "memchr", 1409 + ] 1410 + 1411 + [[package]] 1412 + name = "once_cell" 1413 + version = "1.21.3" 1414 + source = "registry+https://github.com/rust-lang/crates.io-index" 1415 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1416 + 1417 + [[package]] 1418 + name = "openssl" 1419 + version = "0.10.73" 1420 + source = "registry+https://github.com/rust-lang/crates.io-index" 1421 + checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1422 + dependencies = [ 1423 + "bitflags", 1424 + "cfg-if", 1425 + "foreign-types", 1426 + "libc", 1427 + "once_cell", 1428 + "openssl-macros", 1429 + "openssl-sys", 1430 + ] 1431 + 1432 + [[package]] 1433 + name = "openssl-macros" 1434 + version = "0.1.1" 1435 + source = "registry+https://github.com/rust-lang/crates.io-index" 1436 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1437 + dependencies = [ 1438 + "proc-macro2", 1439 + "quote", 1440 + "syn", 1441 + ] 1442 + 1443 + [[package]] 1444 + name = "openssl-probe" 1445 + version = "0.1.6" 1446 + source = "registry+https://github.com/rust-lang/crates.io-index" 1447 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1448 + 1449 + [[package]] 1450 + name = "openssl-sys" 1451 + version = "0.9.109" 1452 + source = "registry+https://github.com/rust-lang/crates.io-index" 1453 + checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1454 + dependencies = [ 1455 + "cc", 1456 + "libc", 1457 + "pkg-config", 1458 + "vcpkg", 1459 + ] 1460 + 1461 + [[package]] 1462 + name = "overload" 1463 + version = "0.1.1" 1464 + source = "registry+https://github.com/rust-lang/crates.io-index" 1465 + checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1466 + 1467 + [[package]] 1468 + name = "parking" 1469 + version = "2.2.1" 1470 + source = "registry+https://github.com/rust-lang/crates.io-index" 1471 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1472 + 1473 + [[package]] 1474 + name = "parking_lot" 1475 + version = "0.12.4" 1476 + source = "registry+https://github.com/rust-lang/crates.io-index" 1477 + checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1478 + dependencies = [ 1479 + "lock_api", 1480 + "parking_lot_core", 1481 + ] 1482 + 1483 + [[package]] 1484 + name = "parking_lot_core" 1485 + version = "0.9.11" 1486 + source = "registry+https://github.com/rust-lang/crates.io-index" 1487 + checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1488 + dependencies = [ 1489 + "cfg-if", 1490 + "libc", 1491 + "redox_syscall", 1492 + "smallvec", 1493 + "windows-targets 0.52.6", 1494 + ] 1495 + 1496 + [[package]] 1497 + name = "pbkdf2" 1498 + version = "0.12.2" 1499 + source = "registry+https://github.com/rust-lang/crates.io-index" 1500 + checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 1501 + dependencies = [ 1502 + "digest", 1503 + "hmac", 1504 + ] 1505 + 1506 + [[package]] 1507 + name = "pem-rfc7468" 1508 + version = "0.7.0" 1509 + source = "registry+https://github.com/rust-lang/crates.io-index" 1510 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1511 + dependencies = [ 1512 + "base64ct", 1513 + ] 1514 + 1515 + [[package]] 1516 + name = "percent-encoding" 1517 + version = "2.3.1" 1518 + source = "registry+https://github.com/rust-lang/crates.io-index" 1519 + checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1520 + 1521 + [[package]] 1522 + name = "pin-project-lite" 1523 + version = "0.2.16" 1524 + source = "registry+https://github.com/rust-lang/crates.io-index" 1525 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1526 + 1527 + [[package]] 1528 + name = "pin-utils" 1529 + version = "0.1.0" 1530 + source = "registry+https://github.com/rust-lang/crates.io-index" 1531 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1532 + 1533 + [[package]] 1534 + name = "pkcs1" 1535 + version = "0.7.5" 1536 + source = "registry+https://github.com/rust-lang/crates.io-index" 1537 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1538 + dependencies = [ 1539 + "der", 1540 + "pkcs8", 1541 + "spki", 1542 + ] 1543 + 1544 + [[package]] 1545 + name = "pkcs8" 1546 + version = "0.10.2" 1547 + source = "registry+https://github.com/rust-lang/crates.io-index" 1548 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1549 + dependencies = [ 1550 + "der", 1551 + "spki", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "pkg-config" 1556 + version = "0.3.32" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1559 + 1560 + [[package]] 1561 + name = "potential_utf" 1562 + version = "0.1.2" 1563 + source = "registry+https://github.com/rust-lang/crates.io-index" 1564 + checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1565 + dependencies = [ 1566 + "zerovec", 1567 + ] 1568 + 1569 + [[package]] 1570 + name = "powerfmt" 1571 + version = "0.2.0" 1572 + source = "registry+https://github.com/rust-lang/crates.io-index" 1573 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1574 + 1575 + [[package]] 1576 + name = "ppmd-rust" 1577 + version = "1.2.1" 1578 + source = "registry+https://github.com/rust-lang/crates.io-index" 1579 + checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" 1580 + 1581 + [[package]] 1582 + name = "ppv-lite86" 1583 + version = "0.2.21" 1584 + source = "registry+https://github.com/rust-lang/crates.io-index" 1585 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1586 + dependencies = [ 1587 + "zerocopy", 1588 + ] 1589 + 1590 + [[package]] 1591 + name = "proc-macro2" 1592 + version = "1.0.97" 1593 + source = "registry+https://github.com/rust-lang/crates.io-index" 1594 + checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" 1595 + dependencies = [ 1596 + "unicode-ident", 1597 + ] 1598 + 1599 + [[package]] 1600 + name = "quote" 1601 + version = "1.0.40" 1602 + source = "registry+https://github.com/rust-lang/crates.io-index" 1603 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1604 + dependencies = [ 1605 + "proc-macro2", 1606 + ] 1607 + 1608 + [[package]] 1609 + name = "r-efi" 1610 + version = "5.3.0" 1611 + source = "registry+https://github.com/rust-lang/crates.io-index" 1612 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1613 + 1614 + [[package]] 1615 + name = "rand" 1616 + version = "0.8.5" 1617 + source = "registry+https://github.com/rust-lang/crates.io-index" 1618 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1619 + dependencies = [ 1620 + "libc", 1621 + "rand_chacha", 1622 + "rand_core", 1623 + ] 1624 + 1625 + [[package]] 1626 + name = "rand_chacha" 1627 + version = "0.3.1" 1628 + source = "registry+https://github.com/rust-lang/crates.io-index" 1629 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1630 + dependencies = [ 1631 + "ppv-lite86", 1632 + "rand_core", 1633 + ] 1634 + 1635 + [[package]] 1636 + name = "rand_core" 1637 + version = "0.6.4" 1638 + source = "registry+https://github.com/rust-lang/crates.io-index" 1639 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1640 + dependencies = [ 1641 + "getrandom 0.2.16", 1642 + ] 1643 + 1644 + [[package]] 1645 + name = "redox_syscall" 1646 + version = "0.5.17" 1647 + source = "registry+https://github.com/rust-lang/crates.io-index" 1648 + checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 1649 + dependencies = [ 1650 + "bitflags", 1651 + ] 1652 + 1653 + [[package]] 1654 + name = "regex" 1655 + version = "1.11.1" 1656 + source = "registry+https://github.com/rust-lang/crates.io-index" 1657 + checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1658 + dependencies = [ 1659 + "aho-corasick", 1660 + "memchr", 1661 + "regex-automata 0.4.9", 1662 + "regex-syntax 0.8.5", 1663 + ] 1664 + 1665 + [[package]] 1666 + name = "regex-automata" 1667 + version = "0.1.10" 1668 + source = "registry+https://github.com/rust-lang/crates.io-index" 1669 + checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1670 + dependencies = [ 1671 + "regex-syntax 0.6.29", 1672 + ] 1673 + 1674 + [[package]] 1675 + name = "regex-automata" 1676 + version = "0.4.9" 1677 + source = "registry+https://github.com/rust-lang/crates.io-index" 1678 + checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1679 + dependencies = [ 1680 + "aho-corasick", 1681 + "memchr", 1682 + "regex-syntax 0.8.5", 1683 + ] 1684 + 1685 + [[package]] 1686 + name = "regex-syntax" 1687 + version = "0.6.29" 1688 + source = "registry+https://github.com/rust-lang/crates.io-index" 1689 + checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1690 + 1691 + [[package]] 1692 + name = "regex-syntax" 1693 + version = "0.8.5" 1694 + source = "registry+https://github.com/rust-lang/crates.io-index" 1695 + checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1696 + 1697 + [[package]] 1698 + name = "reqwest" 1699 + version = "0.12.23" 1700 + source = "registry+https://github.com/rust-lang/crates.io-index" 1701 + checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 1702 + dependencies = [ 1703 + "base64", 1704 + "bytes", 1705 + "encoding_rs", 1706 + "futures-core", 1707 + "futures-util", 1708 + "h2", 1709 + "http", 1710 + "http-body", 1711 + "http-body-util", 1712 + "hyper", 1713 + "hyper-rustls", 1714 + "hyper-tls", 1715 + "hyper-util", 1716 + "js-sys", 1717 + "log", 1718 + "mime", 1719 + "native-tls", 1720 + "percent-encoding", 1721 + "pin-project-lite", 1722 + "rustls-pki-types", 1723 + "serde", 1724 + "serde_json", 1725 + "serde_urlencoded", 1726 + "sync_wrapper", 1727 + "tokio", 1728 + "tokio-native-tls", 1729 + "tokio-util", 1730 + "tower", 1731 + "tower-http", 1732 + "tower-service", 1733 + "url", 1734 + "wasm-bindgen", 1735 + "wasm-bindgen-futures", 1736 + "wasm-streams", 1737 + "web-sys", 1738 + ] 1739 + 1740 + [[package]] 1741 + name = "ring" 1742 + version = "0.17.14" 1743 + source = "registry+https://github.com/rust-lang/crates.io-index" 1744 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1745 + dependencies = [ 1746 + "cc", 1747 + "cfg-if", 1748 + "getrandom 0.2.16", 1749 + "libc", 1750 + "untrusted", 1751 + "windows-sys 0.52.0", 1752 + ] 1753 + 1754 + [[package]] 1755 + name = "rsa" 1756 + version = "0.9.8" 1757 + source = "registry+https://github.com/rust-lang/crates.io-index" 1758 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 1759 + dependencies = [ 1760 + "const-oid", 1761 + "digest", 1762 + "num-bigint-dig", 1763 + "num-integer", 1764 + "num-traits", 1765 + "pkcs1", 1766 + "pkcs8", 1767 + "rand_core", 1768 + "signature", 1769 + "spki", 1770 + "subtle", 1771 + "zeroize", 1772 + ] 1773 + 1774 + [[package]] 1775 + name = "rustc-demangle" 1776 + version = "0.1.26" 1777 + source = "registry+https://github.com/rust-lang/crates.io-index" 1778 + checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 1779 + 1780 + [[package]] 1781 + name = "rustix" 1782 + version = "1.0.8" 1783 + source = "registry+https://github.com/rust-lang/crates.io-index" 1784 + checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 1785 + dependencies = [ 1786 + "bitflags", 1787 + "errno", 1788 + "libc", 1789 + "linux-raw-sys", 1790 + "windows-sys 0.60.2", 1791 + ] 1792 + 1793 + [[package]] 1794 + name = "rustls" 1795 + version = "0.23.31" 1796 + source = "registry+https://github.com/rust-lang/crates.io-index" 1797 + checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 1798 + dependencies = [ 1799 + "once_cell", 1800 + "ring", 1801 + "rustls-pki-types", 1802 + "rustls-webpki", 1803 + "subtle", 1804 + "zeroize", 1805 + ] 1806 + 1807 + [[package]] 1808 + name = "rustls-pki-types" 1809 + version = "1.12.0" 1810 + source = "registry+https://github.com/rust-lang/crates.io-index" 1811 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1812 + dependencies = [ 1813 + "zeroize", 1814 + ] 1815 + 1816 + [[package]] 1817 + name = "rustls-webpki" 1818 + version = "0.103.4" 1819 + source = "registry+https://github.com/rust-lang/crates.io-index" 1820 + checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 1821 + dependencies = [ 1822 + "ring", 1823 + "rustls-pki-types", 1824 + "untrusted", 1825 + ] 1826 + 1827 + [[package]] 1828 + name = "rustversion" 1829 + version = "1.0.22" 1830 + source = "registry+https://github.com/rust-lang/crates.io-index" 1831 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1832 + 1833 + [[package]] 1834 + name = "ryu" 1835 + version = "1.0.20" 1836 + source = "registry+https://github.com/rust-lang/crates.io-index" 1837 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1838 + 1839 + [[package]] 1840 + name = "schannel" 1841 + version = "0.1.27" 1842 + source = "registry+https://github.com/rust-lang/crates.io-index" 1843 + checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1844 + dependencies = [ 1845 + "windows-sys 0.59.0", 1846 + ] 1847 + 1848 + [[package]] 1849 + name = "scopeguard" 1850 + version = "1.2.0" 1851 + source = "registry+https://github.com/rust-lang/crates.io-index" 1852 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1853 + 1854 + [[package]] 1855 + name = "security-framework" 1856 + version = "2.11.1" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1859 + dependencies = [ 1860 + "bitflags", 1861 + "core-foundation", 1862 + "core-foundation-sys", 1863 + "libc", 1864 + "security-framework-sys", 1865 + ] 1866 + 1867 + [[package]] 1868 + name = "security-framework-sys" 1869 + version = "2.14.0" 1870 + source = "registry+https://github.com/rust-lang/crates.io-index" 1871 + checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1872 + dependencies = [ 1873 + "core-foundation-sys", 1874 + "libc", 1875 + ] 1876 + 1877 + [[package]] 1878 + name = "self_cell" 1879 + version = "1.2.0" 1880 + source = "registry+https://github.com/rust-lang/crates.io-index" 1881 + checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" 1882 + 1883 + [[package]] 1884 + name = "serde" 1885 + version = "1.0.219" 1886 + source = "registry+https://github.com/rust-lang/crates.io-index" 1887 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1888 + dependencies = [ 1889 + "serde_derive", 1890 + ] 1891 + 1892 + [[package]] 1893 + name = "serde_derive" 1894 + version = "1.0.219" 1895 + source = "registry+https://github.com/rust-lang/crates.io-index" 1896 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1897 + dependencies = [ 1898 + "proc-macro2", 1899 + "quote", 1900 + "syn", 1901 + ] 1902 + 1903 + [[package]] 1904 + name = "serde_html_form" 1905 + version = "0.2.7" 1906 + source = "registry+https://github.com/rust-lang/crates.io-index" 1907 + checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" 1908 + dependencies = [ 1909 + "form_urlencoded", 1910 + "indexmap", 1911 + "itoa", 1912 + "ryu", 1913 + "serde", 1914 + ] 1915 + 1916 + [[package]] 1917 + name = "serde_json" 1918 + version = "1.0.142" 1919 + source = "registry+https://github.com/rust-lang/crates.io-index" 1920 + checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 1921 + dependencies = [ 1922 + "itoa", 1923 + "memchr", 1924 + "ryu", 1925 + "serde", 1926 + ] 1927 + 1928 + [[package]] 1929 + name = "serde_path_to_error" 1930 + version = "0.1.17" 1931 + source = "registry+https://github.com/rust-lang/crates.io-index" 1932 + checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 1933 + dependencies = [ 1934 + "itoa", 1935 + "serde", 1936 + ] 1937 + 1938 + [[package]] 1939 + name = "serde_urlencoded" 1940 + version = "0.7.1" 1941 + source = "registry+https://github.com/rust-lang/crates.io-index" 1942 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1943 + dependencies = [ 1944 + "form_urlencoded", 1945 + "itoa", 1946 + "ryu", 1947 + "serde", 1948 + ] 1949 + 1950 + [[package]] 1951 + name = "sha1" 1952 + version = "0.10.6" 1953 + source = "registry+https://github.com/rust-lang/crates.io-index" 1954 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1955 + dependencies = [ 1956 + "cfg-if", 1957 + "cpufeatures", 1958 + "digest", 1959 + ] 1960 + 1961 + [[package]] 1962 + name = "sha2" 1963 + version = "0.10.9" 1964 + source = "registry+https://github.com/rust-lang/crates.io-index" 1965 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1966 + dependencies = [ 1967 + "cfg-if", 1968 + "cpufeatures", 1969 + "digest", 1970 + ] 1971 + 1972 + [[package]] 1973 + name = "sharded-slab" 1974 + version = "0.1.7" 1975 + source = "registry+https://github.com/rust-lang/crates.io-index" 1976 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1977 + dependencies = [ 1978 + "lazy_static", 1979 + ] 1980 + 1981 + [[package]] 1982 + name = "shlex" 1983 + version = "1.3.0" 1984 + source = "registry+https://github.com/rust-lang/crates.io-index" 1985 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1986 + 1987 + [[package]] 1988 + name = "signal-hook-registry" 1989 + version = "1.4.6" 1990 + source = "registry+https://github.com/rust-lang/crates.io-index" 1991 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 1992 + dependencies = [ 1993 + "libc", 1994 + ] 1995 + 1996 + [[package]] 1997 + name = "signature" 1998 + version = "2.2.0" 1999 + source = "registry+https://github.com/rust-lang/crates.io-index" 2000 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2001 + dependencies = [ 2002 + "digest", 2003 + "rand_core", 2004 + ] 2005 + 2006 + [[package]] 2007 + name = "simd-adler32" 2008 + version = "0.3.7" 2009 + source = "registry+https://github.com/rust-lang/crates.io-index" 2010 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 2011 + 2012 + [[package]] 2013 + name = "slab" 2014 + version = "0.4.11" 2015 + source = "registry+https://github.com/rust-lang/crates.io-index" 2016 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 2017 + 2018 + [[package]] 2019 + name = "slice" 2020 + version = "0.1.0" 2021 + dependencies = [ 2022 + "axum", 2023 + "axum-extra", 2024 + "chrono", 2025 + "dotenvy", 2026 + "futures-util", 2027 + "minijinja", 2028 + "multer", 2029 + "reqwest", 2030 + "serde", 2031 + "serde_json", 2032 + "sqlx", 2033 + "thiserror 1.0.69", 2034 + "tokio", 2035 + "tokio-tungstenite", 2036 + "tower", 2037 + "tower-http", 2038 + "tracing", 2039 + "tracing-subscriber", 2040 + "uuid", 2041 + "zip", 2042 + ] 2043 + 2044 + [[package]] 2045 + name = "smallvec" 2046 + version = "1.15.1" 2047 + source = "registry+https://github.com/rust-lang/crates.io-index" 2048 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2049 + dependencies = [ 2050 + "serde", 2051 + ] 2052 + 2053 + [[package]] 2054 + name = "socket2" 2055 + version = "0.6.0" 2056 + source = "registry+https://github.com/rust-lang/crates.io-index" 2057 + checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 2058 + dependencies = [ 2059 + "libc", 2060 + "windows-sys 0.59.0", 2061 + ] 2062 + 2063 + [[package]] 2064 + name = "spin" 2065 + version = "0.9.8" 2066 + source = "registry+https://github.com/rust-lang/crates.io-index" 2067 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2068 + dependencies = [ 2069 + "lock_api", 2070 + ] 2071 + 2072 + [[package]] 2073 + name = "spki" 2074 + version = "0.7.3" 2075 + source = "registry+https://github.com/rust-lang/crates.io-index" 2076 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2077 + dependencies = [ 2078 + "base64ct", 2079 + "der", 2080 + ] 2081 + 2082 + [[package]] 2083 + name = "sqlx" 2084 + version = "0.8.6" 2085 + source = "registry+https://github.com/rust-lang/crates.io-index" 2086 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 2087 + dependencies = [ 2088 + "sqlx-core", 2089 + "sqlx-macros", 2090 + "sqlx-mysql", 2091 + "sqlx-postgres", 2092 + "sqlx-sqlite", 2093 + ] 2094 + 2095 + [[package]] 2096 + name = "sqlx-core" 2097 + version = "0.8.6" 2098 + source = "registry+https://github.com/rust-lang/crates.io-index" 2099 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 2100 + dependencies = [ 2101 + "base64", 2102 + "bytes", 2103 + "chrono", 2104 + "crc", 2105 + "crossbeam-queue", 2106 + "either", 2107 + "event-listener", 2108 + "futures-core", 2109 + "futures-intrusive", 2110 + "futures-io", 2111 + "futures-util", 2112 + "hashbrown", 2113 + "hashlink", 2114 + "indexmap", 2115 + "log", 2116 + "memchr", 2117 + "once_cell", 2118 + "percent-encoding", 2119 + "rustls", 2120 + "serde", 2121 + "serde_json", 2122 + "sha2", 2123 + "smallvec", 2124 + "thiserror 2.0.14", 2125 + "tokio", 2126 + "tokio-stream", 2127 + "tracing", 2128 + "url", 2129 + "webpki-roots 0.26.11", 2130 + ] 2131 + 2132 + [[package]] 2133 + name = "sqlx-macros" 2134 + version = "0.8.6" 2135 + source = "registry+https://github.com/rust-lang/crates.io-index" 2136 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 2137 + dependencies = [ 2138 + "proc-macro2", 2139 + "quote", 2140 + "sqlx-core", 2141 + "sqlx-macros-core", 2142 + "syn", 2143 + ] 2144 + 2145 + [[package]] 2146 + name = "sqlx-macros-core" 2147 + version = "0.8.6" 2148 + source = "registry+https://github.com/rust-lang/crates.io-index" 2149 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 2150 + dependencies = [ 2151 + "dotenvy", 2152 + "either", 2153 + "heck", 2154 + "hex", 2155 + "once_cell", 2156 + "proc-macro2", 2157 + "quote", 2158 + "serde", 2159 + "serde_json", 2160 + "sha2", 2161 + "sqlx-core", 2162 + "sqlx-mysql", 2163 + "sqlx-postgres", 2164 + "sqlx-sqlite", 2165 + "syn", 2166 + "tokio", 2167 + "url", 2168 + ] 2169 + 2170 + [[package]] 2171 + name = "sqlx-mysql" 2172 + version = "0.8.6" 2173 + source = "registry+https://github.com/rust-lang/crates.io-index" 2174 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 2175 + dependencies = [ 2176 + "atoi", 2177 + "base64", 2178 + "bitflags", 2179 + "byteorder", 2180 + "bytes", 2181 + "chrono", 2182 + "crc", 2183 + "digest", 2184 + "dotenvy", 2185 + "either", 2186 + "futures-channel", 2187 + "futures-core", 2188 + "futures-io", 2189 + "futures-util", 2190 + "generic-array", 2191 + "hex", 2192 + "hkdf", 2193 + "hmac", 2194 + "itoa", 2195 + "log", 2196 + "md-5", 2197 + "memchr", 2198 + "once_cell", 2199 + "percent-encoding", 2200 + "rand", 2201 + "rsa", 2202 + "serde", 2203 + "sha1", 2204 + "sha2", 2205 + "smallvec", 2206 + "sqlx-core", 2207 + "stringprep", 2208 + "thiserror 2.0.14", 2209 + "tracing", 2210 + "whoami", 2211 + ] 2212 + 2213 + [[package]] 2214 + name = "sqlx-postgres" 2215 + version = "0.8.6" 2216 + source = "registry+https://github.com/rust-lang/crates.io-index" 2217 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 2218 + dependencies = [ 2219 + "atoi", 2220 + "base64", 2221 + "bitflags", 2222 + "byteorder", 2223 + "chrono", 2224 + "crc", 2225 + "dotenvy", 2226 + "etcetera", 2227 + "futures-channel", 2228 + "futures-core", 2229 + "futures-util", 2230 + "hex", 2231 + "hkdf", 2232 + "hmac", 2233 + "home", 2234 + "itoa", 2235 + "log", 2236 + "md-5", 2237 + "memchr", 2238 + "once_cell", 2239 + "rand", 2240 + "serde", 2241 + "serde_json", 2242 + "sha2", 2243 + "smallvec", 2244 + "sqlx-core", 2245 + "stringprep", 2246 + "thiserror 2.0.14", 2247 + "tracing", 2248 + "whoami", 2249 + ] 2250 + 2251 + [[package]] 2252 + name = "sqlx-sqlite" 2253 + version = "0.8.6" 2254 + source = "registry+https://github.com/rust-lang/crates.io-index" 2255 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 2256 + dependencies = [ 2257 + "atoi", 2258 + "chrono", 2259 + "flume", 2260 + "futures-channel", 2261 + "futures-core", 2262 + "futures-executor", 2263 + "futures-intrusive", 2264 + "futures-util", 2265 + "libsqlite3-sys", 2266 + "log", 2267 + "percent-encoding", 2268 + "serde", 2269 + "serde_urlencoded", 2270 + "sqlx-core", 2271 + "thiserror 2.0.14", 2272 + "tracing", 2273 + "url", 2274 + ] 2275 + 2276 + [[package]] 2277 + name = "stable_deref_trait" 2278 + version = "1.2.0" 2279 + source = "registry+https://github.com/rust-lang/crates.io-index" 2280 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2281 + 2282 + [[package]] 2283 + name = "stringprep" 2284 + version = "0.1.5" 2285 + source = "registry+https://github.com/rust-lang/crates.io-index" 2286 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 2287 + dependencies = [ 2288 + "unicode-bidi", 2289 + "unicode-normalization", 2290 + "unicode-properties", 2291 + ] 2292 + 2293 + [[package]] 2294 + name = "subtle" 2295 + version = "2.6.1" 2296 + source = "registry+https://github.com/rust-lang/crates.io-index" 2297 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2298 + 2299 + [[package]] 2300 + name = "syn" 2301 + version = "2.0.106" 2302 + source = "registry+https://github.com/rust-lang/crates.io-index" 2303 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 2304 + dependencies = [ 2305 + "proc-macro2", 2306 + "quote", 2307 + "unicode-ident", 2308 + ] 2309 + 2310 + [[package]] 2311 + name = "sync_wrapper" 2312 + version = "1.0.2" 2313 + source = "registry+https://github.com/rust-lang/crates.io-index" 2314 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2315 + dependencies = [ 2316 + "futures-core", 2317 + ] 2318 + 2319 + [[package]] 2320 + name = "synstructure" 2321 + version = "0.13.2" 2322 + source = "registry+https://github.com/rust-lang/crates.io-index" 2323 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2324 + dependencies = [ 2325 + "proc-macro2", 2326 + "quote", 2327 + "syn", 2328 + ] 2329 + 2330 + [[package]] 2331 + name = "system-configuration" 2332 + version = "0.6.1" 2333 + source = "registry+https://github.com/rust-lang/crates.io-index" 2334 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2335 + dependencies = [ 2336 + "bitflags", 2337 + "core-foundation", 2338 + "system-configuration-sys", 2339 + ] 2340 + 2341 + [[package]] 2342 + name = "system-configuration-sys" 2343 + version = "0.6.0" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2346 + dependencies = [ 2347 + "core-foundation-sys", 2348 + "libc", 2349 + ] 2350 + 2351 + [[package]] 2352 + name = "tempfile" 2353 + version = "3.20.0" 2354 + source = "registry+https://github.com/rust-lang/crates.io-index" 2355 + checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 2356 + dependencies = [ 2357 + "fastrand", 2358 + "getrandom 0.3.3", 2359 + "once_cell", 2360 + "rustix", 2361 + "windows-sys 0.59.0", 2362 + ] 2363 + 2364 + [[package]] 2365 + name = "thiserror" 2366 + version = "1.0.69" 2367 + source = "registry+https://github.com/rust-lang/crates.io-index" 2368 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2369 + dependencies = [ 2370 + "thiserror-impl 1.0.69", 2371 + ] 2372 + 2373 + [[package]] 2374 + name = "thiserror" 2375 + version = "2.0.14" 2376 + source = "registry+https://github.com/rust-lang/crates.io-index" 2377 + checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" 2378 + dependencies = [ 2379 + "thiserror-impl 2.0.14", 2380 + ] 2381 + 2382 + [[package]] 2383 + name = "thiserror-impl" 2384 + version = "1.0.69" 2385 + source = "registry+https://github.com/rust-lang/crates.io-index" 2386 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2387 + dependencies = [ 2388 + "proc-macro2", 2389 + "quote", 2390 + "syn", 2391 + ] 2392 + 2393 + [[package]] 2394 + name = "thiserror-impl" 2395 + version = "2.0.14" 2396 + source = "registry+https://github.com/rust-lang/crates.io-index" 2397 + checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" 2398 + dependencies = [ 2399 + "proc-macro2", 2400 + "quote", 2401 + "syn", 2402 + ] 2403 + 2404 + [[package]] 2405 + name = "thread_local" 2406 + version = "1.1.9" 2407 + source = "registry+https://github.com/rust-lang/crates.io-index" 2408 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2409 + dependencies = [ 2410 + "cfg-if", 2411 + ] 2412 + 2413 + [[package]] 2414 + name = "time" 2415 + version = "0.3.41" 2416 + source = "registry+https://github.com/rust-lang/crates.io-index" 2417 + checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 2418 + dependencies = [ 2419 + "deranged", 2420 + "num-conv", 2421 + "powerfmt", 2422 + "serde", 2423 + "time-core", 2424 + ] 2425 + 2426 + [[package]] 2427 + name = "time-core" 2428 + version = "0.1.4" 2429 + source = "registry+https://github.com/rust-lang/crates.io-index" 2430 + checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 2431 + 2432 + [[package]] 2433 + name = "tinystr" 2434 + version = "0.8.1" 2435 + source = "registry+https://github.com/rust-lang/crates.io-index" 2436 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2437 + dependencies = [ 2438 + "displaydoc", 2439 + "zerovec", 2440 + ] 2441 + 2442 + [[package]] 2443 + name = "tinyvec" 2444 + version = "1.9.0" 2445 + source = "registry+https://github.com/rust-lang/crates.io-index" 2446 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 2447 + dependencies = [ 2448 + "tinyvec_macros", 2449 + ] 2450 + 2451 + [[package]] 2452 + name = "tinyvec_macros" 2453 + version = "0.1.1" 2454 + source = "registry+https://github.com/rust-lang/crates.io-index" 2455 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2456 + 2457 + [[package]] 2458 + name = "tokio" 2459 + version = "1.47.1" 2460 + source = "registry+https://github.com/rust-lang/crates.io-index" 2461 + checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 2462 + dependencies = [ 2463 + "backtrace", 2464 + "bytes", 2465 + "io-uring", 2466 + "libc", 2467 + "mio", 2468 + "parking_lot", 2469 + "pin-project-lite", 2470 + "signal-hook-registry", 2471 + "slab", 2472 + "socket2", 2473 + "tokio-macros", 2474 + "windows-sys 0.59.0", 2475 + ] 2476 + 2477 + [[package]] 2478 + name = "tokio-macros" 2479 + version = "2.5.0" 2480 + source = "registry+https://github.com/rust-lang/crates.io-index" 2481 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2482 + dependencies = [ 2483 + "proc-macro2", 2484 + "quote", 2485 + "syn", 2486 + ] 2487 + 2488 + [[package]] 2489 + name = "tokio-native-tls" 2490 + version = "0.3.1" 2491 + source = "registry+https://github.com/rust-lang/crates.io-index" 2492 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2493 + dependencies = [ 2494 + "native-tls", 2495 + "tokio", 2496 + ] 2497 + 2498 + [[package]] 2499 + name = "tokio-rustls" 2500 + version = "0.26.2" 2501 + source = "registry+https://github.com/rust-lang/crates.io-index" 2502 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 2503 + dependencies = [ 2504 + "rustls", 2505 + "tokio", 2506 + ] 2507 + 2508 + [[package]] 2509 + name = "tokio-stream" 2510 + version = "0.1.17" 2511 + source = "registry+https://github.com/rust-lang/crates.io-index" 2512 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2513 + dependencies = [ 2514 + "futures-core", 2515 + "pin-project-lite", 2516 + "tokio", 2517 + ] 2518 + 2519 + [[package]] 2520 + name = "tokio-tungstenite" 2521 + version = "0.24.0" 2522 + source = "registry+https://github.com/rust-lang/crates.io-index" 2523 + checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" 2524 + dependencies = [ 2525 + "futures-util", 2526 + "log", 2527 + "tokio", 2528 + "tungstenite", 2529 + ] 2530 + 2531 + [[package]] 2532 + name = "tokio-util" 2533 + version = "0.7.16" 2534 + source = "registry+https://github.com/rust-lang/crates.io-index" 2535 + checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 2536 + dependencies = [ 2537 + "bytes", 2538 + "futures-core", 2539 + "futures-sink", 2540 + "pin-project-lite", 2541 + "tokio", 2542 + ] 2543 + 2544 + [[package]] 2545 + name = "tower" 2546 + version = "0.5.2" 2547 + source = "registry+https://github.com/rust-lang/crates.io-index" 2548 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2549 + dependencies = [ 2550 + "futures-core", 2551 + "futures-util", 2552 + "pin-project-lite", 2553 + "sync_wrapper", 2554 + "tokio", 2555 + "tower-layer", 2556 + "tower-service", 2557 + "tracing", 2558 + ] 2559 + 2560 + [[package]] 2561 + name = "tower-http" 2562 + version = "0.6.6" 2563 + source = "registry+https://github.com/rust-lang/crates.io-index" 2564 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2565 + dependencies = [ 2566 + "bitflags", 2567 + "bytes", 2568 + "futures-util", 2569 + "http", 2570 + "http-body", 2571 + "iri-string", 2572 + "pin-project-lite", 2573 + "tower", 2574 + "tower-layer", 2575 + "tower-service", 2576 + "tracing", 2577 + ] 2578 + 2579 + [[package]] 2580 + name = "tower-layer" 2581 + version = "0.3.3" 2582 + source = "registry+https://github.com/rust-lang/crates.io-index" 2583 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2584 + 2585 + [[package]] 2586 + name = "tower-service" 2587 + version = "0.3.3" 2588 + source = "registry+https://github.com/rust-lang/crates.io-index" 2589 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2590 + 2591 + [[package]] 2592 + name = "tracing" 2593 + version = "0.1.41" 2594 + source = "registry+https://github.com/rust-lang/crates.io-index" 2595 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2596 + dependencies = [ 2597 + "log", 2598 + "pin-project-lite", 2599 + "tracing-attributes", 2600 + "tracing-core", 2601 + ] 2602 + 2603 + [[package]] 2604 + name = "tracing-attributes" 2605 + version = "0.1.30" 2606 + source = "registry+https://github.com/rust-lang/crates.io-index" 2607 + checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 2608 + dependencies = [ 2609 + "proc-macro2", 2610 + "quote", 2611 + "syn", 2612 + ] 2613 + 2614 + [[package]] 2615 + name = "tracing-core" 2616 + version = "0.1.34" 2617 + source = "registry+https://github.com/rust-lang/crates.io-index" 2618 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2619 + dependencies = [ 2620 + "once_cell", 2621 + "valuable", 2622 + ] 2623 + 2624 + [[package]] 2625 + name = "tracing-log" 2626 + version = "0.2.0" 2627 + source = "registry+https://github.com/rust-lang/crates.io-index" 2628 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2629 + dependencies = [ 2630 + "log", 2631 + "once_cell", 2632 + "tracing-core", 2633 + ] 2634 + 2635 + [[package]] 2636 + name = "tracing-subscriber" 2637 + version = "0.3.19" 2638 + source = "registry+https://github.com/rust-lang/crates.io-index" 2639 + checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2640 + dependencies = [ 2641 + "matchers", 2642 + "nu-ansi-term", 2643 + "once_cell", 2644 + "regex", 2645 + "sharded-slab", 2646 + "smallvec", 2647 + "thread_local", 2648 + "tracing", 2649 + "tracing-core", 2650 + "tracing-log", 2651 + ] 2652 + 2653 + [[package]] 2654 + name = "try-lock" 2655 + version = "0.2.5" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2658 + 2659 + [[package]] 2660 + name = "tungstenite" 2661 + version = "0.24.0" 2662 + source = "registry+https://github.com/rust-lang/crates.io-index" 2663 + checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" 2664 + dependencies = [ 2665 + "byteorder", 2666 + "bytes", 2667 + "data-encoding", 2668 + "http", 2669 + "httparse", 2670 + "log", 2671 + "rand", 2672 + "sha1", 2673 + "thiserror 1.0.69", 2674 + "utf-8", 2675 + ] 2676 + 2677 + [[package]] 2678 + name = "typenum" 2679 + version = "1.18.0" 2680 + source = "registry+https://github.com/rust-lang/crates.io-index" 2681 + checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2682 + 2683 + [[package]] 2684 + name = "unicode-bidi" 2685 + version = "0.3.18" 2686 + source = "registry+https://github.com/rust-lang/crates.io-index" 2687 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 2688 + 2689 + [[package]] 2690 + name = "unicode-ident" 2691 + version = "1.0.18" 2692 + source = "registry+https://github.com/rust-lang/crates.io-index" 2693 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2694 + 2695 + [[package]] 2696 + name = "unicode-normalization" 2697 + version = "0.1.24" 2698 + source = "registry+https://github.com/rust-lang/crates.io-index" 2699 + checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 2700 + dependencies = [ 2701 + "tinyvec", 2702 + ] 2703 + 2704 + [[package]] 2705 + name = "unicode-properties" 2706 + version = "0.1.3" 2707 + source = "registry+https://github.com/rust-lang/crates.io-index" 2708 + checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 2709 + 2710 + [[package]] 2711 + name = "untrusted" 2712 + version = "0.9.0" 2713 + source = "registry+https://github.com/rust-lang/crates.io-index" 2714 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2715 + 2716 + [[package]] 2717 + name = "url" 2718 + version = "2.5.4" 2719 + source = "registry+https://github.com/rust-lang/crates.io-index" 2720 + checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2721 + dependencies = [ 2722 + "form_urlencoded", 2723 + "idna", 2724 + "percent-encoding", 2725 + ] 2726 + 2727 + [[package]] 2728 + name = "utf-8" 2729 + version = "0.7.6" 2730 + source = "registry+https://github.com/rust-lang/crates.io-index" 2731 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2732 + 2733 + [[package]] 2734 + name = "utf8_iter" 2735 + version = "1.0.4" 2736 + source = "registry+https://github.com/rust-lang/crates.io-index" 2737 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2738 + 2739 + [[package]] 2740 + name = "uuid" 2741 + version = "1.18.0" 2742 + source = "registry+https://github.com/rust-lang/crates.io-index" 2743 + checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" 2744 + dependencies = [ 2745 + "getrandom 0.3.3", 2746 + "js-sys", 2747 + "wasm-bindgen", 2748 + ] 2749 + 2750 + [[package]] 2751 + name = "valuable" 2752 + version = "0.1.1" 2753 + source = "registry+https://github.com/rust-lang/crates.io-index" 2754 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 2755 + 2756 + [[package]] 2757 + name = "vcpkg" 2758 + version = "0.2.15" 2759 + source = "registry+https://github.com/rust-lang/crates.io-index" 2760 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2761 + 2762 + [[package]] 2763 + name = "version_check" 2764 + version = "0.9.5" 2765 + source = "registry+https://github.com/rust-lang/crates.io-index" 2766 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2767 + 2768 + [[package]] 2769 + name = "want" 2770 + version = "0.3.1" 2771 + source = "registry+https://github.com/rust-lang/crates.io-index" 2772 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2773 + dependencies = [ 2774 + "try-lock", 2775 + ] 2776 + 2777 + [[package]] 2778 + name = "wasi" 2779 + version = "0.11.1+wasi-snapshot-preview1" 2780 + source = "registry+https://github.com/rust-lang/crates.io-index" 2781 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2782 + 2783 + [[package]] 2784 + name = "wasi" 2785 + version = "0.14.2+wasi-0.2.4" 2786 + source = "registry+https://github.com/rust-lang/crates.io-index" 2787 + checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2788 + dependencies = [ 2789 + "wit-bindgen-rt", 2790 + ] 2791 + 2792 + [[package]] 2793 + name = "wasite" 2794 + version = "0.1.0" 2795 + source = "registry+https://github.com/rust-lang/crates.io-index" 2796 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2797 + 2798 + [[package]] 2799 + name = "wasm-bindgen" 2800 + version = "0.2.100" 2801 + source = "registry+https://github.com/rust-lang/crates.io-index" 2802 + checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2803 + dependencies = [ 2804 + "cfg-if", 2805 + "once_cell", 2806 + "rustversion", 2807 + "wasm-bindgen-macro", 2808 + ] 2809 + 2810 + [[package]] 2811 + name = "wasm-bindgen-backend" 2812 + version = "0.2.100" 2813 + source = "registry+https://github.com/rust-lang/crates.io-index" 2814 + checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2815 + dependencies = [ 2816 + "bumpalo", 2817 + "log", 2818 + "proc-macro2", 2819 + "quote", 2820 + "syn", 2821 + "wasm-bindgen-shared", 2822 + ] 2823 + 2824 + [[package]] 2825 + name = "wasm-bindgen-futures" 2826 + version = "0.4.50" 2827 + source = "registry+https://github.com/rust-lang/crates.io-index" 2828 + checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2829 + dependencies = [ 2830 + "cfg-if", 2831 + "js-sys", 2832 + "once_cell", 2833 + "wasm-bindgen", 2834 + "web-sys", 2835 + ] 2836 + 2837 + [[package]] 2838 + name = "wasm-bindgen-macro" 2839 + version = "0.2.100" 2840 + source = "registry+https://github.com/rust-lang/crates.io-index" 2841 + checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2842 + dependencies = [ 2843 + "quote", 2844 + "wasm-bindgen-macro-support", 2845 + ] 2846 + 2847 + [[package]] 2848 + name = "wasm-bindgen-macro-support" 2849 + version = "0.2.100" 2850 + source = "registry+https://github.com/rust-lang/crates.io-index" 2851 + checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2852 + dependencies = [ 2853 + "proc-macro2", 2854 + "quote", 2855 + "syn", 2856 + "wasm-bindgen-backend", 2857 + "wasm-bindgen-shared", 2858 + ] 2859 + 2860 + [[package]] 2861 + name = "wasm-bindgen-shared" 2862 + version = "0.2.100" 2863 + source = "registry+https://github.com/rust-lang/crates.io-index" 2864 + checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2865 + dependencies = [ 2866 + "unicode-ident", 2867 + ] 2868 + 2869 + [[package]] 2870 + name = "wasm-streams" 2871 + version = "0.4.2" 2872 + source = "registry+https://github.com/rust-lang/crates.io-index" 2873 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 2874 + dependencies = [ 2875 + "futures-util", 2876 + "js-sys", 2877 + "wasm-bindgen", 2878 + "wasm-bindgen-futures", 2879 + "web-sys", 2880 + ] 2881 + 2882 + [[package]] 2883 + name = "web-sys" 2884 + version = "0.3.77" 2885 + source = "registry+https://github.com/rust-lang/crates.io-index" 2886 + checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2887 + dependencies = [ 2888 + "js-sys", 2889 + "wasm-bindgen", 2890 + ] 2891 + 2892 + [[package]] 2893 + name = "webpki-roots" 2894 + version = "0.26.11" 2895 + source = "registry+https://github.com/rust-lang/crates.io-index" 2896 + checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 2897 + dependencies = [ 2898 + "webpki-roots 1.0.2", 2899 + ] 2900 + 2901 + [[package]] 2902 + name = "webpki-roots" 2903 + version = "1.0.2" 2904 + source = "registry+https://github.com/rust-lang/crates.io-index" 2905 + checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 2906 + dependencies = [ 2907 + "rustls-pki-types", 2908 + ] 2909 + 2910 + [[package]] 2911 + name = "whoami" 2912 + version = "1.6.1" 2913 + source = "registry+https://github.com/rust-lang/crates.io-index" 2914 + checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 2915 + dependencies = [ 2916 + "libredox", 2917 + "wasite", 2918 + ] 2919 + 2920 + [[package]] 2921 + name = "winapi" 2922 + version = "0.3.9" 2923 + source = "registry+https://github.com/rust-lang/crates.io-index" 2924 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2925 + dependencies = [ 2926 + "winapi-i686-pc-windows-gnu", 2927 + "winapi-x86_64-pc-windows-gnu", 2928 + ] 2929 + 2930 + [[package]] 2931 + name = "winapi-i686-pc-windows-gnu" 2932 + version = "0.4.0" 2933 + source = "registry+https://github.com/rust-lang/crates.io-index" 2934 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2935 + 2936 + [[package]] 2937 + name = "winapi-x86_64-pc-windows-gnu" 2938 + version = "0.4.0" 2939 + source = "registry+https://github.com/rust-lang/crates.io-index" 2940 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2941 + 2942 + [[package]] 2943 + name = "windows-core" 2944 + version = "0.61.2" 2945 + source = "registry+https://github.com/rust-lang/crates.io-index" 2946 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 2947 + dependencies = [ 2948 + "windows-implement", 2949 + "windows-interface", 2950 + "windows-link", 2951 + "windows-result", 2952 + "windows-strings", 2953 + ] 2954 + 2955 + [[package]] 2956 + name = "windows-implement" 2957 + version = "0.60.0" 2958 + source = "registry+https://github.com/rust-lang/crates.io-index" 2959 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 2960 + dependencies = [ 2961 + "proc-macro2", 2962 + "quote", 2963 + "syn", 2964 + ] 2965 + 2966 + [[package]] 2967 + name = "windows-interface" 2968 + version = "0.59.1" 2969 + source = "registry+https://github.com/rust-lang/crates.io-index" 2970 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 2971 + dependencies = [ 2972 + "proc-macro2", 2973 + "quote", 2974 + "syn", 2975 + ] 2976 + 2977 + [[package]] 2978 + name = "windows-link" 2979 + version = "0.1.3" 2980 + source = "registry+https://github.com/rust-lang/crates.io-index" 2981 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2982 + 2983 + [[package]] 2984 + name = "windows-registry" 2985 + version = "0.5.3" 2986 + source = "registry+https://github.com/rust-lang/crates.io-index" 2987 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 2988 + dependencies = [ 2989 + "windows-link", 2990 + "windows-result", 2991 + "windows-strings", 2992 + ] 2993 + 2994 + [[package]] 2995 + name = "windows-result" 2996 + version = "0.3.4" 2997 + source = "registry+https://github.com/rust-lang/crates.io-index" 2998 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 2999 + dependencies = [ 3000 + "windows-link", 3001 + ] 3002 + 3003 + [[package]] 3004 + name = "windows-strings" 3005 + version = "0.4.2" 3006 + source = "registry+https://github.com/rust-lang/crates.io-index" 3007 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3008 + dependencies = [ 3009 + "windows-link", 3010 + ] 3011 + 3012 + [[package]] 3013 + name = "windows-sys" 3014 + version = "0.48.0" 3015 + source = "registry+https://github.com/rust-lang/crates.io-index" 3016 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3017 + dependencies = [ 3018 + "windows-targets 0.48.5", 3019 + ] 3020 + 3021 + [[package]] 3022 + name = "windows-sys" 3023 + version = "0.52.0" 3024 + source = "registry+https://github.com/rust-lang/crates.io-index" 3025 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3026 + dependencies = [ 3027 + "windows-targets 0.52.6", 3028 + ] 3029 + 3030 + [[package]] 3031 + name = "windows-sys" 3032 + version = "0.59.0" 3033 + source = "registry+https://github.com/rust-lang/crates.io-index" 3034 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3035 + dependencies = [ 3036 + "windows-targets 0.52.6", 3037 + ] 3038 + 3039 + [[package]] 3040 + name = "windows-sys" 3041 + version = "0.60.2" 3042 + source = "registry+https://github.com/rust-lang/crates.io-index" 3043 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3044 + dependencies = [ 3045 + "windows-targets 0.53.3", 3046 + ] 3047 + 3048 + [[package]] 3049 + name = "windows-targets" 3050 + version = "0.48.5" 3051 + source = "registry+https://github.com/rust-lang/crates.io-index" 3052 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3053 + dependencies = [ 3054 + "windows_aarch64_gnullvm 0.48.5", 3055 + "windows_aarch64_msvc 0.48.5", 3056 + "windows_i686_gnu 0.48.5", 3057 + "windows_i686_msvc 0.48.5", 3058 + "windows_x86_64_gnu 0.48.5", 3059 + "windows_x86_64_gnullvm 0.48.5", 3060 + "windows_x86_64_msvc 0.48.5", 3061 + ] 3062 + 3063 + [[package]] 3064 + name = "windows-targets" 3065 + version = "0.52.6" 3066 + source = "registry+https://github.com/rust-lang/crates.io-index" 3067 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3068 + dependencies = [ 3069 + "windows_aarch64_gnullvm 0.52.6", 3070 + "windows_aarch64_msvc 0.52.6", 3071 + "windows_i686_gnu 0.52.6", 3072 + "windows_i686_gnullvm 0.52.6", 3073 + "windows_i686_msvc 0.52.6", 3074 + "windows_x86_64_gnu 0.52.6", 3075 + "windows_x86_64_gnullvm 0.52.6", 3076 + "windows_x86_64_msvc 0.52.6", 3077 + ] 3078 + 3079 + [[package]] 3080 + name = "windows-targets" 3081 + version = "0.53.3" 3082 + source = "registry+https://github.com/rust-lang/crates.io-index" 3083 + checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 3084 + dependencies = [ 3085 + "windows-link", 3086 + "windows_aarch64_gnullvm 0.53.0", 3087 + "windows_aarch64_msvc 0.53.0", 3088 + "windows_i686_gnu 0.53.0", 3089 + "windows_i686_gnullvm 0.53.0", 3090 + "windows_i686_msvc 0.53.0", 3091 + "windows_x86_64_gnu 0.53.0", 3092 + "windows_x86_64_gnullvm 0.53.0", 3093 + "windows_x86_64_msvc 0.53.0", 3094 + ] 3095 + 3096 + [[package]] 3097 + name = "windows_aarch64_gnullvm" 3098 + version = "0.48.5" 3099 + source = "registry+https://github.com/rust-lang/crates.io-index" 3100 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3101 + 3102 + [[package]] 3103 + name = "windows_aarch64_gnullvm" 3104 + version = "0.52.6" 3105 + source = "registry+https://github.com/rust-lang/crates.io-index" 3106 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3107 + 3108 + [[package]] 3109 + name = "windows_aarch64_gnullvm" 3110 + version = "0.53.0" 3111 + source = "registry+https://github.com/rust-lang/crates.io-index" 3112 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 3113 + 3114 + [[package]] 3115 + name = "windows_aarch64_msvc" 3116 + version = "0.48.5" 3117 + source = "registry+https://github.com/rust-lang/crates.io-index" 3118 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3119 + 3120 + [[package]] 3121 + name = "windows_aarch64_msvc" 3122 + version = "0.52.6" 3123 + source = "registry+https://github.com/rust-lang/crates.io-index" 3124 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3125 + 3126 + [[package]] 3127 + name = "windows_aarch64_msvc" 3128 + version = "0.53.0" 3129 + source = "registry+https://github.com/rust-lang/crates.io-index" 3130 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 3131 + 3132 + [[package]] 3133 + name = "windows_i686_gnu" 3134 + version = "0.48.5" 3135 + source = "registry+https://github.com/rust-lang/crates.io-index" 3136 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3137 + 3138 + [[package]] 3139 + name = "windows_i686_gnu" 3140 + version = "0.52.6" 3141 + source = "registry+https://github.com/rust-lang/crates.io-index" 3142 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3143 + 3144 + [[package]] 3145 + name = "windows_i686_gnu" 3146 + version = "0.53.0" 3147 + source = "registry+https://github.com/rust-lang/crates.io-index" 3148 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 3149 + 3150 + [[package]] 3151 + name = "windows_i686_gnullvm" 3152 + version = "0.52.6" 3153 + source = "registry+https://github.com/rust-lang/crates.io-index" 3154 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3155 + 3156 + [[package]] 3157 + name = "windows_i686_gnullvm" 3158 + version = "0.53.0" 3159 + source = "registry+https://github.com/rust-lang/crates.io-index" 3160 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 3161 + 3162 + [[package]] 3163 + name = "windows_i686_msvc" 3164 + version = "0.48.5" 3165 + source = "registry+https://github.com/rust-lang/crates.io-index" 3166 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3167 + 3168 + [[package]] 3169 + name = "windows_i686_msvc" 3170 + version = "0.52.6" 3171 + source = "registry+https://github.com/rust-lang/crates.io-index" 3172 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3173 + 3174 + [[package]] 3175 + name = "windows_i686_msvc" 3176 + version = "0.53.0" 3177 + source = "registry+https://github.com/rust-lang/crates.io-index" 3178 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 3179 + 3180 + [[package]] 3181 + name = "windows_x86_64_gnu" 3182 + version = "0.48.5" 3183 + source = "registry+https://github.com/rust-lang/crates.io-index" 3184 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3185 + 3186 + [[package]] 3187 + name = "windows_x86_64_gnu" 3188 + version = "0.52.6" 3189 + source = "registry+https://github.com/rust-lang/crates.io-index" 3190 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3191 + 3192 + [[package]] 3193 + name = "windows_x86_64_gnu" 3194 + version = "0.53.0" 3195 + source = "registry+https://github.com/rust-lang/crates.io-index" 3196 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 3197 + 3198 + [[package]] 3199 + name = "windows_x86_64_gnullvm" 3200 + version = "0.48.5" 3201 + source = "registry+https://github.com/rust-lang/crates.io-index" 3202 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3203 + 3204 + [[package]] 3205 + name = "windows_x86_64_gnullvm" 3206 + version = "0.52.6" 3207 + source = "registry+https://github.com/rust-lang/crates.io-index" 3208 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3209 + 3210 + [[package]] 3211 + name = "windows_x86_64_gnullvm" 3212 + version = "0.53.0" 3213 + source = "registry+https://github.com/rust-lang/crates.io-index" 3214 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 3215 + 3216 + [[package]] 3217 + name = "windows_x86_64_msvc" 3218 + version = "0.48.5" 3219 + source = "registry+https://github.com/rust-lang/crates.io-index" 3220 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3221 + 3222 + [[package]] 3223 + name = "windows_x86_64_msvc" 3224 + version = "0.52.6" 3225 + source = "registry+https://github.com/rust-lang/crates.io-index" 3226 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3227 + 3228 + [[package]] 3229 + name = "windows_x86_64_msvc" 3230 + version = "0.53.0" 3231 + source = "registry+https://github.com/rust-lang/crates.io-index" 3232 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 3233 + 3234 + [[package]] 3235 + name = "wit-bindgen-rt" 3236 + version = "0.39.0" 3237 + source = "registry+https://github.com/rust-lang/crates.io-index" 3238 + checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 3239 + dependencies = [ 3240 + "bitflags", 3241 + ] 3242 + 3243 + [[package]] 3244 + name = "writeable" 3245 + version = "0.6.1" 3246 + source = "registry+https://github.com/rust-lang/crates.io-index" 3247 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 3248 + 3249 + [[package]] 3250 + name = "yoke" 3251 + version = "0.8.0" 3252 + source = "registry+https://github.com/rust-lang/crates.io-index" 3253 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 3254 + dependencies = [ 3255 + "serde", 3256 + "stable_deref_trait", 3257 + "yoke-derive", 3258 + "zerofrom", 3259 + ] 3260 + 3261 + [[package]] 3262 + name = "yoke-derive" 3263 + version = "0.8.0" 3264 + source = "registry+https://github.com/rust-lang/crates.io-index" 3265 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 3266 + dependencies = [ 3267 + "proc-macro2", 3268 + "quote", 3269 + "syn", 3270 + "synstructure", 3271 + ] 3272 + 3273 + [[package]] 3274 + name = "zerocopy" 3275 + version = "0.8.26" 3276 + source = "registry+https://github.com/rust-lang/crates.io-index" 3277 + checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 3278 + dependencies = [ 3279 + "zerocopy-derive", 3280 + ] 3281 + 3282 + [[package]] 3283 + name = "zerocopy-derive" 3284 + version = "0.8.26" 3285 + source = "registry+https://github.com/rust-lang/crates.io-index" 3286 + checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 3287 + dependencies = [ 3288 + "proc-macro2", 3289 + "quote", 3290 + "syn", 3291 + ] 3292 + 3293 + [[package]] 3294 + name = "zerofrom" 3295 + version = "0.1.6" 3296 + source = "registry+https://github.com/rust-lang/crates.io-index" 3297 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3298 + dependencies = [ 3299 + "zerofrom-derive", 3300 + ] 3301 + 3302 + [[package]] 3303 + name = "zerofrom-derive" 3304 + version = "0.1.6" 3305 + source = "registry+https://github.com/rust-lang/crates.io-index" 3306 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3307 + dependencies = [ 3308 + "proc-macro2", 3309 + "quote", 3310 + "syn", 3311 + "synstructure", 3312 + ] 3313 + 3314 + [[package]] 3315 + name = "zeroize" 3316 + version = "1.8.1" 3317 + source = "registry+https://github.com/rust-lang/crates.io-index" 3318 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3319 + dependencies = [ 3320 + "zeroize_derive", 3321 + ] 3322 + 3323 + [[package]] 3324 + name = "zeroize_derive" 3325 + version = "1.4.2" 3326 + source = "registry+https://github.com/rust-lang/crates.io-index" 3327 + checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 3328 + dependencies = [ 3329 + "proc-macro2", 3330 + "quote", 3331 + "syn", 3332 + ] 3333 + 3334 + [[package]] 3335 + name = "zerotrie" 3336 + version = "0.2.2" 3337 + source = "registry+https://github.com/rust-lang/crates.io-index" 3338 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 3339 + dependencies = [ 3340 + "displaydoc", 3341 + "yoke", 3342 + "zerofrom", 3343 + ] 3344 + 3345 + [[package]] 3346 + name = "zerovec" 3347 + version = "0.11.4" 3348 + source = "registry+https://github.com/rust-lang/crates.io-index" 3349 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 3350 + dependencies = [ 3351 + "yoke", 3352 + "zerofrom", 3353 + "zerovec-derive", 3354 + ] 3355 + 3356 + [[package]] 3357 + name = "zerovec-derive" 3358 + version = "0.11.1" 3359 + source = "registry+https://github.com/rust-lang/crates.io-index" 3360 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 3361 + dependencies = [ 3362 + "proc-macro2", 3363 + "quote", 3364 + "syn", 3365 + ] 3366 + 3367 + [[package]] 3368 + name = "zip" 3369 + version = "4.3.0" 3370 + source = "registry+https://github.com/rust-lang/crates.io-index" 3371 + checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b" 3372 + dependencies = [ 3373 + "aes", 3374 + "arbitrary", 3375 + "bzip2", 3376 + "constant_time_eq", 3377 + "crc32fast", 3378 + "deflate64", 3379 + "flate2", 3380 + "getrandom 0.3.3", 3381 + "hmac", 3382 + "indexmap", 3383 + "liblzma", 3384 + "memchr", 3385 + "pbkdf2", 3386 + "ppmd-rust", 3387 + "sha1", 3388 + "time", 3389 + "zeroize", 3390 + "zopfli", 3391 + "zstd", 3392 + ] 3393 + 3394 + [[package]] 3395 + name = "zlib-rs" 3396 + version = "0.5.1" 3397 + source = "registry+https://github.com/rust-lang/crates.io-index" 3398 + checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" 3399 + 3400 + [[package]] 3401 + name = "zopfli" 3402 + version = "0.8.2" 3403 + source = "registry+https://github.com/rust-lang/crates.io-index" 3404 + checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" 3405 + dependencies = [ 3406 + "bumpalo", 3407 + "crc32fast", 3408 + "log", 3409 + "simd-adler32", 3410 + ] 3411 + 3412 + [[package]] 3413 + name = "zstd" 3414 + version = "0.13.3" 3415 + source = "registry+https://github.com/rust-lang/crates.io-index" 3416 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 3417 + dependencies = [ 3418 + "zstd-safe", 3419 + ] 3420 + 3421 + [[package]] 3422 + name = "zstd-safe" 3423 + version = "7.2.4" 3424 + source = "registry+https://github.com/rust-lang/crates.io-index" 3425 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 3426 + dependencies = [ 3427 + "zstd-sys", 3428 + ] 3429 + 3430 + [[package]] 3431 + name = "zstd-sys" 3432 + version = "2.0.15+zstd.1.5.7" 3433 + source = "registry+https://github.com/rust-lang/crates.io-index" 3434 + checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 3435 + dependencies = [ 3436 + "cc", 3437 + "pkg-config", 3438 + ]
+50
api/Cargo.toml
··· 1 + [package] 2 + name = "slice" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + # Core async runtime 8 + tokio = { version = "1.0", features = ["full"] } 9 + 10 + # Database and ORM 11 + sqlx = { version = "0.8", features = ["postgres", "chrono", "json", "runtime-tokio-rustls", "migrate"] } 12 + 13 + # Serialization 14 + serde = { version = "1.0", features = ["derive"] } 15 + serde_json = "1.0" 16 + 17 + # HTTP client and server 18 + reqwest = { version = "0.12", features = ["json", "stream"] } 19 + axum = { version = "0.7", features = ["ws", "macros"] } 20 + axum-extra = { version = "0.9", features = ["form"] } 21 + tower = "0.5" 22 + tower-http = { version = "0.6", features = ["cors", "trace"] } 23 + 24 + 25 + # WebSocket for firehose 26 + tokio-tungstenite = "0.24" 27 + 28 + # Error handling 29 + thiserror = "1.0" 30 + 31 + # Logging and tracing 32 + tracing = "0.1" 33 + tracing-subscriber = { version = "0.3", features = ["env-filter"] } 34 + 35 + # Template engine for web interface 36 + minijinja = { version = "2.0", features = ["loader"] } 37 + 38 + # Time handling 39 + chrono = { version = "0.4", features = ["serde"] } 40 + 41 + # UUID generation 42 + uuid = { version = "1.0", features = ["v4"] } 43 + 44 + # Environment variables 45 + dotenvy = "0.15" 46 + 47 + # File upload and zip handling 48 + zip = "4.3" 49 + multer = "3.1" 50 + futures-util = "0.3"
+91
api/README.md
··· 1 + # Slice - AT Protocol Indexer 2 + 3 + A Rust-based AT Protocol indexer service with HTMX web interface for syncing and viewing AT Protocol records. 4 + 5 + ## Features 6 + 7 + - 📚 **Bulk Collection Sync**: Efficiently sync entire AT Protocol collections 8 + - 🔄 **Smart Discovery**: Automatically find repositories with target collections 9 + - 🌐 **Web Interface**: HTMX-powered UI for easy bulk operations 10 + - 🚀 **XRPC API**: Native AT Protocol XRPC endpoints 11 + - 🗄️ **PostgreSQL Storage**: Efficient JSONB storage with smart indexing 12 + 13 + ## Quick Start 14 + 15 + ### Prerequisites 16 + 17 + - Rust 1.70+ 18 + - PostgreSQL 12+ 19 + 20 + ### Setup 21 + 22 + 1. **Clone and setup**: 23 + ```bash 24 + git clone <repo> 25 + cd slice 26 + ``` 27 + 28 + 2. **Database setup**: 29 + ```bash 30 + createdb slice 31 + export DATABASE_URL="postgresql://localhost/slice" 32 + ``` 33 + 34 + 3. **Run the server**: 35 + ```bash 36 + cargo run 37 + ``` 38 + 39 + 4. **Open web interface**: http://127.0.0.1:3000 40 + 41 + ## Usage 42 + 43 + ### Web Interface 44 + 45 + - **Home**: Overview and quick links 46 + - **Records**: Browse indexed records by collection 47 + - **Sync**: Manually sync individual records 48 + 49 + ### API Endpoints 50 + 51 + - `GET /xrpc/com.indexer.records.list?collection=app.bsky.feed.post` - List records 52 + - `POST /xrpc/com.indexer.collections.bulkSync` - Bulk sync collections 53 + 54 + ### Example: Bulk Sync Collections 55 + 56 + ```bash 57 + curl -X POST "http://127.0.0.1:3000/xrpc/com.indexer.collections.bulkSync" \ 58 + -H "Content-Type: application/json" \ 59 + -d '{"collections": ["app.bsky.feed.post", "app.bsky.actor.profile"]}' 60 + ``` 61 + 62 + ### Popular Collections to Sync 63 + 64 + ``` 65 + app.bsky.feed.post # Bluesky posts 66 + app.bsky.actor.profile # User profiles 67 + app.bsky.feed.like # Likes 68 + app.bsky.feed.repost # Reposts 69 + app.bsky.graph.follow # Follows 70 + ``` 71 + 72 + ## Architecture 73 + 74 + Built following the [AT Protocol Indexer Specification](docs/atproto_indexer_spec.md): 75 + 76 + - **Single Table Design**: All records in one `record` table with JSONB for flexibility 77 + - **Smart Syncing**: Hybrid approach supporting both individual record fetch and bulk operations 78 + - **Future CAR Support**: Architecture ready for CAR file import for efficient bulk syncing 79 + 80 + ## Development 81 + 82 + ```bash 83 + # Run with auto-reload 84 + cargo watch -x run 85 + 86 + # Run tests 87 + cargo test 88 + 89 + # Check code 90 + cargo clippy 91 + ```
+22
api/docker-compose.yml
··· 1 + version: '3.8' 2 + 3 + services: 4 + postgres: 5 + image: postgres:15 6 + environment: 7 + POSTGRES_DB: slice 8 + POSTGRES_USER: slice 9 + POSTGRES_PASSWORD: slice 10 + ports: 11 + - "5432:5432" 12 + volumes: 13 + - postgres_data:/var/lib/postgresql/data 14 + - ./schema.sql:/docker-entrypoint-initdb.d/01-schema.sql 15 + healthcheck: 16 + test: ["CMD-SHELL", "pg_isready -U slice -d slice"] 17 + interval: 5s 18 + timeout: 5s 19 + retries: 5 20 + 21 + volumes: 22 + postgres_data:
+907
api/docs/atproto_indexer_spec.md
··· 1 + # AT Protocol Indexing Service - Technical Specification 2 + 3 + ## Project Overview 4 + 5 + Build a high-performance, scalable indexing service for AT Protocol that 6 + automatically generates typed APIs for any lexicon, with intelligent data 7 + fetching strategies and real-time synchronization. 8 + 9 + ### Core Goals 10 + 11 + - **Universal Lexicon Support**: Automatically handle any AT Protocol lexicon 12 + without manual configuration 13 + - **Multi-Language Client Generation**: Generate typed API clients for 14 + TypeScript, Rust, Python, Go, etc. 15 + - **High Performance**: Handle millions of records efficiently with smart 16 + caching and batching 17 + - **Real-time Sync**: Support both bulk imports and live firehose updates 18 + - **Developer Experience**: Hasura-style auto-generated APIs with full type 19 + safety 20 + 21 + ## Architecture Overview 22 + 23 + ### Data Storage Strategy 24 + 25 + **Primary Database: PostgreSQL** 26 + 27 + - Single source of truth for all indexed records 28 + - Single table approach for maximum flexibility across arbitrary lexicons 29 + - JSONB for complete record storage and sophisticated querying 30 + - Optional partitioning by collection for very high volume deployments 31 + 32 + ```sql 33 + -- Single table for all AT Protocol records 34 + CREATE TABLE IF NOT EXISTS "record" ( 35 + "uri" TEXT PRIMARY KEY NOT NULL, 36 + "cid" TEXT NOT NULL, 37 + "did" TEXT NOT NULL, 38 + "collection" TEXT NOT NULL, 39 + "json" JSONB NOT NULL, -- Use JSONB for performance and querying 40 + "indexedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 41 + ); 42 + 43 + -- Essential indexes for performance 44 + CREATE INDEX IF NOT EXISTS idx_record_collection ON "record"("collection"); 45 + CREATE INDEX IF NOT EXISTS idx_record_did ON "record"("did"); 46 + CREATE INDEX IF NOT EXISTS idx_record_indexed_at ON "record"("indexedAt"); 47 + CREATE INDEX IF NOT EXISTS idx_record_json_gin ON "record" USING GIN("json"); 48 + 49 + -- Collection-specific indexes for common queries 50 + CREATE INDEX IF NOT EXISTS idx_record_collection_did ON "record"("collection", "did"); 51 + CREATE INDEX IF NOT EXISTS idx_record_cid ON "record"("cid"); 52 + ``` 53 + 54 + **Caching Strategy** 55 + 56 + - **Redis**: Hot data caching, query result caching, rate limiting 57 + - **Application-level**: Compiled lexicon handlers, parsed schemas 58 + - **CDN**: Public API endpoints with appropriate cache headers 59 + 60 + **PostgreSQL JSONB Advantages** 61 + 62 + - **GIN indexes**: Fast querying on JSON content with `@>`, `?`, `?&`, `?|` 63 + operators 64 + - **JSON operators**: Rich querying with `->`, `->>`, `#>`, `#>>` for nested 65 + access 66 + - **JSON path queries**: Complex nested field access and filtering 67 + - **Performance**: JSONB stored in optimized binary format for fast access 68 + - **Flexibility**: Handle arbitrary lexicon schemas without schema migrations 69 + 70 + ### Search Implementation 71 + 72 + **Hybrid Approach**: 73 + 74 + - **PostgreSQL**: Primary queries, exact matches, admin operations, complex 75 + joins 76 + - **Optional Search Engine**: User-facing search, fuzzy matching, aggregations, 77 + analytics 78 + 79 + **Search Engine Options**: 80 + 81 + - **Typesense**: Easy setup, good performance for smaller deployments 82 + - **Meilisearch**: Excellent for instant search experiences 83 + - **Elasticsearch/OpenSearch**: Full-featured for large-scale deployments 84 + 85 + ## Record Fetching Strategies 86 + 87 + ### Decision Matrix 88 + 89 + | Scenario | Strategy | Reasoning | 90 + | -------------------- | ----------------------- | -------------------------------------------- | 91 + | Initial sync | CAR file download | Most efficient for bulk data | 92 + | Real-time updates | Firehose stream | Live updates as they happen | 93 + | Catch-up sync (<24h) | List + individual fetch | Good for small gaps | 94 + | Catch-up sync (>24h) | CAR file re-download | More efficient than many individual requests | 95 + | Single record update | Individual fetch | Targeted and fast | 96 + 97 + ### Implementation Strategy 98 + 99 + ```rust 100 + async fn smart_sync(&self, did: &str) -> Result<()> { 101 + let last_sync = self.get_last_sync_time(did).await?; 102 + 103 + match last_sync { 104 + None => self.sync_repo_car(did).await?, // Initial: CAR file 105 + Some(last) if Utc::now() - last > Duration::hours(24) => { 106 + self.sync_repo_car(did).await? // Full resync: CAR file 107 + } 108 + Some(last) => { 109 + self.incremental_sync(did, last).await? // Incremental: List + fetch 110 + } 111 + } 112 + 113 + Ok(()) 114 + } 115 + ``` 116 + 117 + ## Dynamic Lexicon System 118 + 119 + ### Why Single Table Works Better for AT Protocol 120 + 121 + **Lexicon characteristics that favor single table:** 122 + 123 + - **Runtime schema definition**: Lexicons can be arbitrary and defined by any 124 + developer 125 + - **Shared metadata**: All records have common fields (CID, timestamp, author, 126 + etc.) 127 + - **Flexible querying**: Query across different record types seamlessly 128 + - **Unknown schema count**: Could have hundreds of different lexicons 129 + 130 + ### Unified Query Interface 131 + 132 + **Cross-lexicon querying capabilities:** 133 + 134 + ```sql 135 + -- Posts with specific hashtags 136 + SELECT * FROM "record" 137 + WHERE "collection" = 'app.bsky.feed.post' 138 + AND "json"->>'text' ILIKE '%#atproto%'; 139 + 140 + -- All records by author across all lexicons 141 + SELECT "collection", COUNT(*) FROM "record" 142 + WHERE "did" = 'did:plc:example' 143 + GROUP BY "collection"; 144 + 145 + -- Cross-lexicon search for any record with text content 146 + SELECT * FROM "record" 147 + WHERE "json" ? 'text' 148 + AND "json"->>'text' ILIKE '%search term%'; 149 + 150 + -- Recent records across all collections 151 + SELECT "uri", "collection", "json"->>'$type' as record_type, "indexedAt" 152 + FROM "record" 153 + WHERE "indexedAt" > NOW() - INTERVAL '24 hours' 154 + ORDER BY "indexedAt" DESC; 155 + ``` 156 + 157 + ### Schema Management 158 + 159 + **Components**: 160 + 161 + 1. **Lexicon Registry**: Parse and store lexicon definitions for validation 162 + 2. **Indexer Lexicons**: Define the indexer's own XRPC procedures with proper 163 + lexicons 164 + 3. **Validation Layer**: Ensure records conform to their lexicon schemas 165 + 4. **XRPC Server**: Serve both indexed AT Protocol data and indexer's own 166 + procedures 167 + 5. **Type Generator**: Generate typed interfaces for all lexicons (AT Protocol + 168 + indexer) 169 + 170 + ### Dynamic Index Creation 171 + 172 + ```sql 173 + -- Add lexicon-specific indexes as needed for performance 174 + CREATE INDEX IF NOT EXISTS idx_posts_text ON "record" USING GIN(("json"->'text')) 175 + WHERE "collection" = 'app.bsky.feed.post'; 176 + 177 + CREATE INDEX IF NOT EXISTS idx_profiles_handle ON "record"(("json"->>'handle')) 178 + WHERE "collection" = 'app.bsky.actor.profile'; 179 + 180 + -- For very high volume, consider partitioning by collection 181 + CREATE TABLE "record_posts" PARTITION OF "record" 182 + FOR VALUES IN ('app.bsky.feed.post'); 183 + 184 + -- Composite indexes for common query patterns 185 + CREATE INDEX IF NOT EXISTS idx_record_collection_created_at ON "record"("collection", ("json"->>'createdAt')) 186 + WHERE "json" ? 'createdAt'; 187 + ``` 188 + 189 + ### Implementation Strategy 190 + 191 + ```rust 192 + async fn register_lexicon(lexicon: LexiconDoc) -> Result<()> { 193 + // 1. Store lexicon definition for validation 194 + self.store_lexicon_schema(lexicon).await?; 195 + 196 + // 2. Create collection-specific indexes if needed 197 + self.create_performance_indexes(&lexicon.id).await?; 198 + 199 + // 3. Register XRPC handlers for core AT Protocol lexicons 200 + if lexicon.id.starts_with("com.atproto.") { 201 + self.register_atproto_handlers(&lexicon.id).await?; 202 + } 203 + 204 + // 4. Generate TypeScript types for all lexicons (AT Protocol + indexer) 205 + self.generate_client_types(&lexicon.id).await?; 206 + 207 + Ok(()) 208 + } 209 + 210 + async fn initialize_indexer_lexicons(&self) -> Result<()> { 211 + // Define and register the indexer's own XRPC procedures 212 + let indexer_lexicons = vec![ 213 + self.create_list_records_lexicon(), 214 + self.create_search_records_lexicon(), 215 + self.create_get_record_lexicon(), 216 + // ... other indexer procedures 217 + ]; 218 + 219 + for lexicon in indexer_lexicons { 220 + self.register_indexer_procedure(lexicon).await?; 221 + } 222 + 223 + Ok(()) 224 + } 225 + ``` 226 + 227 + ### Record Validation 228 + 229 + **Validation Layer**: Ensure data integrity with lexicon schema validation 230 + 231 + ```rust 232 + async fn insert_record(&self, record: ATProtoRecord) -> Result<()> { 233 + // 1. Validate against lexicon schema 234 + let lexicon = self.get_lexicon_schema(&record.collection).await?; 235 + self.validate_record_against_schema(&record.json, &lexicon)?; 236 + 237 + // 2. Insert with proper indexing 238 + sqlx::query!( 239 + r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 240 + VALUES ($1, $2, $3, $4, $5, $6) 241 + ON CONFLICT ("uri") 242 + DO UPDATE SET 243 + "cid" = EXCLUDED."cid", 244 + "json" = EXCLUDED."json", 245 + "indexedAt" = EXCLUDED."indexedAt""#, 246 + record.uri, 247 + record.cid, 248 + record.did, 249 + record.collection, 250 + record.json, 251 + record.indexed_at 252 + ).execute(&self.db).await?; 253 + 254 + Ok(()) 255 + } 256 + 257 + // Batch processing for CAR file imports 258 + async fn batch_insert_records(&self, records: &[ATProtoRecord]) -> Result<()> { 259 + let mut tx = self.db.begin().await?; 260 + 261 + for record in records { 262 + sqlx::query!( 263 + r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 264 + VALUES ($1, $2, $3, $4, $5, $6) 265 + ON CONFLICT ("uri") 266 + DO UPDATE SET 267 + "cid" = EXCLUDED."cid", 268 + "json" = EXCLUDED."json", 269 + "indexedAt" = EXCLUDED."indexedAt""#, 270 + record.uri, 271 + record.cid, 272 + record.did, 273 + record.collection, 274 + record.json, 275 + record.indexed_at 276 + ).execute(&mut *tx).await?; 277 + } 278 + 279 + tx.commit().await?; 280 + Ok(()) 281 + } 282 + ``` 283 + 284 + ### API Generation Strategy 285 + 286 + **XRPC Endpoints** with proper lexicon definitions: 287 + 288 + ``` 289 + GET /xrpc/com.indexer.records.list # List records for collection 290 + GET /xrpc/com.indexer.records.get # Get specific record 291 + POST /xrpc/com.indexer.records.create # Create record 292 + POST /xrpc/com.indexer.records.update # Update record 293 + POST /xrpc/com.indexer.records.delete # Delete record 294 + 295 + # Advanced query procedures 296 + GET /xrpc/com.indexer.records.search # Full-text search on record content 297 + GET /xrpc/com.indexer.records.filter # JSON field filtering 298 + GET /xrpc/com.indexer.author.listRecords # All records by author (cross-collection) 299 + GET /xrpc/com.indexer.search.global # Global search across all collections 300 + ``` 301 + 302 + **Lexicon Definitions** for indexer procedures: 303 + 304 + ```json 305 + { 306 + "lexicon": 1, 307 + "id": "com.indexer.records.list", 308 + "defs": { 309 + "main": { 310 + "type": "query", 311 + "description": "List records for a specific collection", 312 + "parameters": { 313 + "collection": { 314 + "type": "string", 315 + "description": "Collection/lexicon ID (e.g. app.bsky.feed.post)", 316 + "required": true 317 + }, 318 + "author": { 319 + "type": "string", 320 + "description": "Filter by author DID" 321 + }, 322 + "limit": { 323 + "type": "integer", 324 + "minimum": 1, 325 + "maximum": 100, 326 + "default": 25 327 + }, 328 + "cursor": { 329 + "type": "string", 330 + "description": "Pagination cursor" 331 + } 332 + }, 333 + "output": { 334 + "encoding": "application/json", 335 + "schema": { 336 + "type": "object", 337 + "required": ["records"], 338 + "properties": { 339 + "records": { 340 + "type": "array", 341 + "items": { "$ref": "#/defs/indexedRecord" } 342 + }, 343 + "cursor": { "type": "string" } 344 + } 345 + } 346 + } 347 + }, 348 + "indexedRecord": { 349 + "type": "object", 350 + "required": ["uri", "cid", "value", "indexedAt"], 351 + "properties": { 352 + "uri": { "type": "string", "format": "at-uri" }, 353 + "cid": { "type": "string" }, 354 + "value": { "type": "unknown" }, 355 + "indexedAt": { "type": "string", "format": "datetime" }, 356 + "collection": { "type": "string" }, 357 + "rkey": { "type": "string" }, 358 + "authorDid": { "type": "string", "format": "did" } 359 + } 360 + } 361 + } 362 + } 363 + ``` 364 + 365 + **Benefits of XRPC + Lexicons**: 366 + 367 + - **Native AT Protocol**: Indexer becomes a proper AT Protocol service 368 + - **Discoverable APIs**: Lexicons can be fetched and introspected 369 + - **Type Generation**: Same code generation works for indexer APIs 370 + - **Consistent**: Uses established AT Protocol patterns 371 + - **Composable**: Can be mixed with other AT Protocol services 372 + 373 + **XRPC Implementation Examples**: 374 + 375 + ```rust 376 + // XRPC query handler for listing records 377 + async fn handle_list_records(&self, params: ListRecordsParams) -> Result<ListRecordsOutput> { 378 + let records = sqlx::query!( 379 + r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 380 + FROM "record" 381 + WHERE "collection" = $1 382 + AND ($2::text IS NULL OR "did" = $2) 383 + ORDER BY "indexedAt" DESC 384 + LIMIT $3"#, 385 + params.collection, 386 + params.author, 387 + params.limit.unwrap_or(25) as i32 388 + ).fetch_all(&self.db).await?; 389 + 390 + let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|row| { 391 + IndexedRecord { 392 + uri: row.uri, 393 + cid: row.cid, 394 + did: row.did, 395 + collection: row.collection, 396 + value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(), 397 + indexed_at: row.indexedAt.to_rfc3339(), 398 + } 399 + }).collect(); 400 + 401 + Ok(ListRecordsOutput { 402 + records: indexed_records, 403 + cursor: self.generate_cursor(&records).await?, 404 + }) 405 + } 406 + 407 + // XRPC search handler with JSONB queries 408 + async fn handle_search_records(&self, params: SearchParams) -> Result<SearchOutput> { 409 + let records = sqlx::query!( 410 + r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 411 + FROM "record" 412 + WHERE ($1::text IS NULL OR "collection" = $1) 413 + AND "json"->>'text' ILIKE $2 414 + ORDER BY "indexedAt" DESC 415 + LIMIT $3"#, 416 + params.collection, 417 + format!("%{}%", params.query), 418 + params.limit.unwrap_or(25) as i32 419 + ).fetch_all(&self.db).await?; 420 + 421 + Ok(SearchOutput { 422 + records: records.into_iter().map(|row| IndexedRecord { 423 + uri: row.uri, 424 + cid: row.cid, 425 + did: row.did, 426 + collection: row.collection, 427 + value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(), 428 + indexed_at: row.indexedAt.to_rfc3339(), 429 + }).collect() 430 + }) 431 + } 432 + ``` 433 + 434 + ## Multi-Language Client Generation 435 + 436 + ### Initial Target: TypeScript 437 + 438 + **Primary focus**: Generate fully typed TypeScript clients for web applications 439 + and Node.js services 440 + 441 + - **Type Safety**: Complete interfaces for all request/response objects 442 + - **Auto-completion**: Full IDE support with generated types 443 + - **Runtime Validation**: Optional runtime type checking 444 + - **Documentation**: Auto-generated JSDoc comments from lexicon descriptions 445 + 446 + ### Future Language Support 447 + 448 + **Planned targets** for multi-language expansion: 449 + 450 + - **Rust**: High-performance services, CLI tools 451 + - **Python**: Data analysis, ML workflows, web backends 452 + - **Go**: Microservices, system tools 453 + 454 + ### Code Generation Pipeline 455 + 456 + **Extensible architecture** designed for multiple languages: 457 + 458 + ```rust 459 + trait CodeGenerator { 460 + fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String>; 461 + fn generate_types(&self, lexicon: &LexiconDoc) -> Result<String>; 462 + fn generate_method(&self, nsid: &str, def: &LexiconDef) -> Result<String>; 463 + } 464 + 465 + // Initial implementation: TypeScript 466 + impl CodeGenerator for TypeScriptGenerator { 467 + fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String> { 468 + // Generate TypeScript client with full type safety 469 + } 470 + } 471 + 472 + // Future implementations: 473 + // impl CodeGenerator for RustGenerator { /* ... */ } 474 + // impl CodeGenerator for PythonGenerator { /* ... */ } 475 + // impl CodeGenerator for GoGenerator { /* ... */ } 476 + ``` 477 + 478 + ### TypeScript Client Generation 479 + 480 + **Type-Safe Generic XRPC Client with Auto-Discovery:** 481 + 482 + ```typescript 483 + // Registry of all known collections -> their record types 484 + interface CollectionRecordMap { 485 + // Core AT Protocol (always included) 486 + "app.bsky.feed.post": PostRecord; 487 + "app.bsky.actor.profile": ProfileRecord; 488 + "app.bsky.feed.like": LikeRecord; 489 + 490 + // Dynamically discovered custom lexicons 491 + "recipes.cooking-app.com": RecipeRecord; 492 + "tasks.productivity-tool.io": TaskRecord; 493 + "photos.gallery-app.net": PhotoRecord; 494 + "someRecord.something-cool.indexer.com": SomeCustomRecord; 495 + } 496 + 497 + // Generic input/output types with conditional typing 498 + interface CreateRecordInput<T extends keyof CollectionRecordMap> { 499 + collection: T; 500 + repo: string; // The DID that will become the 'did' field 501 + rkey?: string; // Used to construct the URI 502 + record: CollectionRecordMap[T]; // Type depends on collection! 503 + } 504 + 505 + interface ListRecordsParams<T extends keyof CollectionRecordMap> { 506 + collection: T; 507 + author?: string; 508 + limit?: number; 509 + cursor?: string; 510 + } 511 + 512 + interface ListRecordsOutput<T extends keyof CollectionRecordMap> { 513 + records: Array<{ 514 + uri: string; 515 + cid: string; 516 + did: string; // Author DID 517 + collection: T; 518 + value: CollectionRecordMap[T]; // Typed based on collection (parsed from json field) 519 + indexedAt: string; 520 + }>; 521 + cursor?: string; 522 + } 523 + 524 + // Generated client class with conditional types 525 + export class ATProtoIndexerClient { 526 + private client: AxiosInstance; 527 + 528 + constructor(baseURL: string, accessToken?: string) { 529 + this.client = axios.create({ 530 + baseURL, 531 + headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, 532 + }); 533 + } 534 + 535 + // Generic method - fully typed based on collection parameter 536 + async createRecord<T extends keyof CollectionRecordMap>( 537 + input: CreateRecordInput<T>, 538 + ): Promise<CreateRecordOutput>; 539 + 540 + // Fallback for unknown collections 541 + async createRecord(input: { 542 + collection: string; 543 + repo: string; 544 + rkey?: string; 545 + record: unknown; 546 + }): Promise<CreateRecordOutput>; 547 + 548 + // Implementation handles both cases 549 + async createRecord(input: any): Promise<CreateRecordOutput> { 550 + const response = await this.client.post( 551 + "/xrpc/com.indexer.records.create", 552 + input, 553 + ); 554 + return response.data; 555 + } 556 + 557 + // Generic typed list method 558 + async listRecords<T extends keyof CollectionRecordMap>( 559 + params: ListRecordsParams<T>, 560 + ): Promise<ListRecordsOutput<T>>; 561 + 562 + // Fallback for unknown collections 563 + async listRecords(params: { 564 + collection: string; 565 + author?: string; 566 + limit?: number; 567 + cursor?: string; 568 + }): Promise<ListRecordsOutput<string>>; 569 + 570 + async listRecords(params: any): Promise<any> { 571 + const response = await this.client.get("/xrpc/com.indexer.records.list", { 572 + params, 573 + }); 574 + return response.data; 575 + } 576 + 577 + // Convenience methods for popular collections 578 + async createPost( 579 + input: Omit<CreateRecordInput<"app.bsky.feed.post">, "collection">, 580 + ) { 581 + return this.createRecord({ ...input, collection: "app.bsky.feed.post" }); 582 + } 583 + 584 + async listPosts( 585 + params: Omit<ListRecordsParams<"app.bsky.feed.post">, "collection">, 586 + ) { 587 + return this.listRecords({ ...params, collection: "app.bsky.feed.post" }); 588 + } 589 + 590 + // Auto-generated convenience methods for custom lexicons 591 + async createRecipe( 592 + input: Omit<CreateRecordInput<"recipes.cooking-app.com">, "collection">, 593 + ) { 594 + return this.createRecord({ 595 + ...input, 596 + collection: "recipes.cooking-app.com", 597 + }); 598 + } 599 + 600 + async searchRecords(params: SearchRecordsParams): Promise<SearchOutput> { 601 + const response = await this.client.get("/xrpc/com.indexer.records.search", { 602 + params, 603 + }); 604 + return response.data; 605 + } 606 + } 607 + ``` 608 + 609 + **Usage Examples with Full Type Safety:** 610 + 611 + ```typescript 612 + const indexer = new ATProtoIndexerClient("https://indexer.example.com"); 613 + 614 + // ✅ Fully typed for known collections 615 + await indexer.createPost({ 616 + repo: "did:plc:user123", 617 + record: { 618 + $type: "app.bsky.feed.post", 619 + text: "Hello!", 620 + createdAt: new Date().toISOString(), 621 + // TypeScript knows this must be a PostRecord 622 + }, 623 + }); 624 + 625 + // ✅ Custom lexicon with full typing 626 + await indexer.createRecord({ 627 + collection: "recipes.cooking-app.com", 628 + repo: "did:plc:chef456", 629 + record: { 630 + $type: "recipes.cooking-app.com", 631 + title: "Pizza", 632 + ingredients: ["dough", "sauce", "cheese"], 633 + difficulty: "easy", 634 + // TypeScript enforces RecipeRecord structure 635 + }, 636 + }); 637 + 638 + // ✅ Query with same type safety - returns typed results 639 + const posts = await indexer.listPosts({ 640 + author: "did:plc:user123", 641 + limit: 50, 642 + }); 643 + // posts.records[0].value is typed as PostRecord! 644 + 645 + // ✅ Unknown collection - falls back gracefully 646 + await indexer.createRecord({ 647 + collection: "new-app.startup.xyz", 648 + repo: "did:plc:user789", 649 + record: { 650 + customField: "value", // No type checking, but still works 651 + }, 652 + }); 653 + ``` 654 + 655 + **Auto-Discovery Implementation:** 656 + 657 + ```rust 658 + // Indexer discovers and registers custom lexicons dynamically 659 + impl ATProtoIndexer { 660 + async fn discover_lexicons(&self) -> Result<Vec<LexiconDoc>> { 661 + let mut lexicons = Vec::new(); 662 + 663 + // Core AT Protocol lexicons 664 + lexicons.extend(self.load_core_lexicons().await?); 665 + 666 + // Custom lexicons from indexed records 667 + let custom_collections = sqlx::query!( 668 + r#"SELECT DISTINCT "collection" FROM "record" 669 + WHERE "collection" NOT LIKE 'app.bsky.%' 670 + AND "collection" NOT LIKE 'com.atproto.%'"# 671 + ).fetch_all(&self.db).await?; 672 + 673 + for row in custom_collections { 674 + if let Ok(lexicon) = self.fetch_lexicon_definition(&row.collection).await { 675 + lexicons.push(lexicon); 676 + } 677 + } 678 + 679 + Ok(lexicons) 680 + } 681 + 682 + async fn fetch_lexicon_definition(&self, nsid: &str) -> Result<LexiconDoc> { 683 + // Fetch from domain's well-known endpoint 684 + let domain = nsid.split('.').last().unwrap_or(""); 685 + let lexicon_url = format!("https://{}/.well-known/atproto/lexicon/{}", domain, nsid); 686 + 687 + let response = self.client.get(&lexicon_url).send().await?; 688 + let lexicon: LexiconDoc = response.json().await?; 689 + Ok(lexicon) 690 + } 691 + 692 + async fn regenerate_typescript_client(&self) -> Result<()> { 693 + let all_lexicons = self.discover_lexicons().await?; 694 + let typescript_code = self.typescript_generator.generate_client(&all_lexicons)?; 695 + 696 + // Write to file or serve via API endpoint 697 + self.write_client_code("typescript", &typescript_code).await?; 698 + Ok(()) 699 + } 700 + 701 + // Get statistics about indexed collections 702 + async fn get_collection_stats(&self) -> Result<Vec<CollectionStats>> { 703 + let stats = sqlx::query!( 704 + r#"SELECT "collection", 705 + COUNT(*) as record_count, 706 + COUNT(DISTINCT "did") as unique_authors, 707 + MIN("indexedAt") as first_indexed, 708 + MAX("indexedAt") as last_indexed 709 + FROM "record" 710 + GROUP BY "collection" 711 + ORDER BY record_count DESC"# 712 + ).fetch_all(&self.db).await?; 713 + 714 + Ok(stats.into_iter().map(|row| CollectionStats { 715 + collection: row.collection, 716 + record_count: row.record_count.unwrap_or(0) as u64, 717 + unique_authors: row.unique_authors.unwrap_or(0) as u64, 718 + first_indexed: row.first_indexed, 719 + last_indexed: row.last_indexed, 720 + }).collect()) 721 + } 722 + } 723 + ``` 724 + 725 + **Lexicon Discovery Protocol:** 726 + 727 + ```json 728 + // GET https://cooking-app.com/.well-known/atproto/lexicon/recipes.cooking-app.com 729 + { 730 + "lexicon": 1, 731 + "id": "recipes.cooking-app.com", 732 + "description": "Recipe sharing lexicon", 733 + "defs": { 734 + "main": { 735 + "type": "record", 736 + "record": { 737 + "type": "object", 738 + "required": ["$type", "title", "ingredients"], 739 + "properties": { 740 + "$type": { "const": "recipes.cooking-app.com" }, 741 + "title": { "type": "string" }, 742 + "ingredients": { "type": "array", "items": { "type": "string" } }, 743 + "cookingTime": { "type": "integer" }, 744 + "difficulty": { "type": "string", "enum": ["easy", "medium", "hard"] } 745 + } 746 + } 747 + } 748 + } 749 + } 750 + ``` 751 + 752 + **Generated CLI with Discovery:** 753 + 754 + ```bash 755 + # Generate TypeScript client with auto-discovered lexicons 756 + npx atproto-codegen typescript \ 757 + --discover \ 758 + --output ./src/generated/indexer-client.ts \ 759 + --endpoint https://your-indexer.com 760 + 761 + # Or specify additional custom lexicons 762 + npx atproto-codegen typescript \ 763 + --lexicons recipes.cooking-app.com,tasks.productivity-tool.io \ 764 + --output ./src/generated/indexer-client.ts \ 765 + --endpoint https://your-indexer.com 766 + ``` 767 + 768 + ## Implementation Technology Stack 769 + 770 + ### Backend: Rust 771 + 772 + **Rationale**: 773 + 774 + - Zero-copy parsing of CAR files and CBOR data 775 + - Memory safety for long-running indexing processes 776 + - High-performance concurrent processing 777 + - Strong type system prevents runtime errors 778 + - Excellent async ecosystem (Tokio) 779 + 780 + ### Client Generation: TypeScript (Initial Target) 781 + 782 + **Rationale**: 783 + 784 + - **Primary ecosystem**: Most AT Protocol developers use JavaScript/TypeScript 785 + - **Immediate value**: Web apps and Node.js services are common use cases 786 + - **Type safety**: Excellent TypeScript support for generated interfaces 787 + - **Developer experience**: Full IDE support with auto-completion 788 + - **Ecosystem compatibility**: Works with React, Next.js, Express, etc. 789 + 790 + ### Key Dependencies 791 + 792 + ```toml 793 + [dependencies] 794 + tokio = { version = "1.0", features = ["full"] } 795 + sqlx = { version = "0.7", features = ["postgres", "chrono", "serde_json"] } 796 + serde = { version = "1.0", features = ["derive"] } 797 + reqwest = { version = "0.11", features = ["json", "stream"] } 798 + libipld = { version = "0.16", features = ["dag-cbor", "car"] } 799 + tokio-tungstenite = "0.20" # WebSocket for firehose 800 + redis = { version = "0.23", features = ["tokio-comp"] } 801 + tracing = "0.1" 802 + 803 + # Code generation dependencies 804 + handlebars = "4.0" # Template engine for TypeScript generation 805 + ``` 806 + 807 + ## Performance Optimizations 808 + 809 + ### Concurrent Processing 810 + 811 + - **Bounded concurrency**: Limit simultaneous CAR file processing 812 + - **Streaming**: Process large CAR files without loading entirely into memory 813 + - **Batching**: Group database operations for better throughput 814 + - **Connection pooling**: Efficient database connection management 815 + 816 + ### Rate Limiting 817 + 818 + ```rust 819 + // Token bucket implementation for API rate limiting 820 + struct RateLimiter { 821 + tokens: Arc<Mutex<f64>>, 822 + max_tokens: f64, 823 + refill_rate: f64, // tokens per second 824 + } 825 + ``` 826 + 827 + ### Memory Management 828 + 829 + - **Streaming CAR processing**: Avoid loading entire repos into memory 830 + - **LRU caches**: Intelligent caching of frequently accessed data 831 + - **Pagination**: Cursor-based pagination for large result sets 832 + 833 + ## Real-Time Synchronization 834 + 835 + ### Firehose Integration 836 + 837 + ```rust 838 + async fn start_firehose_listener(&self) -> Result<()> { 839 + let (ws_stream, _) = connect_async( 840 + "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos" 841 + ).await?; 842 + 843 + // Process commits in real-time 844 + while let Some(msg) = read.next().await { 845 + if let Ok(commit) = self.parse_commit(&msg) { 846 + self.process_commit(commit).await?; 847 + } 848 + } 849 + 850 + Ok(()) 851 + } 852 + ``` 853 + 854 + ### Sync Strategies 855 + 856 + 1. **Initial Bootstrap**: Download existing data via CAR files 857 + 2. **Real-time Updates**: Process firehose stream for live changes 858 + 3. **Periodic Reconciliation**: Compare local state with remote to catch missed 859 + updates 860 + 4. **Backfill**: Handle gaps in data due to downtime 861 + 862 + ## API Design 863 + 864 + ### Core Principles 865 + 866 + - **RESTful**: Follow REST conventions where applicable 867 + - **Lexicon-Agnostic**: Work with any current or future AT Protocol lexicon 868 + - **Type-Safe**: Generate strongly typed clients 869 + - **Cacheable**: Design for HTTP caching and CDN distribution 870 + - **Paginated**: Support cursor-based pagination for large datasets 871 + 872 + ### Authentication 873 + 874 + - **Optional**: Support authenticated requests for private data 875 + - **Bearer tokens**: Standard AT Protocol authentication 876 + - **Rate limiting**: Per-user and global rate limits 877 + 878 + ### Response Format 879 + 880 + ```json 881 + { 882 + "data": [...], 883 + "cursor": "next_page_token", 884 + "count": 42, 885 + "total": 1337 886 + } 887 + ``` 888 + 889 + ## Risk Mitigation 890 + 891 + ### Data Consistency 892 + 893 + - **Idempotent operations**: Safe to retry any indexing operation 894 + - **Checksum validation**: Verify CAR file integrity 895 + - **Reconciliation**: Periodic comparison with authoritative sources 896 + 897 + ### Scalability 898 + 899 + - **Horizontal scaling**: Design for multiple indexer instances 900 + - **Database sharding**: Partition by lexicon type or DID prefix 901 + - **Caching layers**: Multiple levels of caching for performance 902 + 903 + ### Operational 904 + 905 + - **Circuit breakers**: Prevent cascade failures 906 + - **Graceful degradation**: Continue operating with reduced functionality 907 + - **Monitoring**: Comprehensive observability for quick issue detection
+505
api/docs/lexicons_spec.md
··· 1 + Lexicon Lexicon is a schema definition language used to describe atproto 2 + records, HTTP endpoints (XRPC), and event stream messages. It builds on top of 3 + the atproto Data Model. 4 + 5 + The schema language is similar to JSON Schema and OpenAPI, but includes some 6 + atproto-specific features and semantics. 7 + 8 + This specification describes version 1 of the Lexicon definition language. 9 + 10 + Overview of Types Lexicon Type Data Model Type Category null Null concrete 11 + boolean Boolean concrete integer Integer concrete string String concrete 12 + bytes Bytes concrete cid-link Link concrete blob Blob concrete 13 + array Array container object Object container params container token meta 14 + ref meta union meta unknown meta record primary query primary 15 + procedure primary subscription primary Lexicon Files Lexicons are JSON files 16 + associated with a single NSID. A file contains one or more definitions, each 17 + with a distinct short name. A definition with the name main optionally describes 18 + the "primary" definition for the entire file. A Lexicon with zero definitions is 19 + invalid. 20 + 21 + A Lexicon JSON file is an object with the following fields: 22 + 23 + lexicon (integer, required): indicates Lexicon language version. In this 24 + version, a fixed value of 1 id (string, required): the NSID of the Lexicon 25 + description (string, optional): short overview of the Lexicon, usually one or 26 + two sentences defs (map of strings-to-objects, required): set of definitions, 27 + each with a distinct name (key) Schema definitions under defs all have a type 28 + field to distinguish their type. A file can have at most one definition with one 29 + of the "primary" types. Primary types should always have the name main. It is 30 + possible for main to describe a non-primary type. 31 + 32 + References to specific definitions within a Lexicon use fragment syntax, like 33 + com.example.defs#someView. If a main definition exists, it can be referenced 34 + without a fragment, just using the NSID. For references in the $type fields in 35 + data objects themselves (eg, records or contents of a union), this is a "must" 36 + (use of a #main suffix is invalid). For example, com.example.record not 37 + com.example.record#main. 38 + 39 + Related Lexicons are often grouped together in the NSID hierarchy. As a 40 + convention, any definitions used by multiple Lexicons are defined in a dedicated 41 + *.defs Lexicon (eg, com.atproto.server.defs) within the group. A *.defs Lexicon 42 + should generally not include a definition named main, though it is not strictly 43 + invalid to do so. 44 + 45 + Primary Type Definitions The primary types are: 46 + 47 + query: describes an XRPC Query (HTTP GET) procedure: describes an XRPC Procedure 48 + (HTTP POST) subscription: Event Stream (WebSocket) record: describes an object 49 + that can be stored in a repository record Each primary definition schema object 50 + includes these fields: 51 + 52 + type (string, required): the type value (eg, record for records) description 53 + (string, optional): short, usually only a sentence or two Record Type-specific 54 + fields: 55 + 56 + key (string, required): specifies the Record Key type record (object, required): 57 + a schema definition with type object, which specifies this type of record Query 58 + and Procedure (HTTP API) Type-specific fields: 59 + 60 + parameters (object, optional): a schema definition with type params, describing 61 + the HTTP query parameters for this endpoint output (object, optional): describes 62 + the HTTP response body description (string, optional): short description 63 + encoding (string, required): MIME type for body contents. Use application/json 64 + for JSON responses. schema (object, optional): schema definition, either an 65 + object, a ref, or a union of refs. Used to describe JSON encoded responses, 66 + though schema is optional even for JSON responses. input (object, optional, only 67 + for procedure): describes HTTP request body schema, with the same format as the 68 + output field errors (array of objects, optional): set of string error codes 69 + which might be returned name (string, required): short name for the error type, 70 + with no whitespace description (string, optional): short description, one or two 71 + sentences Subscription (Event Stream) Type-specific fields: 72 + 73 + parameters (object, optional): same as Query and Procedure message (object, 74 + optional): specifies what messages can be description (string, optional): short 75 + description schema (object, required): schema definition, which must be a union 76 + of refs errors (array of objects, optional): same as Query and Procedure 77 + Subscription schemas (referenced by the schema field under message) must be a 78 + union of refs, not an object type. 79 + 80 + Field Type Definitions As with the primary definitions, every schema object 81 + includes these fields: 82 + 83 + type (string, required): fixed value for each type description (string, 84 + optional): short, usually only a sentence or two null No additional fields. 85 + 86 + boolean Type-specific fields: 87 + 88 + default (boolean, optional): a default value for this field const (boolean, 89 + optional): a fixed (constant) value for this field When included as an HTTP 90 + query parameter, should be rendered as true or false (no quotes). 91 + 92 + integer A signed integer number. 93 + 94 + Type-specific fields: 95 + 96 + minimum (integer, optional): minimum acceptable value maximum (integer, 97 + optional): maximum acceptable value enum (array of integers, optional): a closed 98 + set of allowed values default (integer, optional): a default value for this 99 + field const (integer, optional): a fixed (constant) value for this field string 100 + Type-specific fields: 101 + 102 + format (string, optional): string format restriction maxLength (integer, 103 + optional): maximum length of value, in UTF-8 bytes minLength (integer, 104 + optional): minimum length of value, in UTF-8 bytes maxGraphemes (integer, 105 + optional): maximum length of value, counted as Unicode Grapheme Clusters 106 + minGraphemes (integer, optional): minimum length of value, counted as Unicode 107 + Grapheme Clusters knownValues (array of strings, optional): a set of suggested 108 + or common values for this field. Values are not limited to this set (aka, not a 109 + closed enum). enum (array of strings, optional): a closed set of allowed values 110 + default (string, optional): a default value for this field const (string, 111 + optional): a fixed (constant) value for this field Strings are Unicode. For 112 + non-Unicode encodings, use bytes instead. The basic minLength/maxLength 113 + validation constraints are counted as UTF-8 bytes. Note that Javascript stores 114 + strings with UTF-16 by default, and it is necessary to re-encode to count 115 + accurately. The minGraphemes/maxGraphemes validation constraints work with 116 + Grapheme Clusters, which have a complex technical and linguistic definition, but 117 + loosely correspond to "distinct visual characters" like Latin letters, CJK 118 + characters, punctuation, digits, or emoji (which might comprise multiple Unicode 119 + codepoints and many UTF-8 bytes). 120 + 121 + format constrains the string format and provides additional semantic context. 122 + Refer to the Data Model specification for the available format types and their 123 + definitions. 124 + 125 + const and default are mutually exclusive. 126 + 127 + bytes Type-specific fields: 128 + 129 + minLength (integer, optional): minimum size of value, as raw bytes with no 130 + encoding maxLength (integer, optional): maximum size of value, as raw bytes with 131 + no encoding cid-link No type-specific fields. 132 + 133 + See Data Model spec for CID restrictions. 134 + 135 + array Type-specific fields: 136 + 137 + items (object, required): describes the schema elements of this array minLength 138 + (integer, optional): minimum count of elements in array maxLength (integer, 139 + optional): maximum count of elements in array In theory arrays have homogeneous 140 + types (meaning every element as the same type). However, with union types this 141 + restriction is meaningless, so implementations can not assume that all the 142 + elements have the same type. 143 + 144 + object A generic object schema which can be nested inside other definitions by 145 + reference. 146 + 147 + Type-specific fields: 148 + 149 + properties (map of strings-to-objects, required): defines the properties 150 + (fields) by name, each with their own schema required (array of strings, 151 + optional): indicates which properties are required nullable (array of strings, 152 + optional): indicates which properties can have null as a value As described in 153 + the data model specification, there is a semantic difference in data between 154 + omitting a field; including the field with the value null; and including the 155 + field with a "false-y" value (false, 0, empty array, etc). 156 + 157 + blob Type-specific fields: 158 + 159 + accept (array of strings, optional): list of acceptable MIME types. Each may end 160 + in * as a glob pattern (eg, image/*). Use _/_ to indicate that any MIME type is 161 + accepted. maxSize (integer, optional): maximum size in bytes params This is a 162 + limited-scope type which is only ever used for the parameters field on query, 163 + procedure, and subscription primary types. These map to HTTP query parameters. 164 + 165 + Type-specific fields: 166 + 167 + required (array of strings, optional): same semantics as field on object 168 + properties: similar to properties under object, but can only include the types 169 + boolean, integer, string, and unknown; or an array of one of these types Note 170 + that unlike object, there is no nullable field on params. 171 + 172 + token Tokens are empty data values which exist only to be referenced by name. 173 + They are used to define a set of values with specific meanings. The description 174 + field should clarify the meaning of the token. Tokens encode as string data, 175 + with the string being the fully-qualified reference to the token itself (NSID 176 + followed by an optional fragment). 177 + 178 + Tokens are similar to the concept of a "symbol" in some programming languages, 179 + distinct from strings, variables, built-in keywords, or other identifiers. 180 + 181 + For example, tokens could be defined to represent the state of an entity (in a 182 + state machine), or to enumerate a list of categories. 183 + 184 + No type-specific fields. 185 + 186 + ref Type-specific fields: 187 + 188 + ref (string, required): reference to another schema definition Refs are a 189 + mechanism for re-using a schema definition in multiple places. The ref string 190 + can be a global reference to a Lexicon type definition (an NSID, optionally with 191 + a #-delimited name indicating a definition other than main), or can indicate a 192 + local definition within the same Lexicon file (a # followed by a name). 193 + 194 + union Type-specific fields: 195 + 196 + refs (array of strings, required): references to schema definitions closed 197 + (boolean, optional): indicates if a union is "open" or "closed". defaults to 198 + false (open union) Unions represent that multiple possible types could be 199 + present at this location in the schema. The references follow the same syntax as 200 + ref, allowing references to both global or local schema definitions. Actual data 201 + will validate against a single specific type: the union does not combine fields 202 + from multiple schemas, or define a new hybrid data type. The different types are 203 + referred to as variants. 204 + 205 + By default unions are "open", meaning that future revisions of the schema could 206 + add more types to the list of refs (though can not remove types). This means 207 + that implementations should be permissive when validating, in case they do not 208 + have the most recent version of the Lexicon. The closed flag (boolean) can 209 + indicate that the set of types is fixed and can not be extended in the future. 210 + 211 + A union schema definition with no refs is allowed and similar to unknown, as 212 + long as the closed flag is false (the default). The main difference is that the 213 + data would be required to have the $type field. An empty refs list with closed 214 + set to true is an invalid schema. 215 + 216 + The schema definitions pointed to by a union are objects or types with a clear 217 + mapping to an object, like a record. All the variants must be represented by a 218 + CBOR map (or JSON Object) and must include a $type field indicating the variant 219 + type. Because the data must be an object, unions can not reference token (which 220 + would correspond to string data). 221 + 222 + unknown Indicates than any data object could appear at this location, with no 223 + specific validation. The top-level data must be an object (not a string, 224 + boolean, etc). As with all other data types, the value null is not allowed 225 + unless the field is specifically marked as nullable. 226 + 227 + The data object may contain a 228 + $type field indicating the schema of the data, but this is not currently required. The top-level data object must not have the structure of a compound data type, like blob ($type: 229 + blob) or CID link ($link). 230 + 231 + The (nested) contents of the data object must still be valid under the atproto 232 + data model. For example, it should not contain floats. Nested compound types 233 + like blobs and CID links should be validated and transformed as expected. 234 + 235 + Lexicon designers are strongly recommended to not use unknown fields in record 236 + objects for now. 237 + 238 + No type-specific fields. 239 + 240 + String Formats Strings can optionally be constrained to one of the following 241 + format types: 242 + 243 + at-identifier: either a Handle or a DID, details described below at-uri: AT-URI 244 + cid: CID in string format, details specified in Data Model datetime: timestamp, 245 + details specified below did: generic DID Identifier handle: Handle Identifier 246 + nsid: Namespaced Identifier tid: Timestamp Identifier (TID) record-key: Record 247 + Key, matching the general syntax ("any") uri: generic URI, details specified 248 + below language: language code, details specified below For the various 249 + identifier formats, when doing Lexicon schema validation the most expansive 250 + identifier syntax format should be permitted. Problems with identifiers which do 251 + pass basic syntax validation should be reported as application errors, not 252 + lexicon data validation errors. For example, data with any kind of DID in a did 253 + format string field should pass Lexicon validation, with unsupported DID methods 254 + being raised separately as an application error. 255 + 256 + at-identifier A string type which is either a DID (type: did) or a handle 257 + (handle). Mostly used in XRPC query parameters. It is unambiguous whether an 258 + at-identifier is a handle or a DID because a DID always starts with did:, and 259 + the colon character (:) is not allowed in handles. 260 + 261 + datetime Full-precision date and time, with timezone information. 262 + 263 + This format is intended for use with computer-generated timestamps in the modern 264 + computing era (eg, after the UNIX epoch). If you need to represent historical or 265 + ancient events, ambiguity, or far-future times, a different format is probably 266 + more appropriate. Datetimes before the Current Era (year zero) as specifically 267 + disallowed. 268 + 269 + Datetime format standards are notoriously flexible and overlapping. Datetime 270 + strings in atproto should meet the intersecting requirements of the RFC 3339, 271 + ISO 8601, and WHATWG HTML datetime standards. 272 + 273 + The character separating "date" and "time" parts must be an upper-case T. 274 + 275 + Timezone specification is required. It is strongly preferred to use the UTC 276 + timezone, and to represent the timezone with a simple capital Z suffix 277 + (lower-case is not allowed). While hour/minute suffix syntax (like +01:00 or 278 + -10:30) is supported, "negative zero" (-00:00) is specifically disallowed (by 279 + ISO 8601). 280 + 281 + Whole seconds precision is required, and arbitrary fractional precision digits 282 + are allowed. Best practice is to use at least millisecond precision, and to pad 283 + with zeros to the generated precision (eg, trailing :12.340Z instead of 284 + :12.34Z). Not all datetime formatting libraries support trailing zero 285 + formatting. Both millisecond and microsecond precision have reasonable 286 + cross-language support; nanosecond precision does not. 287 + 288 + Implementations should be aware when round-tripping records containing datetimes 289 + of two ambiguities: loss-of-precision, and ambiguity with trailing fractional 290 + second zeros. If de-serializing Lexicon records into native types, and then 291 + re-serializing, the string representation may not be the same, which could 292 + result in broken hash references, sanity check failures, or repository update 293 + churn. A safer thing to do is to deserialize the datetime as a simple string, 294 + which ensures round-trip re-serialization. 295 + 296 + Implementations "should" validate that the semantics of the datetime are valid. 297 + For example, a month or day 00 is invalid. 298 + 299 + Valid examples: 300 + 301 + # preferred 302 + 303 + 1985-04-12T23:20:50.123Z 1985-04-12T23:20:50.123456Z 1985-04-12T23:20:50.120Z 304 + 1985-04-12T23:20:50.120000Z 305 + 306 + # supported 307 + 308 + 1985-04-12T23:20:50.12345678912345Z 1985-04-12T23:20:50Z 1985-04-12T23:20:50.0Z 309 + 1985-04-12T23:20:50.123+00:00 1985-04-12T23:20:50.123-07:00 310 + 311 + Copy Copied! Invalid examples: 312 + 313 + 1985-04-12 1985-04-12T23:20Z 1985-04-12T23:20:5Z 1985-04-12T23:20:50.123 314 + +001985-04-12T23:20:50.123Z 23:20:50.123Z -1985-04-12T23:20:50.123Z 315 + 1985-4-12T23:20:50.123Z 01985-04-12T23:20:50.123Z 1985-04-12T23:20:50.123+00 316 + 1985-04-12T23:20:50.123+0000 317 + 318 + # ISO-8601 strict capitalization 319 + 320 + 1985-04-12t23:20:50.123Z 1985-04-12T23:20:50.123z 321 + 322 + # RFC-3339, but not ISO-8601 323 + 324 + 1985-04-12T23:20:50.123-00:00 1985-04-12 23:20:50.123Z 325 + 326 + # timezone is required 327 + 328 + 1985-04-12T23:20:50.123 329 + 330 + # syntax looks ok, but datetime is not valid 331 + 332 + 1985-04-12T23:99:50.123Z 1985-00-12T23:20:50.123Z 333 + 334 + Copy Copied! uri Flexible to any URI schema, following the generic RFC-3986 on 335 + URIs. This includes, but isn’t limited to: did, https, wss, ipfs (for CIDs), 336 + dns, and of course at. Maximum length in Lexicons is 8 KBytes. 337 + 338 + language An IETF Language Tag string, compliant with BCP 47, defined in RFC 5646 339 + ("Tags for Identifying Languages"). This is the same standard used to identify 340 + languages in HTTP, HTML, and other web standards. The Lexicon string must 341 + validate as a "well-formed" language tag, as defined in the RFC. Clients should 342 + ignore language strings which are "well-formed" but not "valid" according to the 343 + RFC. 344 + 345 + As specified in the RFC, ISO 639 two-character and three-character language 346 + codes can be used on their own, lower-cased, such as ja (Japanese) or ban 347 + (Balinese). Regional sub-tags can be added, like pt-BR (Brazilian Portuguese). 348 + Additional subtags can also be added, such as hy-Latn-IT-arevela. 349 + 350 + Language codes generally need to be parsed, normalized, and matched 351 + semantically, not simply string-compared. For example, a search engine might 352 + simplify language tags to ISO 639 codes for indexing and filtering, while a 353 + client application (user agent) would retain the full language code for 354 + presentation (text rendering) locally. 355 + 356 + When to use $type Data objects sometimes include a $type field which indicates 357 + their Lexicon type. The general principle is that this field needs to be 358 + included any time there could be ambiguity about the content type when 359 + validating data. 360 + 361 + The specific rules are: 362 + 363 + record objects must always include $type. While the type is often known from 364 + context (eg, the collection part of the path for records stored in a 365 + repository), record objects can also be passed around outside of repositories 366 + and need to be self-describing union variants must always include $type, except 367 + at the top level of subscription messages Note that blob objects always include 368 + $type, which allows generic processing. 369 + 370 + As a reminder, main types must be referenced in $type fields as just the NSID, 371 + not including a #main suffix. 372 + 373 + Lexicon Evolution Lexicons are allowed to change over time, within some bounds 374 + to ensure both forwards and backwards compatibility. The basic principle is that 375 + all old data must still be valid under the updated Lexicon, and new data must be 376 + valid under the old Lexicon. 377 + 378 + Any new fields must be optional Non-optional fields can not be removed. A best 379 + practice is to retain all fields in the Lexicon and mark them as deprecated if 380 + they are no longer used. Types can not change Fields can not be renamed If 381 + larger breaking changes are necessary, a new Lexicon name must be used. 382 + 383 + It can be ambiguous when a Lexicon has been published and becomes "set in 384 + stone". At a minimum, public adoption and implementation by a third party, even 385 + without explicit permission, indicates that the Lexicon has been released and 386 + should not break compatibility. A best practice is to clearly indicate in the 387 + Lexicon type name any experimental or development status. Eg, 388 + com.corp.experimental.newRecord. 389 + 390 + Authority and Control The authority for a Lexicon is determined by the NSID, and 391 + rooted in DNS control of the domain authority. That authority has ultimate 392 + control over the Lexicon definition, and responsibility for maintenance and 393 + distribution of Lexicon schema definitions. 394 + 395 + In a crisis, such as unintentional loss of DNS control to a bad actor, the 396 + protocol ecosystem could decide to disregard this chain of authority. This 397 + should only be done in exceptional circumstances, and not as a mechanism to 398 + subvert an active authority. The primary mechanism for resolving protocol 399 + disputes is to fork Lexicons in to a new namespace. 400 + 401 + Protocol implementations should generally consider data which fails to validate 402 + against the Lexicon to be entirely invalid, and should not try to repair or do 403 + partial processing on the individual piece of data. 404 + 405 + Unexpected fields in data which otherwise conforms to the Lexicon should be 406 + ignored. When doing schema validation, they should be treated at worst as 407 + warnings. This is necessary to allow evolution of the schema by the controlling 408 + authority, and to be robust in the case of out-of-date Lexicons. 409 + 410 + Third parties can technically insert any additional fields they want into data. 411 + This is not the recommended way to extend applications, but it is not 412 + specifically disallowed. One danger with this is that the Lexicon may be updated 413 + to include fields with the same field names but different types, which would 414 + make existing data invalid. 415 + 416 + Lexicon Publication and Resolution Lexicon schemas are published publicly as 417 + records in atproto repositories, using the com.atproto.lexicon.schema type. The 418 + domain name authority for NSIDs to specific atproto repositories (identified by 419 + DID is linked by a DNS TXT record (_lexicon), similar to but distinct from the 420 + handle resolution system. 421 + 422 + The com.atproto.lexicon.schema Lexicon itself is very minimal: it only requires 423 + the lexicon integer field, which must be 1 for this version of the Lexicon 424 + language. In practice, same fields as Lexicon Files should be included, along 425 + with $type. The record key is the NSID of the schema. 426 + 427 + A summary of record fields: 428 + 429 + $type: must be com.atproto.lexicon.schema (as with all atproto records) lexicon: 430 + integer, indicates the overall version of the Lexicon (currently 1) id: the NSID 431 + of this Lexicon. Must be a simple NSID (no fragment), and must match the record 432 + key defs: the schema definitions themselves, as a map-of-objects. Names should 433 + not include a # prefix. description: optional description of the overall schema; 434 + though descriptions are best included on individual defs, not the overall 435 + schema. The com.atproto.lexicon.schema meta-schema is somewhat unlike other 436 + Lexicons, in that it is defined and governed as part of the protocol. Future 437 + versions of the language and protocol might not follow the evolution rules. It 438 + is an intentional decision to not express the Lexicon schema language itself 439 + recursively, using the schema language. 440 + 441 + Authority for NSID namespaces is done at the "group" level, meaning that all 442 + NSIDs which differ only by the final "name" part are all published in the same 443 + repository. Lexicon resolution of NSIDs is not hierarchical: DNS TXT records 444 + must be created for each authority section, and resolvers should not recurse up 445 + or down the DNS hierarchy looking for TXT records. 446 + 447 + As an example, the NSID edu.university.dept.lab.blogging.getBlogPost has a 448 + "name" getBlogPost. Removing the name and reversing the rest of the NSID gives 449 + an "authority domain name" of blogging.lab.dept.university.edu. To link the 450 + authority to a specific DID (say did:plc:ewvi7nxzyoun6zhxrhs64oiz), a DNS TXT 451 + record with the name _lexicon.blogging.lab.dept.university.edu and value 452 + did=did:plc:ewvi7nxzyoun6zhxrhs64oiz (note the did= prefix) would be created. 453 + Then a record with collection com.atproto.lexicon.schema and record-key 454 + edu.university.dept.lab.blogging.getBlogPost would be created in that account's 455 + repository. 456 + 457 + A resolving service would start with the NSID 458 + (edu.university.dept.lab.blogging.getBlogPost) and do a DNS TXT resolution for 459 + _lexicon.blogging.lab.dept.university.edu. Finding the DID, it would proceed 460 + with atproto DID resolution, look for a PDS, and then fetch the relevant record. 461 + The overall AT-URI for the record would be 462 + at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.lexicon.schema/edu.university.dept.lab.blogging.getBlogPost. 463 + 464 + If the DNS TXT resolution for _lexicon.blogging.lab.dept.university.edu failed, 465 + the resolving service would NOT try _lexicon.lab.dept.university.edu or 466 + _lexicon.getBlogPost.blogging.lab.dept.university.edu or 467 + _lexicon.university.edu, or any other domain name. The Lexicon resolution would 468 + simply fail. 469 + 470 + If another NSID edu.university.dept.lab.blogging.getBlogComments was created, it 471 + would have the same authority name, and must be published in the same atproto 472 + repository (with a different record key). If a Lexicon for 473 + edu.university.dept.lab.gallery.photo was published, a new DNS TXT record would 474 + be required (_lexicon.gallery.lab.dept.university.edu; it could point at the 475 + same repository (DID), or a different repository. 476 + 477 + As a simpler example, an NSID app.toy.record would resolve via _lexicon.toy.app. 478 + 479 + A single repository can host Lexicons for multiple authority domains, possibly 480 + across multiple registered domains and TLDs. Resolution DNS records can change 481 + over time, moving schema resolution to different repositories, though it may 482 + take time for DNS and cache changes to propagate. 483 + 484 + Note that Lexicon record operations are broadcast over repository event streams 485 + ("firehose"), but that DNS resolution changes do not (unlike handle changes). 486 + Resolving services should not cache DNS resolution results for long time 487 + periods. 488 + 489 + Usage and Implementation Guidelines It should be possible to translate Lexicon 490 + schemas to JSON Schema or OpenAPI and use tools and libraries from those 491 + ecosystems to work with atproto data in JSON format. 492 + 493 + Implementations which serialize and deserialize data from JSON or CBOR into 494 + structures derived from specific Lexicons should be aware of the risk of 495 + "clobbering" unexpected fields. For example, if a Lexicon is updated to add a 496 + new (optional) field, old implementations would not be aware of that field, and 497 + might accidentally strip the data when de-serializing and then re-serializing. 498 + Depending on the context, one way to avoid this problem is to retain any "extra" 499 + fields, or to pass-through the original data object instead of re-serializing 500 + it. 501 + 502 + Possible Future Changes The validation rules for unexpected additional fields 503 + may change. For example, a mechanism for Lexicons to indicate that the schema is 504 + "closed" and unexpected fields are not allowed, or a convention around field 505 + name prefixes (x-) to indicate unofficial extension.
api/lexicons.zip

This is a binary file and will not be displayed.

+21
api/migrations/001_initial.sql
··· 1 + -- AT Protocol Indexer Database Schema 2 + -- Single table approach for maximum flexibility across arbitrary lexicons 3 + 4 + CREATE TABLE IF NOT EXISTS "record" ( 5 + "uri" TEXT PRIMARY KEY NOT NULL, 6 + "cid" TEXT NOT NULL, 7 + "did" TEXT NOT NULL, 8 + "collection" TEXT NOT NULL, 9 + "json" JSONB NOT NULL, 10 + "indexedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 11 + ); 12 + 13 + -- Essential indexes for performance 14 + CREATE INDEX IF NOT EXISTS idx_record_collection ON "record"("collection"); 15 + CREATE INDEX IF NOT EXISTS idx_record_did ON "record"("did"); 16 + CREATE INDEX IF NOT EXISTS idx_record_indexed_at ON "record"("indexedAt"); 17 + CREATE INDEX IF NOT EXISTS idx_record_json_gin ON "record" USING GIN("json"); 18 + 19 + -- Collection-specific indexes for common queries 20 + CREATE INDEX IF NOT EXISTS idx_record_collection_did ON "record"("collection", "did"); 21 + CREATE INDEX IF NOT EXISTS idx_record_cid ON "record"("cid");
+10
api/migrations/002_lexicons.sql
··· 1 + -- Add lexicons table for storing AT Protocol lexicon schemas 2 + CREATE TABLE IF NOT EXISTS "lexicons" ( 3 + "nsid" TEXT PRIMARY KEY NOT NULL, 4 + "definitions" JSONB NOT NULL, 5 + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 6 + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 7 + ); 8 + 9 + CREATE INDEX IF NOT EXISTS idx_lexicons_nsid ON "lexicons"("nsid"); 10 + CREATE INDEX IF NOT EXISTS idx_lexicons_definitions ON "lexicons" USING gin("definitions");
+9
api/migrations/003_actors.sql
··· 1 + -- Add actors table for storing AT Protocol actor/profile data 2 + CREATE TABLE IF NOT EXISTS "actor" ( 3 + "did" TEXT PRIMARY KEY NOT NULL, 4 + "handle" TEXT, 5 + "indexedAt" TEXT NOT NULL 6 + ); 7 + 8 + CREATE INDEX IF NOT EXISTS idx_actor_handle ON "actor"("handle"); 9 + CREATE INDEX IF NOT EXISTS idx_actor_indexed_at ON "actor"("indexedAt");
+431
api/scripts/generate-typescript.ts
··· 1 + #!/usr/bin/env deno run --allow-all 2 + 3 + // @ts-ignore 4 + import { Project } from "npm:ts-morph@26.0.0"; 5 + 6 + interface LexiconProperty { 7 + type: string; 8 + description?: string; 9 + } 10 + 11 + interface LexiconRecord { 12 + type: string; 13 + properties?: Record<string, LexiconProperty>; 14 + required?: string[]; 15 + } 16 + 17 + interface LexiconDefinition { 18 + type: string; 19 + record?: LexiconRecord; 20 + } 21 + 22 + interface Lexicon { 23 + nsid: string; 24 + definitions?: Record<string, LexiconDefinition>; 25 + } 26 + 27 + // Get lexicon data from command line args 28 + // @ts-ignore 29 + const lexiconsInput = Deno.args[0] || ""; 30 + if (!lexiconsInput) { 31 + console.error("No lexicon data provided"); 32 + // @ts-ignore 33 + Deno.exit(1); 34 + } 35 + 36 + const lexicons: Lexicon[] = JSON.parse(lexiconsInput); 37 + 38 + // Create a new TypeScript project in memory 39 + const project = new Project({ useInMemoryFileSystem: true }); 40 + const sourceFile = project.createSourceFile("generated-client.ts", ""); 41 + 42 + // Add header comment 43 + const headerComment = `// Generated TypeScript client for AT Protocol records 44 + // Generated at: ${new Date().toISOString().slice(0, 19).replace("T", " ")} UTC 45 + // Lexicons: ${lexicons.length} 46 + 47 + `; 48 + 49 + // Add base interfaces 50 + function addBaseInterfaces(): void { 51 + // RecordResponse interface 52 + sourceFile.addInterface({ 53 + name: "RecordResponse", 54 + typeParameters: [{ name: "T", constraint: "any" }], 55 + isExported: true, 56 + properties: [ 57 + { name: "uri", type: "string" }, 58 + { name: "cid", type: "string" }, 59 + { name: "did", type: "string" }, 60 + { name: "collection", type: "string" }, 61 + { name: "value", type: "T" }, 62 + { name: "indexed_at", type: "string" }, 63 + ], 64 + }); 65 + 66 + // ListRecordsResponse interface 67 + sourceFile.addInterface({ 68 + name: "ListRecordsResponse", 69 + typeParameters: [{ name: "T", constraint: "any" }], 70 + isExported: true, 71 + properties: [ 72 + { name: "records", type: "RecordResponse<T>[]" }, 73 + { name: "cursor", type: "string", hasQuestionToken: true }, 74 + ], 75 + }); 76 + 77 + // ListRecordsParams interface 78 + sourceFile.addInterface({ 79 + name: "ListRecordsParams", 80 + isExported: true, 81 + properties: [ 82 + { name: "author", type: "string", hasQuestionToken: true }, 83 + { name: "limit", type: "number", hasQuestionToken: true }, 84 + { name: "cursor", type: "string", hasQuestionToken: true }, 85 + ], 86 + }); 87 + 88 + // GetRecordParams interface 89 + sourceFile.addInterface({ 90 + name: "GetRecordParams", 91 + isExported: true, 92 + properties: [{ name: "uri", type: "string" }], 93 + }); 94 + 95 + // CollectionOperations interface 96 + sourceFile.addInterface({ 97 + name: "CollectionOperations", 98 + typeParameters: [{ name: "T" }], 99 + isExported: true, 100 + methods: [ 101 + { 102 + name: "listRecords", 103 + parameters: [ 104 + { name: "params", type: "ListRecordsParams", hasQuestionToken: true }, 105 + ], 106 + returnType: "Promise<ListRecordsResponse<T>>", 107 + }, 108 + { 109 + name: "getRecord", 110 + parameters: [{ name: "params", type: "GetRecordParams" }], 111 + returnType: "Promise<RecordResponse<T>>", 112 + }, 113 + ], 114 + }); 115 + } 116 + 117 + // Convert lexicon type to TypeScript type 118 + function convertLexiconTypeToTypeScript(def: LexiconProperty): string { 119 + const type = def.type; 120 + switch (type) { 121 + case "string": 122 + return "string"; 123 + case "integer": 124 + return "number"; 125 + case "boolean": 126 + return "boolean"; 127 + case "object": 128 + return "Record<string, any>"; 129 + case "array": 130 + return "any[]"; 131 + case "blob": 132 + return "Blob"; 133 + default: 134 + return "any"; 135 + } 136 + } 137 + 138 + // Convert NSID to PascalCase 139 + function nsidToPascalCase(nsid: string): string { 140 + return ( 141 + nsid 142 + .split(".") 143 + .map((part) => 144 + part 145 + .split(/[-_]/) 146 + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 147 + .join("") 148 + ) 149 + .join("") + "Record" 150 + ); 151 + } 152 + 153 + // Capitalize first letter 154 + function capitalizeFirst(str: string): string { 155 + return str.charAt(0).toUpperCase() + str.slice(1); 156 + } 157 + 158 + // Check if property is required 159 + function isPropertyRequired( 160 + recordObj: LexiconRecord, 161 + propName: string 162 + ): boolean { 163 + return Boolean(recordObj.required && recordObj.required.includes(propName)); 164 + } 165 + 166 + // Add lexicon-specific interfaces 167 + function addLexiconInterfaces(): void { 168 + for (const lexicon of lexicons) { 169 + const interfaceName = nsidToPascalCase(lexicon.nsid); 170 + 171 + if (lexicon.definitions && typeof lexicon.definitions === "object") { 172 + for (const [, defValue] of Object.entries(lexicon.definitions)) { 173 + if (defValue.type === "record" && defValue.record) { 174 + const recordDef = defValue.record; 175 + const properties: Array<{ 176 + name: string; 177 + type: string; 178 + hasQuestionToken: boolean; 179 + docs?: string[]; 180 + }> = []; 181 + 182 + if (recordDef.properties) { 183 + for (const [propName, propDef] of Object.entries( 184 + recordDef.properties 185 + )) { 186 + const tsType = convertLexiconTypeToTypeScript(propDef); 187 + const required = isPropertyRequired(recordDef, propName); 188 + 189 + properties.push({ 190 + name: propName, 191 + type: tsType, 192 + hasQuestionToken: !required, 193 + // Add JSDoc comment if description exists 194 + docs: propDef.description ? [propDef.description] : undefined, 195 + }); 196 + } 197 + } 198 + 199 + sourceFile.addInterface({ 200 + name: interfaceName, 201 + isExported: true, 202 + properties: properties, 203 + }); 204 + } 205 + } 206 + } 207 + } 208 + } 209 + 210 + // Add base client class with shared request logic 211 + function addBaseClientClass(): void { 212 + sourceFile.addClass({ 213 + name: "BaseClient", 214 + properties: [ 215 + { name: "baseUrl", type: "string", scope: "protected", isReadonly: true }, 216 + ], 217 + ctors: [ 218 + { 219 + parameters: [{ name: "baseUrl", type: "string" }], 220 + statements: ["this.baseUrl = baseUrl;"], 221 + }, 222 + ], 223 + methods: [ 224 + { 225 + name: "makeRequest", 226 + scope: "protected", 227 + isAsync: true, 228 + parameters: [ 229 + { name: "endpoint", type: "string" }, 230 + { 231 + name: "method", 232 + type: '"GET" | "POST" | "PUT" | "DELETE"', 233 + hasQuestionToken: true, 234 + }, 235 + { name: "params", type: "any", hasQuestionToken: true }, 236 + ], 237 + returnType: "Promise<any>", 238 + statements: [ 239 + `const httpMethod = method || 'GET';`, 240 + `let url = \`\${this.baseUrl}/xrpc/\${endpoint}\`;`, 241 + `let requestInit: RequestInit = {`, 242 + ` method: httpMethod`, 243 + `};`, 244 + ``, 245 + `if (httpMethod === 'GET' && params) {`, 246 + ` const searchParams = new URLSearchParams();`, 247 + ` Object.entries(params).forEach(([key, value]) => {`, 248 + ` if (value !== undefined && value !== null) {`, 249 + ` searchParams.append(key, String(value));`, 250 + ` }`, 251 + ` });`, 252 + ` const queryString = searchParams.toString();`, 253 + ` if (queryString) {`, 254 + ` url += '?' + queryString;`, 255 + ` }`, 256 + `} else if (httpMethod !== 'GET' && params) {`, 257 + ` requestInit.headers = { 'Content-Type': 'application/json' };`, 258 + ` requestInit.body = JSON.stringify(params);`, 259 + `}`, 260 + ``, 261 + `const response = await fetch(url, requestInit);`, 262 + `if (!response.ok) {`, 263 + ` throw new Error(\`Request failed: \${response.status} \${response.statusText}\`);`, 264 + `}`, 265 + `return await response.json();`, 266 + ], 267 + }, 268 + ], 269 + }); 270 + } 271 + 272 + interface NestedStructure { 273 + [key: string]: NestedStructure | string | undefined; 274 + _recordType?: string; 275 + _collectionPath?: string; 276 + } 277 + 278 + interface PropertyInfo { 279 + name: string; 280 + type: string; 281 + } 282 + 283 + interface MethodInfo { 284 + name: string; 285 + parameters: Array<{ name: string; type: string; hasQuestionToken?: boolean }>; 286 + returnType: string; 287 + } 288 + 289 + // Add client class with nested collections 290 + function addClientClass(): void { 291 + // Create nested structure from lexicons 292 + const nestedStructure: NestedStructure = {}; 293 + 294 + for (const lexicon of lexicons) { 295 + if (lexicon.definitions && typeof lexicon.definitions === "object") { 296 + for (const [, defValue] of Object.entries(lexicon.definitions)) { 297 + if (defValue.type === "record" && defValue.record) { 298 + const parts = lexicon.nsid.split("."); 299 + let current = nestedStructure; 300 + 301 + // Build nested structure 302 + for (const part of parts) { 303 + if (!current[part]) { 304 + current[part] = {}; 305 + } 306 + current = current[part] as NestedStructure; 307 + } 308 + 309 + // Add the record interface name and store collection path 310 + current._recordType = nsidToPascalCase(lexicon.nsid); 311 + current._collectionPath = lexicon.nsid; 312 + } 313 + } 314 + } 315 + } 316 + 317 + // Generate nested class structure 318 + function generateNestedClass( 319 + obj: NestedStructure, 320 + className = "CollectionNode", 321 + currentPath: string[] = [] 322 + ): void { 323 + const properties: PropertyInfo[] = []; 324 + const methods: MethodInfo[] = []; 325 + 326 + let collectionPath = ""; 327 + 328 + for (const [key, value] of Object.entries(obj)) { 329 + if (key === "_recordType") { 330 + // Add collection operations for this record type 331 + methods.push({ 332 + name: "listRecords", 333 + parameters: [ 334 + { 335 + name: "params", 336 + type: "ListRecordsParams", 337 + hasQuestionToken: true, 338 + }, 339 + ], 340 + returnType: `Promise<ListRecordsResponse<${value}>>`, 341 + }); 342 + methods.push({ 343 + name: "getRecord", 344 + parameters: [{ name: "params", type: "GetRecordParams" }], 345 + returnType: `Promise<RecordResponse<${value}>>`, 346 + }); 347 + } else if (key === "_collectionPath") { 348 + collectionPath = value as string; 349 + } else if (typeof value === "object" && Object.keys(value).length > 0) { 350 + // Add nested property with PascalCase class name 351 + const nestedClassName = `${capitalizeFirst(key)}${className}`; 352 + generateNestedClass(value as NestedStructure, nestedClassName, [ 353 + ...currentPath, 354 + key, 355 + ]); 356 + properties.push({ 357 + name: key, 358 + type: nestedClassName, 359 + }); 360 + } 361 + } 362 + 363 + if (properties.length > 0 || methods.length > 0) { 364 + // Use proper naming for the main client 365 + const finalClassName = 366 + className === "Client" ? "AtProtoClient" : className; 367 + 368 + const classDeclaration = sourceFile.addClass({ 369 + name: finalClassName, 370 + isExported: className === "Client", 371 + extends: "BaseClient", 372 + properties: [ 373 + ...properties.map((p) => ({ 374 + name: p.name, 375 + type: p.type, 376 + isReadonly: true, 377 + })), 378 + ], 379 + }); 380 + 381 + // Add constructor 382 + const ctor = classDeclaration.addConstructor({ 383 + parameters: [{ name: "baseUrl", type: "string" }], 384 + }); 385 + ctor.addStatements([ 386 + "super(baseUrl);", 387 + ...properties.map((p) => `this.${p.name} = new ${p.type}(baseUrl);`), 388 + ]); 389 + 390 + // Add methods with implementations 391 + for (const method of methods) { 392 + const methodDecl = classDeclaration.addMethod({ 393 + name: method.name, 394 + parameters: method.parameters, 395 + returnType: method.returnType, 396 + isAsync: true, 397 + }); 398 + 399 + // Add basic implementation using shared makeRequest method 400 + if (method.name === "listRecords") { 401 + methodDecl.addStatements([ 402 + `return await this.makeRequest('${collectionPath}.list', 'GET', params);`, 403 + ]); 404 + } else if (method.name === "getRecord") { 405 + methodDecl.addStatements([ 406 + `return await this.makeRequest('${collectionPath}.get', 'GET', params);`, 407 + ]); 408 + } 409 + } 410 + } 411 + } 412 + 413 + // Generate the main client class 414 + if (Object.keys(nestedStructure).length > 0) { 415 + generateNestedClass(nestedStructure, "Client"); 416 + } 417 + } 418 + 419 + // Generate the TypeScript 420 + addBaseInterfaces(); 421 + addLexiconInterfaces(); 422 + addBaseClientClass(); 423 + addClientClass(); 424 + 425 + // Get the generated code and add header 426 + const generatedCode = sourceFile.getFullText(); 427 + const finalCode = headerComment + generatedCode; 428 + 429 + // Output to stdout for the Rust handler to capture 430 + // @ts-ignore 431 + Deno.stdout.writeSync(new TextEncoder().encode(finalCode));
+154
api/scripts/generated_client.ts
··· 1 + // Generated TypeScript client for AT Protocol records 2 + // Generated at: 2025-08-18 03:54:49 UTC 3 + // Lexicons: 2 4 + 5 + export interface RecordResponse<T extends any> { 6 + uri: string; 7 + cid: string; 8 + did: string; 9 + collection: string; 10 + value: T; 11 + indexed_at: string; 12 + } 13 + 14 + export interface ListRecordsResponse<T extends any> { 15 + records: RecordResponse<T>[]; 16 + cursor?: string; 17 + } 18 + 19 + export interface ListRecordsParams { 20 + author?: string; 21 + limit?: number; 22 + cursor?: string; 23 + } 24 + 25 + export interface GetRecordParams { 26 + uri: string; 27 + } 28 + 29 + export interface CollectionOperations<T> { 30 + listRecords(params?: ListRecordsParams): Promise<ListRecordsResponse<T>>; 31 + getRecord(params: GetRecordParams): Promise<RecordResponse<T>>; 32 + } 33 + 34 + export interface SocialGrainGalleryRecord { 35 + createdAt: string; 36 + description?: string; 37 + /** Annotations of description text (mentions, URLs, hashtags, etc) */ 38 + facets?: any[]; 39 + /** Self-label values for this post. Effectively content warnings. */ 40 + labels?: any; 41 + title: string; 42 + updatedAt?: string; 43 + } 44 + 45 + export interface SocialGrainCommentRecord { 46 + createdAt: string; 47 + /** Annotations of description text (mentions and URLs, hashtags, etc) */ 48 + facets?: any[]; 49 + focus?: string; 50 + replyTo?: string; 51 + subject: string; 52 + text: string; 53 + } 54 + 55 + class BaseClient { 56 + protected readonly baseUrl: string; 57 + 58 + constructor(baseUrl: string) { 59 + this.baseUrl = baseUrl; 60 + } 61 + 62 + protected async makeRequest(endpoint: string, method?: "GET" | "POST" | "PUT" | "DELETE", params?: any): Promise<any> { 63 + const httpMethod = method || 'GET'; 64 + let url = `${this.baseUrl}/xrpc/${endpoint}`; 65 + let requestInit: RequestInit = { 66 + method: httpMethod 67 + }; 68 + 69 + if (httpMethod === 'GET' && params) { 70 + const searchParams = new URLSearchParams(); 71 + Object.entries(params).forEach(([key, value]) => { 72 + if (value !== undefined && value !== null) { 73 + searchParams.append(key, String(value)); 74 + } 75 + 76 + }); 77 + const queryString = searchParams.toString(); 78 + if (queryString) { 79 + url += '?' + queryString; 80 + } 81 + 82 + } else if (httpMethod !== 'GET' && params) { 83 + requestInit.headers = { 'Content-Type': 'application/json' }; 84 + requestInit.body = JSON.stringify(params); 85 + } 86 + 87 + 88 + 89 + const response = await fetch(url, requestInit); 90 + if (!response.ok) { 91 + throw new Error(`Request failed: ${response.status} ${response.statusText}`); 92 + } 93 + 94 + return await response.json(); 95 + } 96 + } 97 + 98 + class GalleryGrainSocialClient extends BaseClient { 99 + constructor(baseUrl: string) { 100 + super(baseUrl); 101 + } 102 + 103 + async listRecords(params?: ListRecordsParams): Promise<ListRecordsResponse<SocialGrainGalleryRecord>> { 104 + return await this.makeRequest('social.grain.gallery.list', 'GET', params); 105 + } 106 + 107 + async getRecord(params: GetRecordParams): Promise<RecordResponse<SocialGrainGalleryRecord>> { 108 + return await this.makeRequest('social.grain.gallery.get', 'GET', params); 109 + } 110 + } 111 + 112 + class CommentGrainSocialClient extends BaseClient { 113 + constructor(baseUrl: string) { 114 + super(baseUrl); 115 + } 116 + 117 + async listRecords(params?: ListRecordsParams): Promise<ListRecordsResponse<SocialGrainCommentRecord>> { 118 + return await this.makeRequest('social.grain.comment.list', 'GET', params); 119 + } 120 + 121 + async getRecord(params: GetRecordParams): Promise<RecordResponse<SocialGrainCommentRecord>> { 122 + return await this.makeRequest('social.grain.comment.get', 'GET', params); 123 + } 124 + } 125 + 126 + class GrainSocialClient extends BaseClient { 127 + readonly gallery: GalleryGrainSocialClient; 128 + readonly comment: CommentGrainSocialClient; 129 + 130 + constructor(baseUrl: string) { 131 + super(baseUrl); 132 + this.gallery = new GalleryGrainSocialClient(baseUrl); 133 + this.comment = new CommentGrainSocialClient(baseUrl); 134 + } 135 + } 136 + 137 + class SocialClient extends BaseClient { 138 + readonly grain: GrainSocialClient; 139 + 140 + constructor(baseUrl: string) { 141 + super(baseUrl); 142 + this.grain = new GrainSocialClient(baseUrl); 143 + } 144 + } 145 + 146 + export class AtProtoClient extends BaseClient { 147 + readonly social: SocialClient; 148 + 149 + constructor(baseUrl: string) { 150 + super(baseUrl); 151 + this.social = new SocialClient(baseUrl); 152 + } 153 + } 154 +
+34
api/scripts/test_codegen.sh
··· 1 + #!/bin/bash 2 + 3 + echo "🧪 Testing TypeScript Code Generation..." 4 + 5 + # Test with multiple lexicons 6 + echo "📝 Generating TypeScript client with multiple lexicons..." 7 + curl -s -X POST http://localhost:3000/xrpc/com.indexer.codegen.generate \ 8 + -H "Content-Type: application/json" \ 9 + -d '{ 10 + "target": "typescript-deno", 11 + "client_type": "records", 12 + "lexicons": ["social.grain.gallery", "social.grain.comment"] 13 + }' | jq -r '.generated_code' > generated_client.ts 14 + 15 + if [ $? -eq 0 ] && [ -f generated_client.ts ]; then 16 + echo "✅ Generated client saved to generated_client.ts" 17 + echo "📊 Generated code stats:" 18 + echo " Lines: $(wc -l < generated_client.ts)" 19 + echo " Size: $(du -h generated_client.ts | cut -f1)" 20 + 21 + echo "" 22 + echo "🔍 Preview of generated interfaces:" 23 + grep -A 5 "export interface.*Record {" generated_client.ts || echo "No record interfaces found" 24 + 25 + echo "" 26 + echo "🎯 Preview of auto-typing examples:" 27 + grep -A 10 "// Usage examples:" generated_client.ts || echo "No usage examples found" 28 + else 29 + echo "❌ Failed to generate client" 30 + exit 1 31 + fi 32 + 33 + echo "" 34 + echo "🎉 Test complete! Check generated_client.ts to see the auto-typing functionality."
+3
api/src/codegen/mod.rs
··· 1 + pub mod typescript; 2 + 3 + pub use typescript::TypeScriptGenerator;
+34
api/src/codegen/typescript.rs
··· 1 + use crate::models::Lexicon; 2 + 3 + pub struct TypeScriptGenerator; 4 + 5 + impl TypeScriptGenerator { 6 + pub fn new() -> Self { 7 + Self 8 + } 9 + 10 + pub fn generate_client(&self, lexicons: &[Lexicon]) -> Result<String, String> { 11 + // Serialize lexicons to JSON for the Deno script 12 + let lexicons_json = serde_json::to_string(lexicons) 13 + .map_err(|e| format!("Failed to serialize lexicons: {}", e))?; 14 + 15 + // Call the Deno script to generate TypeScript with proper comments 16 + let output = std::process::Command::new("deno") 17 + .arg("run") 18 + .arg("--allow-all") 19 + .arg("scripts/generate-typescript.ts") 20 + .arg(&lexicons_json) 21 + .output() 22 + .map_err(|e| format!("Failed to execute deno script: {}", e))?; 23 + 24 + if !output.status.success() { 25 + let stderr = String::from_utf8_lossy(&output.stderr); 26 + return Err(format!("Deno script failed: {}", stderr)); 27 + } 28 + 29 + let generated_code = String::from_utf8(output.stdout) 30 + .map_err(|e| format!("Failed to decode output: {}", e))?; 31 + 32 + Ok(generated_code) 33 + } 34 + }
+238
api/src/database.rs
··· 1 + use sqlx::PgPool; 2 + 3 + use crate::errors::DatabaseError; 4 + use crate::models::{Actor, IndexedRecord, Lexicon, ListRecordsParams, Record}; 5 + 6 + #[derive(Clone)] 7 + pub struct Database { 8 + pool: PgPool, 9 + } 10 + 11 + impl Database { 12 + pub fn new(pool: PgPool) -> Self { 13 + Self { pool } 14 + } 15 + 16 + #[allow(dead_code)] 17 + pub async fn insert_record(&self, record: &Record) -> Result<(), DatabaseError> { 18 + sqlx::query!( 19 + r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 20 + VALUES ($1, $2, $3, $4, $5, $6) 21 + ON CONFLICT ("uri") 22 + DO UPDATE SET 23 + "cid" = EXCLUDED."cid", 24 + "json" = EXCLUDED."json", 25 + "indexedAt" = EXCLUDED."indexedAt""#, 26 + record.uri, 27 + record.cid, 28 + record.did, 29 + record.collection, 30 + record.json, 31 + record.indexed_at 32 + ) 33 + .execute(&self.pool) 34 + .await?; 35 + 36 + Ok(()) 37 + } 38 + 39 + pub async fn batch_insert_records(&self, records: &[Record]) -> Result<(), DatabaseError> { 40 + let mut tx = self.pool.begin().await?; 41 + 42 + for record in records { 43 + sqlx::query!( 44 + r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 45 + VALUES ($1, $2, $3, $4, $5, $6) 46 + ON CONFLICT ("uri") 47 + DO UPDATE SET 48 + "cid" = EXCLUDED."cid", 49 + "json" = EXCLUDED."json", 50 + "indexedAt" = EXCLUDED."indexedAt""#, 51 + record.uri, 52 + record.cid, 53 + record.did, 54 + record.collection, 55 + record.json, 56 + record.indexed_at 57 + ) 58 + .execute(&mut *tx) 59 + .await?; 60 + } 61 + 62 + tx.commit().await?; 63 + Ok(()) 64 + } 65 + 66 + #[allow(dead_code)] 67 + pub async fn get_record(&self, uri: &str) -> Result<Option<Record>, DatabaseError> { 68 + let record = sqlx::query_as::<_, Record>( 69 + r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 70 + FROM "record" 71 + WHERE "uri" = $1"#, 72 + ) 73 + .bind(uri) 74 + .fetch_optional(&self.pool) 75 + .await?; 76 + 77 + Ok(record) 78 + } 79 + 80 + pub async fn list_records(&self, params: ListRecordsParams) -> Result<Vec<IndexedRecord>, DatabaseError> { 81 + let limit = params.limit.unwrap_or(25).min(100); 82 + 83 + let records = sqlx::query_as::<_, Record>( 84 + r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 85 + FROM "record" 86 + WHERE "collection" = $1 87 + AND ($2::text IS NULL OR "did" = $2) 88 + ORDER BY "indexedAt" DESC 89 + LIMIT $3"#, 90 + ) 91 + .bind(&params.collection) 92 + .bind(&params.author) 93 + .bind(limit) 94 + .fetch_all(&self.pool) 95 + .await?; 96 + 97 + let indexed_records: Vec<IndexedRecord> = records 98 + .into_iter() 99 + .map(|record| IndexedRecord { 100 + uri: record.uri, 101 + cid: record.cid, 102 + did: record.did, 103 + collection: record.collection, 104 + value: record.json, 105 + indexed_at: record.indexed_at.to_rfc3339(), 106 + }) 107 + .collect(); 108 + 109 + Ok(indexed_records) 110 + } 111 + 112 + pub async fn get_available_collections(&self) -> Result<Vec<(String, i64)>, DatabaseError> { 113 + let collections = sqlx::query!( 114 + r#"SELECT "collection", COUNT(*) as count 115 + FROM "record" 116 + GROUP BY "collection" 117 + ORDER BY count DESC, "collection" ASC"# 118 + ) 119 + .fetch_all(&self.pool) 120 + .await?; 121 + 122 + Ok(collections 123 + .into_iter() 124 + .map(|row| (row.collection, row.count.unwrap_or(0))) 125 + .collect()) 126 + } 127 + 128 + pub async fn get_total_record_count(&self) -> Result<i64, DatabaseError> { 129 + let count = sqlx::query!("SELECT COUNT(*) as count FROM record") 130 + .fetch_one(&self.pool) 131 + .await?; 132 + 133 + Ok(count.count.unwrap_or(0)) 134 + } 135 + 136 + pub async fn insert_lexicon(&self, lexicon: &Lexicon) -> Result<(), DatabaseError> { 137 + sqlx::query!( 138 + r#"INSERT INTO "lexicons" ("nsid", "definitions", "created_at", "updated_at") 139 + VALUES ($1, $2, $3, $4) 140 + ON CONFLICT ("nsid") 141 + DO UPDATE SET 142 + "definitions" = EXCLUDED."definitions", 143 + "updated_at" = EXCLUDED."updated_at""#, 144 + lexicon.nsid, 145 + lexicon.definitions, 146 + lexicon.created_at, 147 + lexicon.updated_at 148 + ) 149 + .execute(&self.pool) 150 + .await?; 151 + 152 + Ok(()) 153 + } 154 + 155 + pub async fn get_lexicon(&self, nsid: &str) -> Result<Option<Lexicon>, DatabaseError> { 156 + let lexicon = sqlx::query_as::<_, Lexicon>( 157 + r#"SELECT "nsid", "definitions", "created_at", "updated_at" 158 + FROM "lexicons" 159 + WHERE "nsid" = $1"#, 160 + ) 161 + .bind(nsid) 162 + .fetch_optional(&self.pool) 163 + .await?; 164 + 165 + Ok(lexicon) 166 + } 167 + 168 + pub async fn get_all_lexicons(&self) -> Result<Vec<Lexicon>, DatabaseError> { 169 + let lexicons = sqlx::query_as::<_, Lexicon>( 170 + r#"SELECT "nsid", "definitions", "created_at", "updated_at" 171 + FROM "lexicons" 172 + ORDER BY "nsid""#, 173 + ) 174 + .fetch_all(&self.pool) 175 + .await?; 176 + 177 + Ok(lexicons) 178 + } 179 + 180 + pub async fn update_record(&self, record: &Record) -> Result<(), DatabaseError> { 181 + let result = sqlx::query!( 182 + r#"UPDATE "record" 183 + SET "cid" = $1, "json" = $2, "indexedAt" = $3 184 + WHERE "uri" = $4"#, 185 + record.cid, 186 + record.json, 187 + record.indexed_at, 188 + record.uri 189 + ) 190 + .execute(&self.pool) 191 + .await?; 192 + 193 + if result.rows_affected() == 0 { 194 + return Err(DatabaseError::RecordNotFound { uri: record.uri.clone() }); 195 + } 196 + 197 + Ok(()) 198 + } 199 + 200 + pub async fn delete_record(&self, uri: &str) -> Result<(), DatabaseError> { 201 + let result = sqlx::query!( 202 + r#"DELETE FROM "record" WHERE "uri" = $1"#, 203 + uri 204 + ) 205 + .execute(&self.pool) 206 + .await?; 207 + 208 + if result.rows_affected() == 0 { 209 + return Err(DatabaseError::RecordNotFound { uri: uri.to_string() }); 210 + } 211 + 212 + Ok(()) 213 + } 214 + 215 + pub async fn batch_insert_actors(&self, actors: &[Actor]) -> Result<(), DatabaseError> { 216 + let mut tx = self.pool.begin().await?; 217 + 218 + for actor in actors { 219 + sqlx::query!( 220 + r#"INSERT INTO "actor" ("did", "handle", "indexedAt") 221 + VALUES ($1, $2, $3) 222 + ON CONFLICT ("did") 223 + DO UPDATE SET 224 + "handle" = EXCLUDED."handle", 225 + "indexedAt" = EXCLUDED."indexedAt""#, 226 + actor.did, 227 + actor.handle, 228 + actor.indexed_at 229 + ) 230 + .execute(&mut *tx) 231 + .await?; 232 + } 233 + 234 + tx.commit().await?; 235 + Ok(()) 236 + } 237 + 238 + }
+71
api/src/errors.rs
··· 1 + use thiserror::Error; 2 + 3 + #[derive(Error, Debug)] 4 + pub enum LexiconError { 5 + #[error("error-slice-lexicon-1 Failed to parse multipart boundary: {0}")] 6 + MultipartBoundary(String), 7 + 8 + #[error("error-slice-lexicon-2 Failed to read request body: {0}")] 9 + RequestBody(String), 10 + 11 + #[error("error-slice-lexicon-3 Failed to parse zip archive: {0}")] 12 + ZipArchive(String), 13 + 14 + #[error("error-slice-lexicon-4 Failed to read file from archive: {0}")] 15 + FileRead(String), 16 + 17 + #[error("error-slice-lexicon-5 Failed to parse JSON lexicon: {0}")] 18 + JsonParse(String), 19 + } 20 + 21 + #[derive(Error, Debug)] 22 + pub enum DatabaseError { 23 + #[error("error-slice-database-1 SQL query failed: {0}")] 24 + SqlQuery(#[from] sqlx::Error), 25 + 26 + #[error("error-slice-database-2 Transaction failed: {0}")] 27 + Transaction(String), 28 + 29 + #[error("error-slice-database-3 Record not found: {uri}")] 30 + RecordNotFound { uri: String }, 31 + } 32 + 33 + #[derive(Error, Debug)] 34 + pub enum SyncError { 35 + #[error("error-slice-sync-1 HTTP request failed: {0}")] 36 + HttpRequest(#[from] reqwest::Error), 37 + 38 + #[error("error-slice-sync-2 Database operation failed: {0}")] 39 + Database(#[from] DatabaseError), 40 + 41 + #[error("error-slice-sync-3 JSON parsing failed: {0}")] 42 + JsonParse(#[from] serde_json::Error), 43 + 44 + #[error("error-slice-sync-4 Failed to list repos for collection: {status}")] 45 + ListRepos { status: u16 }, 46 + 47 + #[error("error-slice-sync-5 Failed to list records: {status}")] 48 + ListRecords { status: u16 }, 49 + 50 + #[error("error-slice-sync-6 Task join failed: {0}")] 51 + TaskJoin(#[from] tokio::task::JoinError), 52 + 53 + #[error("error-slice-sync-7 Generic error: {0}")] 54 + Generic(String), 55 + } 56 + 57 + #[derive(Error, Debug)] 58 + pub enum AppError { 59 + #[error("error-slice-app-1 Database connection failed: {0}")] 60 + DatabaseConnection(#[from] sqlx::Error), 61 + 62 + #[error("error-slice-app-2 Migration failed: {0}")] 63 + Migration(#[from] sqlx::migrate::MigrateError), 64 + 65 + #[error("error-slice-app-3 Server bind failed: {0}")] 66 + ServerBind(#[from] std::io::Error), 67 + 68 + #[error("error-slice-app-4 Environment variable error: {0}")] 69 + Environment(String), 70 + } 71 +
+103
api/src/handler_codegen.rs
··· 1 + use axum::{ 2 + extract::State, 3 + http::StatusCode, 4 + response::{Html, IntoResponse}, 5 + }; 6 + use axum_extra::extract::Form; 7 + use minijinja::{context, Environment}; 8 + use serde::Deserialize; 9 + use crate::AppState; 10 + use crate::codegen::TypeScriptGenerator; 11 + 12 + #[derive(Deserialize)] 13 + pub struct CodegenForm { 14 + target: String, 15 + client_type: String, 16 + #[serde(default)] 17 + lexicons: Vec<String>, 18 + } 19 + 20 + 21 + pub async fn generate_client( 22 + State(state): State<AppState>, 23 + Form(form): Form<CodegenForm>, 24 + ) -> Result<impl IntoResponse, StatusCode> { 25 + let selected_lexicons = form.lexicons; 26 + 27 + if selected_lexicons.is_empty() { 28 + return Ok(Html(r#" 29 + <div class="alert alert-error"> 30 + <h4>❌ No lexicons selected</h4> 31 + <p>Please select at least one lexicon to generate client code.</p> 32 + </div> 33 + "#.to_string())); 34 + } 35 + 36 + // Fetch the selected lexicons from database 37 + let mut lexicons = Vec::new(); 38 + for nsid in &selected_lexicons { 39 + if let Ok(Some(lexicon)) = state.database.get_lexicon(nsid).await { 40 + lexicons.push(lexicon); 41 + } 42 + } 43 + 44 + let generated_code = match form.target.as_str() { 45 + "typescript-deno" => match form.client_type.as_str() { 46 + "records" => { 47 + let generator = TypeScriptGenerator::new(); 48 + match generator.generate_client(&lexicons) { 49 + Ok(code) => code, 50 + Err(e) => return Ok(Html(format!(r#" 51 + <div class="alert alert-error"> 52 + <h4>❌ TypeScript generation failed</h4> 53 + <p>Error: {}</p> 54 + </div> 55 + "#, e))), 56 + } 57 + }, 58 + _ => return Ok(Html(r#" 59 + <div class="alert alert-error"> 60 + <h4>❌ Unsupported client type</h4> 61 + <p>Only "records" client type is currently supported.</p> 62 + </div> 63 + "#.to_string())), 64 + }, 65 + _ => return Ok(Html(r#" 66 + <div class="alert alert-error"> 67 + <h4>❌ Unsupported target</h4> 68 + <p>Only "typescript-deno" is currently supported.</p> 69 + </div> 70 + "#.to_string())), 71 + }; 72 + 73 + let mut env = Environment::new(); 74 + env.add_template("codegen_result.html", r#" 75 + <div class="alert alert-success"> 76 + <h4>✅ Client code generated successfully!</h4> 77 + <p><strong>Target:</strong> {{ target }}</p> 78 + <p><strong>Client Type:</strong> {{ client_type }}</p> 79 + <p><strong>Lexicons:</strong> {{ lexicons_count }}</p> 80 + 81 + <div class="mt-4"> 82 + <div class="flex justify-between items-center mb-2"> 83 + <h5 class="font-medium text-gray-800">Generated Code</h5> 84 + <button onclick="navigator.clipboard.writeText(document.getElementById('generated-code').textContent);" 85 + class="bg-blue-500 text-white px-3 py-1 rounded text-sm"> 86 + Copy to Clipboard 87 + </button> 88 + </div> 89 + <pre id="generated-code" class="bg-gray-100 p-4 rounded text-xs overflow-x-auto max-h-96 overflow-y-auto">{{ generated_code }}</pre> 90 + </div> 91 + </div> 92 + "#).unwrap(); 93 + 94 + let tmpl = env.get_template("codegen_result.html").unwrap(); 95 + let rendered = tmpl.render(context! { 96 + target => form.target, 97 + client_type => form.client_type, 98 + lexicons_count => lexicons.len(), 99 + generated_code => generated_code 100 + }).unwrap(); 101 + 102 + Ok(Html(rendered)) 103 + }
+249
api/src/handler_dynamic_xrpc.rs
··· 1 + use axum::{ 2 + extract::{Path, Query, State}, 3 + http::StatusCode, 4 + response::Json, 5 + }; 6 + use serde::{Deserialize, Serialize}; 7 + use chrono::Utc; 8 + 9 + use crate::models::{ListRecordsParams, ListRecordsOutput, Record}; 10 + use crate::AppState; 11 + 12 + #[derive(Deserialize)] 13 + pub struct DynamicListParams { 14 + pub author: Option<String>, 15 + pub limit: Option<i32>, 16 + pub cursor: Option<String>, 17 + } 18 + 19 + #[derive(Deserialize)] 20 + pub struct GetRecordParams { 21 + pub uri: String, 22 + } 23 + 24 + #[derive(Deserialize)] 25 + pub struct CreateRecordParams { 26 + pub repo: String, 27 + pub collection: String, 28 + pub rkey: Option<String>, 29 + pub record: serde_json::Value, 30 + } 31 + 32 + #[derive(Deserialize)] 33 + pub struct UpdateRecordParams { 34 + pub repo: String, 35 + pub collection: String, 36 + pub rkey: String, 37 + pub record: serde_json::Value, 38 + } 39 + 40 + #[derive(Deserialize)] 41 + pub struct DeleteRecordParams { 42 + pub repo: String, 43 + pub collection: String, 44 + pub rkey: String, 45 + } 46 + 47 + #[derive(Serialize)] 48 + pub struct CreateRecordOutput { 49 + pub uri: String, 50 + pub cid: String, 51 + } 52 + 53 + // Dynamic XRPC handler that routes based on method name (for GET requests) 54 + pub async fn dynamic_xrpc_handler( 55 + Path(method): Path<String>, 56 + State(state): State<AppState>, 57 + Query(params): Query<serde_json::Value>, 58 + ) -> Result<Json<serde_json::Value>, StatusCode> { 59 + // Parse the XRPC method (e.g., "social.grain.gallery.list") 60 + if method.ends_with(".list") { 61 + let collection = method.trim_end_matches(".list").to_string(); 62 + dynamic_list_records_impl(collection, state, params).await 63 + } else if method.ends_with(".get") { 64 + let collection = method.trim_end_matches(".get").to_string(); 65 + dynamic_get_record_impl(collection, state, params).await 66 + } else { 67 + Err(StatusCode::NOT_FOUND) 68 + } 69 + } 70 + 71 + // Dynamic XRPC handler for POST requests (create, update, delete) 72 + pub async fn dynamic_xrpc_post_handler( 73 + Path(method): Path<String>, 74 + State(state): State<AppState>, 75 + Json(body): Json<serde_json::Value>, 76 + ) -> Result<Json<serde_json::Value>, StatusCode> { 77 + if method == "com.atproto.repo.createRecord" { 78 + dynamic_create_record_impl(state, body).await 79 + } else if method == "com.atproto.repo.putRecord" { 80 + dynamic_update_record_impl(state, body).await 81 + } else if method == "com.atproto.repo.deleteRecord" { 82 + dynamic_delete_record_impl(state, body).await 83 + } else { 84 + Err(StatusCode::NOT_FOUND) 85 + } 86 + } 87 + 88 + // Implementation for list records 89 + async fn dynamic_list_records_impl( 90 + collection: String, 91 + state: AppState, 92 + params: serde_json::Value, 93 + ) -> Result<Json<serde_json::Value>, StatusCode> { 94 + let dynamic_params: DynamicListParams = serde_json::from_value(params) 95 + .map_err(|_| StatusCode::BAD_REQUEST)?; 96 + 97 + let list_params = ListRecordsParams { 98 + collection, 99 + author: dynamic_params.author, 100 + limit: dynamic_params.limit, 101 + cursor: dynamic_params.cursor, 102 + }; 103 + 104 + match state.database.list_records(list_params).await { 105 + Ok(records) => { 106 + let output = ListRecordsOutput { 107 + records, 108 + cursor: None, // TODO: implement cursor pagination 109 + }; 110 + let json_value = serde_json::to_value(output) 111 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 112 + Ok(Json(json_value)) 113 + }, 114 + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 115 + } 116 + } 117 + 118 + // Implementation for get record 119 + async fn dynamic_get_record_impl( 120 + collection: String, 121 + state: AppState, 122 + params: serde_json::Value, 123 + ) -> Result<Json<serde_json::Value>, StatusCode> { 124 + let get_params: GetRecordParams = serde_json::from_value(params) 125 + .map_err(|_| StatusCode::BAD_REQUEST)?; 126 + 127 + // Extract the record key from the URI 128 + // AT Protocol URIs are like: at://did:plc:example/collection/rkey 129 + let uri_parts: Vec<&str> = get_params.uri.split('/').collect(); 130 + if uri_parts.len() < 3 { 131 + return Err(StatusCode::BAD_REQUEST); 132 + } 133 + 134 + // For now, we'll use the existing list_records with a filter 135 + // In a real implementation, you'd want a dedicated get_record method 136 + let list_params = ListRecordsParams { 137 + collection, 138 + author: None, 139 + limit: Some(1), 140 + cursor: None, 141 + }; 142 + 143 + match state.database.list_records(list_params).await { 144 + Ok(records) => { 145 + // Find the record with matching URI 146 + if let Some(record) = records.into_iter().find(|r| r.uri == get_params.uri) { 147 + let json_value = serde_json::to_value(record) 148 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 149 + Ok(Json(json_value)) 150 + } else { 151 + Err(StatusCode::NOT_FOUND) 152 + } 153 + }, 154 + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 155 + } 156 + } 157 + 158 + // Implementation for create record 159 + async fn dynamic_create_record_impl( 160 + state: AppState, 161 + body: serde_json::Value, 162 + ) -> Result<Json<serde_json::Value>, StatusCode> { 163 + let params: CreateRecordParams = serde_json::from_value(body) 164 + .map_err(|_| StatusCode::BAD_REQUEST)?; 165 + 166 + // Generate a record key if not provided (using timestamp) 167 + let rkey = params.rkey.unwrap_or_else(|| { 168 + // Simple TID-like generation using timestamp 169 + let now = Utc::now(); 170 + now.format("%Y%m%dT%H%M%S").to_string() 171 + }); 172 + 173 + // Construct the AT-URI 174 + let uri = format!("at://{}/{}/{}", params.repo, params.collection, rkey); 175 + 176 + // Generate a simple CID (in a real implementation, this would be a proper CID) 177 + let cid = format!("baf{}", &uri.chars().take(50).collect::<String>().replace(":", "").replace("/", "")); 178 + 179 + let record = Record { 180 + uri: uri.clone(), 181 + cid: cid.clone(), 182 + did: params.repo, 183 + collection: params.collection, 184 + json: params.record, 185 + indexed_at: Utc::now(), 186 + }; 187 + 188 + match state.database.insert_record(&record).await { 189 + Ok(_) => { 190 + let output = CreateRecordOutput { uri, cid }; 191 + let json_value = serde_json::to_value(output) 192 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 193 + Ok(Json(json_value)) 194 + }, 195 + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 196 + } 197 + } 198 + 199 + // Implementation for update record 200 + async fn dynamic_update_record_impl( 201 + state: AppState, 202 + body: serde_json::Value, 203 + ) -> Result<Json<serde_json::Value>, StatusCode> { 204 + let params: UpdateRecordParams = serde_json::from_value(body) 205 + .map_err(|_| StatusCode::BAD_REQUEST)?; 206 + 207 + let uri = format!("at://{}/{}/{}", params.repo, params.collection, params.rkey); 208 + 209 + // Generate a new CID for the updated record 210 + let cid = format!("baf{}", &uri.chars().take(50).collect::<String>().replace(":", "").replace("/", "")); 211 + 212 + let record = Record { 213 + uri: uri.clone(), 214 + cid: cid.clone(), 215 + did: params.repo, 216 + collection: params.collection, 217 + json: params.record, 218 + indexed_at: Utc::now(), 219 + }; 220 + 221 + match state.database.update_record(&record).await { 222 + Ok(_) => { 223 + let output = CreateRecordOutput { uri, cid }; 224 + let json_value = serde_json::to_value(output) 225 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 226 + Ok(Json(json_value)) 227 + }, 228 + Err(_) => Err(StatusCode::NOT_FOUND), 229 + } 230 + } 231 + 232 + // Implementation for delete record 233 + async fn dynamic_delete_record_impl( 234 + state: AppState, 235 + body: serde_json::Value, 236 + ) -> Result<Json<serde_json::Value>, StatusCode> { 237 + let params: DeleteRecordParams = serde_json::from_value(body) 238 + .map_err(|_| StatusCode::BAD_REQUEST)?; 239 + 240 + let uri = format!("at://{}/{}/{}", params.repo, params.collection, params.rkey); 241 + 242 + match state.database.delete_record(&uri).await { 243 + Ok(_) => { 244 + // Return empty success response 245 + Ok(Json(serde_json::json!({}))) 246 + }, 247 + Err(_) => Err(StatusCode::NOT_FOUND), 248 + } 249 + }
+38
api/src/handler_lexicon.rs
··· 1 + use axum::{ 2 + extract::State, 3 + http::StatusCode, 4 + response::{Html, IntoResponse}, 5 + }; 6 + use minijinja::{context, Environment}; 7 + 8 + use crate::AppState; 9 + 10 + pub async fn lexicon_page( 11 + State(state): State<AppState>, 12 + ) -> Result<impl IntoResponse, StatusCode> { 13 + let lexicons = state.database.get_all_lexicons().await.unwrap_or_default(); 14 + 15 + // Transform lexicons to include pretty-printed JSON 16 + let lexicons_with_pretty_json: Vec<serde_json::Value> = lexicons.into_iter().map(|lexicon| { 17 + let pretty_definitions = serde_json::to_string_pretty(&lexicon.definitions).unwrap_or_else(|_| "{}".to_string()); 18 + serde_json::json!({ 19 + "nsid": lexicon.nsid, 20 + "definitions": lexicon.definitions, 21 + "pretty_definitions": pretty_definitions, 22 + "created_at": lexicon.created_at, 23 + "updated_at": lexicon.updated_at 24 + }) 25 + }).collect(); 26 + 27 + let mut env = Environment::new(); 28 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 29 + env.add_template("lexicon.html", include_str!("../templates/lexicon.html")).unwrap(); 30 + 31 + let tmpl = env.get_template("lexicon.html").unwrap(); 32 + let rendered = tmpl.render(context! { 33 + title => "Lexicon Definitions", 34 + lexicons => lexicons_with_pretty_json 35 + }).unwrap(); 36 + 37 + Ok(Html(rendered)) 38 + }
+37
api/src/handler_oauth.rs
··· 1 + use axum::{ 2 + extract::State, 3 + http::StatusCode, 4 + response::Json, 5 + }; 6 + use serde::{Deserialize, Serialize}; 7 + 8 + use crate::AppState; 9 + 10 + #[derive(Deserialize)] 11 + pub struct OAuthAuthorizeParams { 12 + pub handle: String, 13 + } 14 + 15 + #[derive(Serialize)] 16 + pub struct OAuthAuthorizeResponse { 17 + pub success: bool, 18 + pub message: String, 19 + } 20 + 21 + pub async fn oauth_authorize( 22 + State(_state): State<AppState>, 23 + Json(params): Json<OAuthAuthorizeParams>, 24 + ) -> Result<Json<OAuthAuthorizeResponse>, StatusCode> { 25 + // TODO: Implement OAuth authorize flow 26 + // 1. Resolve handle to DID 27 + // 2. Discover user's PDS 28 + // 3. Initiate OAuth flow with user's PDS 29 + // 4. Return authorization URL or handle the callback 30 + 31 + let response = OAuthAuthorizeResponse { 32 + success: true, 33 + message: format!("OAuth authorize initiated for handle: {}", params.handle), 34 + }; 35 + 36 + Ok(Json(response)) 37 + }
+250
api/src/handler_upload_lexicon.rs
··· 1 + use axum::{ 2 + extract::{Request, State}, 3 + http::StatusCode, 4 + response::{Html, IntoResponse}, 5 + }; 6 + use axum::body::to_bytes; 7 + use multer::Multipart; 8 + use futures_util::stream::once; 9 + use crate::errors::LexiconError; 10 + use crate::models::Lexicon; 11 + use crate::AppState; 12 + use minijinja::{context, Environment}; 13 + use serde::Deserialize; 14 + use std::collections::HashSet; 15 + use std::io::Read; 16 + use tracing::{error, warn}; 17 + use zip::ZipArchive; 18 + use chrono::Utc; 19 + 20 + #[derive(Deserialize)] 21 + struct LexiconFile { 22 + id: String, 23 + defs: serde_json::Map<String, serde_json::Value>, 24 + } 25 + 26 + pub async fn upload_lexicons( 27 + State(state): State<AppState>, 28 + request: Request, 29 + ) -> Result<impl IntoResponse, StatusCode> { 30 + 31 + let boundary = request 32 + .headers() 33 + .get("content-type") 34 + .and_then(|ct| ct.to_str().ok()) 35 + .and_then(|ct| multer::parse_boundary(ct).ok()) 36 + .ok_or_else(|| { 37 + let err = LexiconError::MultipartBoundary("Missing or invalid content-type header".to_string()); 38 + error!("{}", err); 39 + StatusCode::BAD_REQUEST 40 + })?; 41 + 42 + let body = request.into_body(); 43 + let body_bytes = to_bytes(body, usize::MAX).await.map_err(|e| { 44 + let err = LexiconError::RequestBody(e.to_string()); 45 + error!("{}", err); 46 + StatusCode::BAD_REQUEST 47 + })?; 48 + 49 + 50 + let body_stream = once(async move { Ok::<_, multer::Error>(body_bytes) }); 51 + let mut multipart = Multipart::new(body_stream, boundary); 52 + let mut collections = HashSet::new(); 53 + let mut file_count = 0; 54 + let mut record_count = 0; 55 + 56 + while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? { 57 + if field.name() == Some("lexicon_file") { 58 + let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?; 59 + 60 + // Parse zip file 61 + let cursor = std::io::Cursor::new(data); 62 + let mut archive = ZipArchive::new(cursor).map_err(|e| { 63 + let err = LexiconError::ZipArchive(e.to_string()); 64 + error!("{}", err); 65 + StatusCode::BAD_REQUEST 66 + })?; 67 + 68 + 69 + for i in 0..archive.len() { 70 + let mut file = archive.by_index(i).map_err(|e| { 71 + let err = LexiconError::FileRead(format!("Failed to access file at index {}: {}", i, e)); 72 + error!("{}", err); 73 + StatusCode::INTERNAL_SERVER_ERROR 74 + })?; 75 + 76 + // Only process JSON files, skip macOS metadata files 77 + if file.name().ends_with(".json") && 78 + !file.name().contains("__MACOSX") && 79 + !file.name().starts_with("._") { 80 + 81 + let mut contents = String::new(); 82 + if let Err(e) = file.read_to_string(&mut contents) { 83 + let err = LexiconError::FileRead(format!("Failed to read {}: {}", file.name(), e)); 84 + warn!("{}", err); 85 + continue; // Skip this file and continue processing others 86 + } 87 + 88 + // Try to parse as lexicon 89 + match serde_json::from_str::<LexiconFile>(&contents) { 90 + Ok(lexicon_file) => { 91 + file_count += 1; 92 + 93 + // Look for record definitions first 94 + let mut has_record_def = false; 95 + for (_def_name, def_value) in &lexicon_file.defs { 96 + if let Some(def_obj) = def_value.as_object() { 97 + if let Some(type_val) = def_obj.get("type") { 98 + if type_val == "record" { 99 + // This is a record definition - for AT Protocol listRecords, we only use the NSID 100 + // Fragments (#definition) are for Lexicon references, not collection names 101 + collections.insert(lexicon_file.id.clone()); 102 + record_count += 1; 103 + has_record_def = true; 104 + } 105 + } 106 + } 107 + } 108 + 109 + // Only store lexicon in database if it has record definitions (will be synced) 110 + if has_record_def { 111 + let now = Utc::now(); 112 + let lexicon = Lexicon { 113 + nsid: lexicon_file.id.clone(), 114 + definitions: serde_json::Value::Object(lexicon_file.defs.clone()), 115 + created_at: now, 116 + updated_at: now, 117 + }; 118 + 119 + if let Err(e) = state.database.insert_lexicon(&lexicon).await { 120 + warn!("Failed to store lexicon {}: {}", lexicon_file.id, e); 121 + } 122 + } 123 + } 124 + Err(e) => { 125 + let err = LexiconError::JsonParse(format!("Failed to parse {}: {}", file.name(), e)); 126 + warn!("{}", err); 127 + } 128 + } 129 + } 130 + } 131 + } 132 + } 133 + 134 + let collections_list: Vec<String> = collections.into_iter().collect(); 135 + let collections_str = collections_list.join(", "); 136 + 137 + // Group collections by domain - create a vector of objects for template 138 + let mut domain_groups: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new(); 139 + for collection in &collections_list { 140 + let domain = if let Some(dot_index) = collection.find('.') { 141 + collection[..dot_index].to_string() 142 + } else { 143 + "other".to_string() 144 + }; 145 + domain_groups.entry(domain).or_insert_with(Vec::new).push(collection.clone()); 146 + } 147 + 148 + // Convert to a format that minijinja can handle 149 + let grouped_collections: Vec<serde_json::Value> = domain_groups 150 + .into_iter() 151 + .map(|(domain, collections)| { 152 + serde_json::json!({ 153 + "domain": domain, 154 + "collections": collections 155 + }) 156 + }) 157 + .collect(); 158 + 159 + 160 + let mut env = Environment::new(); 161 + env.add_template("upload_result.html", r#" 162 + <div class="alert alert-success"> 163 + <h4>✅ Lexicon parsing completed!</h4> 164 + <p><strong>Files processed:</strong> {{ file_count }}</p> 165 + <p><strong>Record definitions found:</strong> {{ record_count }}</p> 166 + 167 + <div class="mt-4"> 168 + <h5 class="font-medium text-gray-800 mb-2">Select Collections to Sync:</h5> 169 + <div class="space-y-3"> 170 + {% for group in grouped_collections %} 171 + <div class="border border-gray-200 rounded-lg p-3"> 172 + <div class="flex items-center mb-2"> 173 + <input type="checkbox" 174 + id="domain-{{ group.domain }}" 175 + class="domain-checkbox mr-2" 176 + _="on change toggle .checked on .collection-{{ group.domain }} then call updateCollections()"> 177 + <label for="domain-{{ group.domain }}" class="font-medium text-gray-700">{{ group.domain }}.*</label> 178 + </div> 179 + <div class="ml-6 space-y-1"> 180 + {% for collection in group.collections %} 181 + <div class="flex items-center"> 182 + <input type="checkbox" 183 + id="collection-{{ collection }}" 184 + class="collection-checkbox collection-{{ group.domain }} mr-2" 185 + value="{{ collection }}" 186 + _="on change call updateCollections()"> 187 + <label for="collection-{{ collection }}" class="text-sm text-gray-600 font-mono">{{ collection }}</label> 188 + </div> 189 + {% endfor %} 190 + </div> 191 + </div> 192 + {% endfor %} 193 + </div> 194 + 195 + <div class="mt-4"> 196 + <button class="bg-blue-500 text-white px-3 py-1 rounded text-sm mr-2" 197 + _="on click add .checked to .collection-checkbox then call updateCollections()"> 198 + Select All 199 + </button> 200 + <button class="bg-gray-500 text-white px-3 py-1 rounded text-sm" 201 + _="on click remove .checked from .collection-checkbox then call updateCollections()"> 202 + Select None 203 + </button> 204 + </div> 205 + </div> 206 + 207 + <script> 208 + function updateCollections() { 209 + const selectedCollections = Array.from(document.querySelectorAll('.collection-checkbox:checked')) 210 + .map(cb => cb.value); 211 + 212 + document.getElementById('collections').value = selectedCollections.join(', '); 213 + 214 + // Update domain checkboxes 215 + document.querySelectorAll('.domain-checkbox').forEach(domainCb => { 216 + const domain = domainCb.id.replace('domain-', ''); 217 + const domainCollections = document.querySelectorAll('.collection-' + domain); 218 + const checkedDomainCollections = document.querySelectorAll('.collection-' + domain + ':checked'); 219 + 220 + if (checkedDomainCollections.length === 0) { 221 + domainCb.checked = false; 222 + domainCb.indeterminate = false; 223 + } else if (checkedDomainCollections.length === domainCollections.length) { 224 + domainCb.checked = true; 225 + domainCb.indeterminate = false; 226 + } else { 227 + domainCb.checked = false; 228 + domainCb.indeterminate = true; 229 + } 230 + }); 231 + } 232 + 233 + // Auto-populate with all collections initially and check all boxes 234 + document.getElementById('collections').value = '{{ collections_str }}'; 235 + document.querySelectorAll('.collection-checkbox').forEach(cb => cb.checked = true); 236 + document.querySelectorAll('.domain-checkbox').forEach(cb => cb.checked = true); 237 + </script> 238 + </div> 239 + "#).unwrap(); 240 + 241 + let tmpl = env.get_template("upload_result.html").unwrap(); 242 + let rendered = tmpl.render(context! { 243 + file_count => file_count, 244 + record_count => record_count, 245 + collections_str => collections_str, 246 + grouped_collections => grouped_collections 247 + }).unwrap(); 248 + 249 + Ok(Html(rendered)) 250 + }
+83
api/src/handler_xrpc_codegen.rs
··· 1 + use axum::{ 2 + extract::{Json, State}, 3 + http::StatusCode, 4 + response::Json as ResponseJson, 5 + }; 6 + use serde::{Deserialize, Serialize}; 7 + use crate::AppState; 8 + use crate::codegen::TypeScriptGenerator; 9 + 10 + #[derive(Deserialize)] 11 + pub struct CodegenXrpcRequest { 12 + target: String, 13 + client_type: String, 14 + lexicons: Vec<String>, 15 + } 16 + 17 + #[derive(Serialize)] 18 + pub struct CodegenXrpcResponse { 19 + success: bool, 20 + generated_code: Option<String>, 21 + error: Option<String>, 22 + } 23 + 24 + pub async fn generate_client_xrpc( 25 + State(state): State<AppState>, 26 + Json(request): Json<CodegenXrpcRequest>, 27 + ) -> Result<ResponseJson<CodegenXrpcResponse>, StatusCode> { 28 + if request.lexicons.is_empty() { 29 + return Ok(ResponseJson(CodegenXrpcResponse { 30 + success: false, 31 + generated_code: None, 32 + error: Some("No lexicons specified".to_string()), 33 + })); 34 + } 35 + 36 + // Fetch the selected lexicons from database 37 + let mut lexicons = Vec::new(); 38 + for nsid in &request.lexicons { 39 + if let Ok(Some(lexicon)) = state.database.get_lexicon(nsid).await { 40 + lexicons.push(lexicon); 41 + } 42 + } 43 + 44 + if lexicons.is_empty() { 45 + return Ok(ResponseJson(CodegenXrpcResponse { 46 + success: false, 47 + generated_code: None, 48 + error: Some("No valid lexicons found".to_string()), 49 + })); 50 + } 51 + 52 + let generated_code = match request.target.as_str() { 53 + "typescript-deno" => match request.client_type.as_str() { 54 + "records" => { 55 + let generator = TypeScriptGenerator::new(); 56 + match generator.generate_client(&lexicons) { 57 + Ok(code) => code, 58 + Err(e) => return Ok(ResponseJson(CodegenXrpcResponse { 59 + success: false, 60 + generated_code: None, 61 + error: Some(format!("TypeScript generation failed: {}", e)), 62 + })), 63 + } 64 + }, 65 + _ => return Ok(ResponseJson(CodegenXrpcResponse { 66 + success: false, 67 + generated_code: None, 68 + error: Some("Unsupported client type".to_string()), 69 + })), 70 + }, 71 + _ => return Ok(ResponseJson(CodegenXrpcResponse { 72 + success: false, 73 + generated_code: None, 74 + error: Some("Unsupported target".to_string()), 75 + })), 76 + }; 77 + 78 + Ok(ResponseJson(CodegenXrpcResponse { 79 + success: true, 80 + generated_code: Some(generated_code), 81 + error: None, 82 + })) 83 + }
+170
api/src/main.rs
··· 1 + mod codegen; 2 + mod database; 3 + mod errors; 4 + mod handler_codegen; 5 + mod handler_dynamic_xrpc; 6 + mod handler_lexicon; 7 + mod handler_oauth; 8 + mod handler_upload_lexicon; 9 + mod handler_xrpc_codegen; 10 + mod models; 11 + mod sync; 12 + mod utils; 13 + mod web; 14 + 15 + use axum::{ 16 + extract::{Query, State}, 17 + http::StatusCode, 18 + response::Json, 19 + routing::{get, post}, 20 + Router, 21 + }; 22 + use sqlx::PgPool; 23 + use std::env; 24 + use tower_http::{cors::CorsLayer, trace::TraceLayer}; 25 + use tracing::{info, Level}; 26 + use tracing_subscriber; 27 + 28 + use crate::database::Database; 29 + use crate::errors::AppError; 30 + use crate::models::{BulkSyncOutput, BulkSyncParams, ListRecordsOutput, ListRecordsParams, SmartSyncParams}; 31 + use crate::sync::SyncService; 32 + use crate::web::WebService; 33 + 34 + #[derive(Clone)] 35 + pub struct AppState { 36 + database: Database, 37 + sync_service: SyncService, 38 + #[allow(dead_code)] 39 + web_service: WebService, 40 + } 41 + 42 + #[tokio::main] 43 + async fn main() -> Result<(), AppError> { 44 + // Load environment variables from .env file 45 + dotenvy::dotenv().ok(); 46 + 47 + // Initialize tracing 48 + tracing_subscriber::fmt() 49 + .with_max_level(Level::INFO) 50 + .init(); 51 + 52 + // Database connection 53 + let database_url = env::var("DATABASE_URL") 54 + .unwrap_or_else(|_| "postgresql://slice:slice@localhost:5432/slice".to_string()); 55 + 56 + let pool = PgPool::connect(&database_url).await?; 57 + 58 + // Run migrations if needed 59 + sqlx::migrate!("./migrations").run(&pool).await?; 60 + 61 + let database = Database::new(pool); 62 + let sync_service = SyncService::new(database.clone()); 63 + let web_service = WebService::new(); 64 + 65 + let state = AppState { 66 + database: database.clone(), 67 + sync_service, 68 + web_service, 69 + }; 70 + 71 + // Build application with routes 72 + let app = Router::new() 73 + // XRPC endpoints 74 + .route("/xrpc/com.indexer.records.list", get(list_records)) 75 + .route("/xrpc/com.indexer.collections.bulkSync", post(bulk_sync)) 76 + .route("/xrpc/com.indexer.repos.smartSync", post(smart_sync)) 77 + .route("/xrpc/com.indexer.codegen.generate", post(handler_xrpc_codegen::generate_client_xrpc)) 78 + // Dynamic collection-specific XRPC endpoints 79 + .route("/xrpc/*method", get(handler_dynamic_xrpc::dynamic_xrpc_handler)) 80 + .route("/xrpc/*method", post(handler_dynamic_xrpc::dynamic_xrpc_post_handler)) 81 + // OAuth endpoints 82 + .route("/oauth/authorize", post(handler_oauth::oauth_authorize)) 83 + // Web interface 84 + .route("/", get(web::index)) 85 + .route("/records", get(web::records_page)) 86 + .route("/sync", get(web::sync_page)) 87 + .route("/sync", post(web::bulk_sync_action)) 88 + .route("/codegen", get(web::codegen_page)) 89 + .route("/codegen/generate", post(handler_codegen::generate_client)) 90 + .route("/lexicon", get(handler_lexicon::lexicon_page)) 91 + .route("/upload-lexicons", post(handler_upload_lexicon::upload_lexicons)) 92 + .layer(TraceLayer::new_for_http()) 93 + .layer(CorsLayer::permissive()) 94 + .with_state(state); 95 + 96 + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; 97 + info!("🚀 Server running on http://127.0.0.1:3000"); 98 + 99 + axum::serve(listener, app).await?; 100 + Ok(()) 101 + } 102 + 103 + async fn list_records( 104 + State(state): State<AppState>, 105 + Query(params): Query<ListRecordsParams>, 106 + ) -> Result<Json<ListRecordsOutput>, StatusCode> { 107 + match state.database.list_records(params).await { 108 + Ok(records) => Ok(Json(ListRecordsOutput { 109 + records, 110 + cursor: None, // TODO: implement cursor pagination 111 + })), 112 + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 113 + } 114 + } 115 + 116 + async fn bulk_sync( 117 + State(state): State<AppState>, 118 + axum::extract::Json(params): axum::extract::Json<BulkSyncParams>, 119 + ) -> Result<Json<BulkSyncOutput>, StatusCode> { 120 + match state.sync_service.backfill_collections(&params.collections, params.repos.as_deref()).await { 121 + Ok(_) => { 122 + let total_records = state.database.get_total_record_count().await.unwrap_or(0); 123 + Ok(Json(BulkSyncOutput { 124 + success: true, 125 + total_records, 126 + collections_synced: params.collections, 127 + repos_processed: params.repos.map(|r| r.len() as i64).unwrap_or(0), 128 + message: "Bulk sync completed successfully".to_string(), 129 + })) 130 + }, 131 + Err(e) => { 132 + Ok(Json(BulkSyncOutput { 133 + success: false, 134 + total_records: 0, 135 + collections_synced: vec![], 136 + repos_processed: 0, 137 + message: format!("Bulk sync failed: {}", e), 138 + })) 139 + } 140 + } 141 + } 142 + 143 + async fn smart_sync( 144 + State(state): State<AppState>, 145 + axum::extract::Json(params): axum::extract::Json<SmartSyncParams>, 146 + ) -> Result<Json<BulkSyncOutput>, StatusCode> { 147 + let collections = params.collections.as_deref(); 148 + 149 + match state.sync_service.sync_repo(&params.did, collections).await { 150 + Ok(records_count) => { 151 + let total_records = state.database.get_total_record_count().await.unwrap_or(0); 152 + Ok(Json(BulkSyncOutput { 153 + success: true, 154 + total_records, 155 + collections_synced: params.collections.unwrap_or_default(), 156 + repos_processed: 1, 157 + message: format!("Smart sync completed for {}: {} records", params.did, records_count), 158 + })) 159 + }, 160 + Err(e) => { 161 + Ok(Json(BulkSyncOutput { 162 + success: false, 163 + total_records: 0, 164 + collections_synced: vec![], 165 + repos_processed: 0, 166 + message: format!("Smart sync failed for {}: {}", params.did, e), 167 + })) 168 + } 169 + } 170 + }
+96
api/src/models.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::{Deserialize, Serialize}; 3 + use serde_json::Value; 4 + 5 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 6 + pub struct Record { 7 + pub uri: String, 8 + pub cid: String, 9 + pub did: String, 10 + pub collection: String, 11 + pub json: Value, 12 + #[serde(rename = "indexedAt")] 13 + #[sqlx(rename = "indexedAt")] 14 + pub indexed_at: DateTime<Utc>, 15 + } 16 + 17 + #[derive(Debug, Serialize, Deserialize)] 18 + pub struct CreateRecordInput { 19 + pub collection: String, 20 + pub repo: String, 21 + pub rkey: Option<String>, 22 + pub record: Value, 23 + } 24 + 25 + #[derive(Debug, Serialize, Deserialize)] 26 + pub struct CreateRecordOutput { 27 + pub uri: String, 28 + pub cid: String, 29 + } 30 + 31 + #[derive(Debug, Serialize, Deserialize)] 32 + pub struct ListRecordsParams { 33 + pub collection: String, 34 + pub author: Option<String>, 35 + pub limit: Option<i32>, 36 + pub cursor: Option<String>, 37 + } 38 + 39 + #[derive(Debug, Serialize, Deserialize)] 40 + pub struct ListRecordsOutput { 41 + pub records: Vec<IndexedRecord>, 42 + pub cursor: Option<String>, 43 + } 44 + 45 + #[derive(Debug, Serialize, Deserialize)] 46 + pub struct IndexedRecord { 47 + pub uri: String, 48 + pub cid: String, 49 + pub did: String, 50 + pub collection: String, 51 + pub value: Value, 52 + #[serde(rename = "indexedAt")] 53 + pub indexed_at: String, 54 + } 55 + 56 + 57 + #[derive(Debug, Serialize, Deserialize)] 58 + pub struct BulkSyncParams { 59 + pub collections: Vec<String>, 60 + pub repos: Option<Vec<String>>, 61 + pub limit_per_repo: Option<i32>, 62 + } 63 + 64 + #[derive(Debug, Serialize, Deserialize)] 65 + pub struct BulkSyncOutput { 66 + pub success: bool, 67 + pub total_records: i64, 68 + pub collections_synced: Vec<String>, 69 + pub repos_processed: i64, 70 + pub message: String, 71 + } 72 + 73 + 74 + #[derive(Debug, Serialize, Deserialize)] 75 + pub struct SmartSyncParams { 76 + pub did: String, 77 + pub collections: Option<Vec<String>>, 78 + pub force_full_sync: Option<bool>, 79 + } 80 + 81 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 82 + pub struct Lexicon { 83 + pub nsid: String, 84 + pub definitions: serde_json::Value, 85 + pub created_at: DateTime<Utc>, 86 + pub updated_at: DateTime<Utc>, 87 + } 88 + 89 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 90 + pub struct Actor { 91 + pub did: String, 92 + pub handle: Option<String>, 93 + #[serde(rename = "indexedAt")] 94 + #[sqlx(rename = "indexedAt")] 95 + pub indexed_at: String, 96 + }
+380
api/src/sync.rs
··· 1 + use chrono::{Utc}; 2 + use reqwest::Client; 3 + use serde::Deserialize; 4 + use serde_json::Value; 5 + use tracing::{error, info}; 6 + 7 + use crate::database::Database; 8 + use crate::errors::SyncError; 9 + use crate::models::{Actor, Record}; 10 + use crate::utils::is_primary_collection; 11 + 12 + 13 + #[derive(Debug, Deserialize)] 14 + struct AtProtoRecord { 15 + uri: String, 16 + cid: String, 17 + value: Value, 18 + } 19 + 20 + #[derive(Debug, Deserialize)] 21 + struct ListRecordsResponse { 22 + records: Vec<AtProtoRecord>, 23 + cursor: Option<String>, 24 + } 25 + 26 + 27 + #[derive(Debug, Deserialize)] 28 + struct ListReposByCollectionResponse { 29 + repos: Vec<RepoRef>, 30 + } 31 + 32 + #[derive(Debug, Deserialize)] 33 + struct RepoRef { 34 + did: String, 35 + } 36 + 37 + #[derive(Debug, Deserialize)] 38 + struct DidDocument { 39 + service: Option<Vec<Service>>, 40 + } 41 + 42 + #[derive(Debug, Deserialize)] 43 + struct Service { 44 + #[serde(rename = "type")] 45 + service_type: String, 46 + #[serde(rename = "serviceEndpoint")] 47 + service_endpoint: String, 48 + } 49 + 50 + #[derive(Debug, Clone)] 51 + struct AtpData { 52 + did: String, 53 + pds: String, 54 + handle: Option<String>, 55 + } 56 + 57 + #[derive(Clone)] 58 + pub struct SyncService { 59 + client: Client, 60 + database: Database, 61 + } 62 + 63 + impl SyncService { 64 + pub fn new(database: Database) -> Self { 65 + Self { 66 + client: Client::new(), 67 + database, 68 + } 69 + } 70 + 71 + // Sync using listRecords 72 + pub async fn sync_repo(&self, did: &str, collections: Option<&[String]>) -> Result<i64, SyncError> { 73 + info!("🔄 Starting sync for DID: {}", did); 74 + 75 + let total_records = self.listrecords_sync(did, collections).await?; 76 + 77 + info!("✅ Sync completed for {}: {} records", did, total_records); 78 + Ok(total_records) 79 + } 80 + 81 + 82 + // Sync using listRecords 83 + async fn listrecords_sync(&self, did: &str, collections: Option<&[String]>) -> Result<i64, SyncError> { 84 + let collections_to_sync = match collections { 85 + Some(cols) => cols, 86 + None => return Ok(0), // No collections specified = no records 87 + }; 88 + 89 + // Get ATP data for this single repo 90 + let atp_map = self.get_atp_map_for_repos(&[did.to_string()]).await?; 91 + 92 + let mut total_records = 0; 93 + for collection in collections_to_sync { 94 + match self.fetch_records_for_repo_collection_with_atp_map(did, collection, &atp_map).await { 95 + Ok(records) => { 96 + if !records.is_empty() { 97 + info!("📋 Fallback sync: {} records for {}/{}", records.len(), did, collection); 98 + self.database.batch_insert_records(&records).await?; 99 + total_records += records.len() as i64; 100 + } 101 + } 102 + Err(e) => { 103 + error!("Failed fallback sync for {}/{}: {}", did, collection, e); 104 + } 105 + } 106 + } 107 + 108 + Ok(total_records) 109 + } 110 + 111 + 112 + pub async fn backfill_collections(&self, collections: &[String], repos: Option<&[String]>) -> Result<(), SyncError> { 113 + info!("🔄 Starting backfill operation"); 114 + info!("📚 Processing {} collections: {}", collections.len(), collections.join(", ")); 115 + 116 + let all_repos = if let Some(provided_repos) = repos { 117 + info!("📋 Using {} provided repositories", provided_repos.len()); 118 + provided_repos.to_vec() 119 + } else { 120 + info!("📊 Fetching repositories for collections..."); 121 + let mut unique_repos = std::collections::HashSet::new(); 122 + 123 + // Separate primary and external collections 124 + let (primary_collections, _external_collections): (Vec<_>, Vec<_>) = collections 125 + .iter() 126 + .partition(|collection| is_primary_collection(collection)); 127 + 128 + // First, get all repos from primary collections 129 + let mut primary_repos = std::collections::HashSet::new(); 130 + for collection in &primary_collections { 131 + match self.get_repos_for_collection(collection).await { 132 + Ok(repos) => { 133 + info!("✓ Found {} repositories for primary collection \"{}\"", repos.len(), collection); 134 + primary_repos.extend(repos); 135 + }, 136 + Err(e) => { 137 + error!("Failed to get repos for primary collection {}: {}", collection, e); 138 + } 139 + } 140 + } 141 + 142 + info!("📋 Found {} unique repositories from primary collections", primary_repos.len()); 143 + 144 + // Use primary repos for syncing (both primary and external collections) 145 + unique_repos.extend(primary_repos); 146 + 147 + let repos: Vec<String> = unique_repos.into_iter().collect(); 148 + info!("📋 Processing {} unique repositories", repos.len()); 149 + repos 150 + }; 151 + 152 + // Get ATP data for all repos at once 153 + info!("🔍 Resolving ATP data for repositories..."); 154 + let atp_map = self.get_atp_map_for_repos(&all_repos).await?; 155 + info!("✓ Resolved ATP data for {}/{} repositories", atp_map.len(), all_repos.len()); 156 + 157 + // Only sync repos that have valid ATP data 158 + let valid_repos: Vec<String> = atp_map.keys().cloned().collect(); 159 + let failed_resolutions = all_repos.len() - valid_repos.len(); 160 + 161 + if failed_resolutions > 0 { 162 + info!("⚠️ {} repositories failed DID resolution and will be skipped", failed_resolutions); 163 + } 164 + 165 + info!("🧠 Starting sync for {} repositories...", valid_repos.len()); 166 + 167 + // Create parallel fetch tasks for repos with valid ATP data only 168 + let mut fetch_tasks = Vec::new(); 169 + for repo in &valid_repos { 170 + for collection in collections { 171 + let repo_clone = repo.clone(); 172 + let collection_clone = collection.clone(); 173 + let sync_service = self.clone(); 174 + let atp_map_clone = atp_map.clone(); 175 + 176 + let task = tokio::spawn(async move { 177 + match sync_service.fetch_records_for_repo_collection_with_atp_map(&repo_clone, &collection_clone, &atp_map_clone).await { 178 + Ok(records) => { 179 + Ok((repo_clone, collection_clone, records)) 180 + } 181 + Err(e) => { 182 + // Handle common "not error" scenarios as empty results 183 + match &e { 184 + SyncError::ListRecords { status } => { 185 + if *status == 404 || *status == 400 { 186 + // Collection doesn't exist for this repo - return empty 187 + Ok((repo_clone, collection_clone, vec![])) 188 + } else { 189 + Err(e) 190 + } 191 + } 192 + SyncError::HttpRequest(_) => { 193 + // Network errors - treat as empty (like TypeScript version) 194 + Ok((repo_clone, collection_clone, vec![])) 195 + } 196 + _ => Err(e) 197 + } 198 + } 199 + } 200 + }); 201 + fetch_tasks.push(task); 202 + } 203 + } 204 + 205 + info!("📥 Fetching records for repositories and collections..."); 206 + info!("🔧 Debug: Created {} fetch tasks for {} repos × {} collections", fetch_tasks.len(), valid_repos.len(), collections.len()); 207 + 208 + // Collect all results 209 + let mut all_records = Vec::new(); 210 + let mut successful_tasks = 0; 211 + let mut failed_tasks = 0; 212 + for task in fetch_tasks { 213 + match task.await { 214 + Ok(Ok((_repo, _collection, records))) => { 215 + all_records.extend(records); 216 + successful_tasks += 1; 217 + } 218 + Ok(Err(_)) => { 219 + failed_tasks += 1; 220 + } 221 + Err(_e) => { 222 + failed_tasks += 1; 223 + } 224 + } 225 + } 226 + 227 + info!("🔧 Debug: {} successful tasks, {} failed tasks", successful_tasks, failed_tasks); 228 + 229 + let total_records = all_records.len() as i64; 230 + info!("✓ Fetched {} total records", total_records); 231 + 232 + // Index actors first (like the TypeScript version) 233 + info!("📝 Indexing actors..."); 234 + self.index_actors(&valid_repos, &atp_map).await?; 235 + info!("✓ Indexed {} actors", valid_repos.len()); 236 + 237 + // Single batch insert for all records 238 + if !all_records.is_empty() { 239 + info!("📝 Indexing {} records...", total_records); 240 + self.database.batch_insert_records(&all_records).await?; 241 + } 242 + 243 + info!("✅ Backfill complete!"); 244 + 245 + Ok(()) 246 + } 247 + 248 + async fn get_repos_for_collection(&self, collection: &str) -> Result<Vec<String>, SyncError> { 249 + let response = self.client 250 + .get("https://relay1.us-west.bsky.network/xrpc/com.atproto.sync.listReposByCollection") 251 + .query(&[("collection", collection)]) 252 + .send() 253 + .await?; 254 + 255 + if !response.status().is_success() { 256 + return Err(SyncError::ListRepos { status: response.status().as_u16() }); 257 + } 258 + 259 + let repos_response: ListReposByCollectionResponse = response.json().await?; 260 + Ok(repos_response.repos.into_iter().map(|r| r.did).collect()) 261 + } 262 + 263 + async fn fetch_records_for_repo_collection_with_atp_map(&self, repo: &str, collection: &str, atp_map: &std::collections::HashMap<String, AtpData>) -> Result<Vec<Record>, SyncError> { 264 + let atp_data = atp_map.get(repo).ok_or_else(|| SyncError::Generic(format!("No ATP data found for repo: {}", repo)))?; 265 + self.fetch_records_for_repo_collection(repo, collection, &atp_data.pds).await 266 + } 267 + 268 + async fn fetch_records_for_repo_collection(&self, repo: &str, collection: &str, pds_url: &str) -> Result<Vec<Record>, SyncError> { 269 + let mut records = Vec::new(); 270 + let mut cursor: Option<String> = None; 271 + 272 + loop { 273 + let mut params = vec![("repo", repo), ("collection", collection), ("limit", "100")]; 274 + if let Some(ref c) = cursor { 275 + params.push(("cursor", c)); 276 + } 277 + 278 + let response = self.client 279 + .get(&format!("{}/xrpc/com.atproto.repo.listRecords", pds_url)) 280 + .query(&params) 281 + .send() 282 + .await?; 283 + 284 + if !response.status().is_success() { 285 + return Err(SyncError::ListRecords { status: response.status().as_u16() }); 286 + } 287 + 288 + let list_response: ListRecordsResponse = response.json().await?; 289 + 290 + for atproto_record in list_response.records { 291 + let record = Record { 292 + uri: atproto_record.uri, 293 + cid: atproto_record.cid, 294 + did: repo.to_string(), 295 + collection: collection.to_string(), 296 + json: atproto_record.value, 297 + indexed_at: Utc::now(), 298 + }; 299 + records.push(record); 300 + } 301 + 302 + cursor = list_response.cursor; 303 + if cursor.is_none() { 304 + break; 305 + } 306 + } 307 + 308 + Ok(records) 309 + } 310 + 311 + async fn get_atp_map_for_repos(&self, repos: &[String]) -> Result<std::collections::HashMap<String, AtpData>, SyncError> { 312 + let mut atp_map = std::collections::HashMap::new(); 313 + 314 + for repo in repos { 315 + if let Ok(atp_data) = self.resolve_atp_data(repo).await { 316 + atp_map.insert(atp_data.did.clone(), atp_data); 317 + } 318 + } 319 + 320 + Ok(atp_map) 321 + } 322 + 323 + async fn resolve_atp_data(&self, did: &str) -> Result<AtpData, SyncError> { 324 + let pds = if did.starts_with("did:plc:") { 325 + // Resolve PLC DID 326 + let response = self.client 327 + .get(&format!("https://plc.directory/{}", did)) 328 + .send() 329 + .await?; 330 + 331 + if response.status().is_success() { 332 + let did_doc: DidDocument = response.json().await?; 333 + if let Some(services) = did_doc.service { 334 + for service in services { 335 + if service.service_type == "AtprotoPersonalDataServer" { 336 + return Ok(AtpData { 337 + did: did.to_string(), 338 + pds: service.service_endpoint, 339 + handle: None, 340 + }); 341 + } 342 + } 343 + } 344 + } 345 + 346 + // Fallback to bsky.social 347 + "https://bsky.social".to_string() 348 + } else { 349 + // Fallback to bsky.social for non-PLC DIDs 350 + "https://bsky.social".to_string() 351 + }; 352 + 353 + Ok(AtpData { 354 + did: did.to_string(), 355 + pds, 356 + handle: None, 357 + }) 358 + } 359 + 360 + async fn index_actors(&self, repos: &[String], atp_map: &std::collections::HashMap<String, AtpData>) -> Result<(), SyncError> { 361 + let mut actors = Vec::new(); 362 + let now = chrono::Utc::now().to_rfc3339(); 363 + 364 + for repo in repos { 365 + if let Some(atp_data) = atp_map.get(repo) { 366 + actors.push(Actor { 367 + did: atp_data.did.clone(), 368 + handle: atp_data.handle.clone(), 369 + indexed_at: now.clone(), 370 + }); 371 + } 372 + } 373 + 374 + if !actors.is_empty() { 375 + self.database.batch_insert_actors(&actors).await?; 376 + } 377 + 378 + Ok(()) 379 + } 380 + }
+6
api/src/utils.rs
··· 1 + /// Determines if a collection NSID is considered "primary" vs "external" 2 + /// Primary collections are social.grain.* domain 3 + /// Everything else is considered external 4 + pub fn is_primary_collection(nsid: &str) -> bool { 5 + nsid.starts_with("social.grain.") 6 + }
+204
api/src/web.rs
··· 1 + use axum::{ 2 + extract::{Query, State}, 3 + http::StatusCode, 4 + response::{Html, IntoResponse}, 5 + Form, 6 + }; 7 + use minijinja::{context, Environment}; 8 + use serde::Deserialize; 9 + use std::collections::HashMap; 10 + 11 + use crate::models::ListRecordsParams; 12 + use crate::AppState; 13 + 14 + #[derive(Clone)] 15 + pub struct WebService { 16 + #[allow(dead_code)] 17 + env: Environment<'static>, 18 + } 19 + 20 + impl WebService { 21 + pub fn new() -> Self { 22 + let mut env = Environment::new(); 23 + 24 + // Add base template 25 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 26 + env.add_template("index.html", include_str!("../templates/index.html")).unwrap(); 27 + env.add_template("records.html", include_str!("../templates/records.html")).unwrap(); 28 + env.add_template("sync.html", include_str!("../templates/sync.html")).unwrap(); 29 + 30 + Self { env } 31 + } 32 + } 33 + 34 + #[derive(Deserialize)] 35 + pub struct BulkSyncForm { 36 + collections: String, 37 + repos: Option<String>, 38 + } 39 + 40 + 41 + pub async fn index(State(state): State<AppState>) -> Result<impl IntoResponse, StatusCode> { 42 + let collections = state.database.get_available_collections().await.unwrap_or_default(); 43 + let total_records = state.database.get_total_record_count().await.unwrap_or(0); 44 + 45 + let mut env = Environment::new(); 46 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 47 + env.add_template("index.html", include_str!("../templates/index.html")).unwrap(); 48 + 49 + let tmpl = env.get_template("index.html").unwrap(); 50 + let rendered = tmpl.render(context! { 51 + title => "AT Protocol Indexer", 52 + collections => collections, 53 + total_records => total_records 54 + }).unwrap(); 55 + 56 + Ok(Html(rendered)) 57 + } 58 + 59 + pub async fn records_page( 60 + State(state): State<AppState>, 61 + Query(params): Query<HashMap<String, String>>, 62 + ) -> Result<impl IntoResponse, StatusCode> { 63 + let collection = params.get("collection").cloned().unwrap_or_default(); 64 + let author = params.get("author").cloned(); 65 + 66 + // Get available collections for the dropdown 67 + let available_collections = state.database.get_available_collections().await.unwrap_or_default(); 68 + 69 + let records = if !collection.is_empty() { 70 + let list_params = ListRecordsParams { 71 + collection: collection.clone(), 72 + author, 73 + limit: Some(50), 74 + cursor: None, 75 + }; 76 + let raw_records = state.database.list_records(list_params).await.unwrap_or_default(); 77 + 78 + // Transform records to include pretty-printed JSON 79 + raw_records.into_iter().map(|record| { 80 + let pretty_json = serde_json::to_string_pretty(&record.value).unwrap_or_else(|_| record.value.to_string()); 81 + serde_json::json!({ 82 + "uri": record.uri, 83 + "cid": record.cid, 84 + "did": record.did, 85 + "collection": record.collection, 86 + "value": record.value, 87 + "pretty_value": pretty_json, 88 + "indexed_at": record.indexed_at 89 + }) 90 + }).collect() 91 + } else { 92 + Vec::new() 93 + }; 94 + 95 + let mut env = Environment::new(); 96 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 97 + env.add_template("records.html", include_str!("../templates/records.html")).unwrap(); 98 + 99 + let tmpl = env.get_template("records.html").unwrap(); 100 + let rendered = tmpl.render(context! { 101 + title => "Records", 102 + records => records, 103 + collection => collection, 104 + available_collections => available_collections 105 + }).unwrap(); 106 + 107 + Ok(Html(rendered)) 108 + } 109 + 110 + pub async fn codegen_page(State(state): State<AppState>) -> Result<impl IntoResponse, StatusCode> { 111 + // Get stored lexicons for the UI 112 + let lexicons = match state.database.get_all_lexicons().await { 113 + Ok(lexicons) => lexicons, 114 + Err(_) => Vec::new(), 115 + }; 116 + 117 + let mut env = Environment::new(); 118 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 119 + env.add_template("codegen.html", include_str!("../templates/codegen.html")).unwrap(); 120 + 121 + let tmpl = env.get_template("codegen.html").unwrap(); 122 + let rendered = tmpl.render(context! { 123 + title => "Client Code Generation", 124 + lexicons => lexicons 125 + }).unwrap(); 126 + 127 + Ok(Html(rendered)) 128 + } 129 + 130 + pub async fn sync_page() -> impl IntoResponse { 131 + let mut env = Environment::new(); 132 + env.add_template("base.html", include_str!("../templates/base.html")).unwrap(); 133 + env.add_template("sync.html", include_str!("../templates/sync.html")).unwrap(); 134 + 135 + let tmpl = env.get_template("sync.html").unwrap(); 136 + let rendered = tmpl.render(context! { 137 + title => "Sync Records" 138 + }).unwrap(); 139 + 140 + Html(rendered) 141 + } 142 + 143 + pub async fn bulk_sync_action( 144 + State(state): State<AppState>, 145 + Form(form): Form<BulkSyncForm>, 146 + ) -> Result<impl IntoResponse, StatusCode> { 147 + // Parse collections from comma-separated string 148 + let collections: Vec<String> = form.collections 149 + .split(',') 150 + .map(|s| s.trim().to_string()) 151 + .filter(|s| !s.is_empty()) 152 + .collect(); 153 + 154 + // Parse repos from newline-separated string if provided 155 + let repos = form.repos 156 + .filter(|s| !s.trim().is_empty()) 157 + .map(|s| s.lines() 158 + .map(|line| line.trim().to_string()) 159 + .filter(|line| !line.is_empty()) 160 + .collect::<Vec<String>>()); 161 + 162 + if collections.is_empty() { 163 + return Ok(Html(r#" 164 + <div class="alert alert-error"> 165 + <h4>❌ No collections specified</h4> 166 + <p>Please specify at least one collection to sync.</p> 167 + </div> 168 + "#.to_string())); 169 + } 170 + 171 + match state.sync_service.backfill_collections(&collections, repos.as_deref()).await { 172 + Ok(_) => { 173 + let total_records = state.database.get_total_record_count().await.unwrap_or(0); 174 + 175 + let mut env = Environment::new(); 176 + env.add_template("sync_result.html", r#" 177 + <div class="alert alert-success"> 178 + <h4>✅ Bulk sync completed successfully!</h4> 179 + <p><strong>Collections synced:</strong> {{ collections|join(", ") }}</p> 180 + <p><strong>Total records in database:</strong> {{ total_records }}</p> 181 + <p><strong>Operation:</strong> {{ message }}</p> 182 + </div> 183 + "#).unwrap(); 184 + 185 + let tmpl = env.get_template("sync_result.html").unwrap(); 186 + let rendered = tmpl.render(context! { 187 + collections => collections, 188 + total_records => total_records, 189 + message => "Bulk sync operation completed" 190 + }).unwrap(); 191 + 192 + Ok(Html(rendered)) 193 + }, 194 + Err(e) => { 195 + Ok(Html(format!(r#" 196 + <div class="alert alert-error"> 197 + <h4>❌ Bulk sync failed</h4> 198 + <p>Error: {}</p> 199 + </div> 200 + "#, e))) 201 + } 202 + } 203 + } 204 +
+70
api/templates/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{ title }} - AT Protocol Indexer</title> 8 + <script src="https://unpkg.com/htmx.org@1.9.10"></script> 9 + <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> 10 + <script src="https://cdn.tailwindcss.com/3.4.1"></script> 11 + <style> 12 + .alert { 13 + padding: 1rem; 14 + margin-bottom: 1rem; 15 + border-radius: 0.375rem; 16 + border-width: 1px; 17 + } 18 + 19 + .alert-success { 20 + background-color: #dcfce7; 21 + border-color: #22c55e; 22 + color: #15803d; 23 + } 24 + 25 + .alert-warning { 26 + background-color: #fef3c7; 27 + border-color: #eab308; 28 + color: #a16207; 29 + } 30 + 31 + .alert-error { 32 + background-color: #fecaca; 33 + border-color: #ef4444; 34 + color: #dc2626; 35 + } 36 + 37 + .htmx-indicator { 38 + display: none; 39 + } 40 + 41 + .htmx-request .htmx-indicator { 42 + display: inline; 43 + } 44 + 45 + .htmx-request .default-text { 46 + display: none; 47 + } 48 + </style> 49 + </head> 50 + 51 + <body class="bg-gray-100 min-h-screen"> 52 + <nav class="bg-blue-600 text-white p-4"> 53 + <div class="container mx-auto flex justify-between items-center"> 54 + <h1 class="text-xl font-bold">AT Protocol Indexer</h1> 55 + <div class="space-x-4"> 56 + <a href="/" class="hover:underline">Home</a> 57 + <a href="/records" class="hover:underline">Records</a> 58 + <a href="/lexicon" class="hover:underline">Lexicon</a> 59 + <a href="/sync" class="hover:underline">Sync</a> 60 + <a href="/codegen" class="hover:underline">Codegen</a> 61 + </div> 62 + </div> 63 + </nav> 64 + 65 + <main class="container mx-auto mt-8 px-4"> 66 + {% block content %}{% endblock %} 67 + </main> 68 + </body> 69 + 70 + </html>
+82
api/templates/codegen.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div class="max-w-6xl mx-auto"> 5 + <h1 class="text-3xl font-bold text-gray-800 mb-8">Client Code Generation</h1> 6 + 7 + <div class="bg-white rounded-lg shadow-md p-6 mb-6"> 8 + <h2 class="text-xl font-semibold mb-4">Generate Typed Clients</h2> 9 + <p class="text-gray-600 mb-6">Generate TypeScript clients for interacting with XRPC APIs based on stored lexicon definitions.</p> 10 + 11 + <form hx-post="/codegen/generate" 12 + hx-target="#codegen-result" 13 + hx-indicator="#generate-button" 14 + class="space-y-6"> 15 + <div> 16 + <label for="target" class="block text-sm font-medium text-gray-700 mb-2">Target Language</label> 17 + <select name="target" id="target" class="w-full border border-gray-300 rounded-md px-3 py-2"> 18 + <option value="typescript-deno">TypeScript (Deno)</option> 19 + </select> 20 + </div> 21 + 22 + <div> 23 + <label for="client_type" class="block text-sm font-medium text-gray-700 mb-2">Client Type</label> 24 + <select name="client_type" id="client_type" class="w-full border border-gray-300 rounded-md px-3 py-2"> 25 + <option value="records">Records Client</option> 26 + </select> 27 + </div> 28 + 29 + <div> 30 + <label class="block text-sm font-medium text-gray-700 mb-2">Include Collections</label> 31 + <div class="space-y-2 max-h-64 overflow-y-auto border border-gray-300 rounded-md p-3"> 32 + {% if lexicons %} 33 + {% for lexicon in lexicons %} 34 + <div class="flex items-center"> 35 + <input type="checkbox" 36 + id="lexicon-{{ lexicon.nsid }}" 37 + name="lexicons" 38 + value="{{ lexicon.nsid }}" 39 + class="mr-2"> 40 + <label for="lexicon-{{ lexicon.nsid }}" class="text-sm text-gray-600 font-mono">{{ lexicon.nsid }}</label> 41 + </div> 42 + {% endfor %} 43 + {% else %} 44 + <p class="text-gray-500 text-sm">No lexicons available. Upload some lexicon files first.</p> 45 + {% endif %} 46 + </div> 47 + </div> 48 + 49 + <button type="submit" 50 + id="generate-button" 51 + class="w-full bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-md font-medium"> 52 + <span class="default-text">Generate Client</span> 53 + <span class="htmx-indicator">🔄 Generating...</span> 54 + </button> 55 + </form> 56 + 57 + <div id="codegen-result" class="mt-6"></div> 58 + </div> 59 + 60 + <div class="bg-blue-50 border border-blue-200 rounded-lg p-6"> 61 + <h3 class="text-lg font-semibold text-blue-800 mb-2">💡 About Client Generation</h3> 62 + <ul class="space-y-2 text-blue-700"> 63 + <li class="flex items-start"> 64 + <span class="font-bold mr-2">•</span> 65 + <span>Generates typed TypeScript clients for interacting with XRPC APIs</span> 66 + </li> 67 + <li class="flex items-start"> 68 + <span class="font-bold mr-2">•</span> 69 + <span>Based on stored lexicon definitions with full type safety</span> 70 + </li> 71 + <li class="flex items-start"> 72 + <span class="font-bold mr-2">•</span> 73 + <span>Includes interfaces for records, queries, and procedures</span> 74 + </li> 75 + <li class="flex items-start"> 76 + <span class="font-bold mr-2">•</span> 77 + <span>Compatible with Deno and modern TypeScript environments</span> 78 + </li> 79 + </ul> 80 + </div> 81 + </div> 82 + {% endblock %}
+94
api/templates/index.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div class="max-w-4xl mx-auto"> 5 + <h1 class="text-3xl font-bold text-gray-800 mb-8">AT Protocol Indexer</h1> 6 + 7 + {% if total_records > 0 %} 8 + <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> 9 + <h2 class="text-xl font-semibold text-blue-800 mb-2">📊 Database Status</h2> 10 + <p class="text-blue-700">Currently indexing <strong>{{ total_records }}</strong> records across <strong>{{ 11 + collections|length }}</strong> collections.</p> 12 + </div> 13 + {% endif %} 14 + 15 + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 16 + <div class="bg-white rounded-lg shadow-md p-6"> 17 + <h2 class="text-xl font-semibold text-gray-800 mb-4">📝 View Records</h2> 18 + <p class="text-gray-600 mb-4">Browse indexed AT Protocol records by collection.</p> 19 + {% if collections %} 20 + <a href="/records" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> 21 + Browse Records 22 + </a> 23 + {% else %} 24 + <p class="text-gray-500 text-sm">No records synced yet. Start by syncing some records!</p> 25 + {% endif %} 26 + </div> 27 + 28 + <div class="bg-white rounded-lg shadow-md p-6"> 29 + <h2 class="text-xl font-semibold text-gray-800 mb-4">📚 Lexicon Definitions</h2> 30 + <p class="text-gray-600 mb-4">View lexicon definitions and schemas.</p> 31 + <a href="/lexicon" class="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded"> 32 + View Lexicons 33 + </a> 34 + </div> 35 + 36 + <div class="bg-white rounded-lg shadow-md p-6"> 37 + <h2 class="text-xl font-semibold text-gray-800 mb-4">⚡ Code Generation</h2> 38 + <p class="text-gray-600 mb-4">Generate TypeScript client from your lexicon definitions.</p> 39 + <a href="/codegen" class="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded"> 40 + Generate Client 41 + </a> 42 + </div> 43 + 44 + <div class="bg-white rounded-lg shadow-md p-6"> 45 + <h2 class="text-xl font-semibold text-gray-800 mb-4">🔄 Bulk Sync</h2> 46 + <p class="text-gray-600 mb-4">Sync entire collections from AT Protocol networks.</p> 47 + <a href="/sync" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"> 48 + Start Bulk Sync 49 + </a> 50 + </div> 51 + 52 + {% if collections %} 53 + <div class="bg-white rounded-lg shadow-md p-6"> 54 + <h2 class="text-xl font-semibold text-gray-800 mb-4">📊 Synced Collections</h2> 55 + <p class="text-gray-600 mb-4">Collections currently indexed in the database.</p> 56 + <div class="space-y-2 max-h-40 overflow-y-auto"> 57 + {% for collection in collections %} 58 + <a href="/records?collection={{ collection[0] }}" 59 + class="flex justify-between items-center text-blue-600 hover:underline text-sm"> 60 + <span>{{ collection[0] }}</span> 61 + <span class="text-gray-500">{{ collection[1] }}</span> 62 + </a> 63 + {% endfor %} 64 + </div> 65 + </div> 66 + {% else %} 67 + <div class="bg-white rounded-lg shadow-md p-6"> 68 + <h2 class="text-xl font-semibold text-gray-800 mb-4">🌟 Get Started</h2> 69 + <p class="text-gray-600 mb-4">No records indexed yet. Start by syncing some AT Protocol collections!</p> 70 + <div class="space-y-2 text-sm"> 71 + <p class="text-gray-500">Try syncing collections like:</p> 72 + <code class="block bg-gray-100 p-2 rounded text-xs">app.bsky.feed.post</code> 73 + <code class="block bg-gray-100 p-2 rounded text-xs">app.bsky.actor.profile</code> 74 + </div> 75 + </div> 76 + {% endif %} 77 + </div> 78 + 79 + <div class="mt-12 bg-white rounded-lg shadow-md p-6"> 80 + <h2 class="text-2xl font-semibold text-gray-800 mb-4">API Endpoints</h2> 81 + <div class="space-y-4"> 82 + <div> 83 + <code 84 + class="bg-gray-100 px-2 py-1 rounded">GET /xrpc/com.indexer.records.list?collection=app.bsky.feed.post</code> 85 + <p class="text-gray-600 mt-1">List records for a collection</p> 86 + </div> 87 + <div> 88 + <code class="bg-gray-100 px-2 py-1 rounded">POST /xrpc/com.indexer.collections.bulkSync</code> 89 + <p class="text-gray-600 mt-1">Bulk sync collections (JSON: {"collections": ["app.bsky.feed.post"]})</p> 90 + </div> 91 + </div> 92 + </div> 93 + </div> 94 + {% endblock %}
+49
api/templates/lexicon.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div class="max-w-6xl mx-auto"> 5 + <h1 class="text-3xl font-bold text-gray-800 mb-8">Lexicon Definitions</h1> 6 + 7 + {% if lexicons %} 8 + <div class="bg-white rounded-lg shadow-md overflow-hidden"> 9 + <div class="px-6 py-4 bg-gray-50 border-b"> 10 + <h3 class="text-lg font-semibold">Found {{ lexicons|length }} lexicon definitions</h3> 11 + </div> 12 + <div class="divide-y divide-gray-200"> 13 + {% for lexicon in lexicons %} 14 + <div class="p-6"> 15 + <div class="flex justify-between items-start mb-4"> 16 + <h4 class="text-lg font-medium text-blue-600">{{ lexicon.nsid }}</h4> 17 + <span class="text-xs text-gray-500">Updated: {{ lexicon.updated_at }}</span> 18 + </div> 19 + 20 + <div class="mt-4"> 21 + <details class="cursor-pointer"> 22 + <summary class="font-medium text-gray-700 hover:text-gray-900">View Definitions</summary> 23 + <div class="mt-4"> 24 + {% if lexicon.pretty_definitions %} 25 + <pre class="bg-gray-100 p-3 rounded text-xs overflow-x-auto">{{ lexicon.pretty_definitions }}</pre> 26 + {% else %} 27 + <p class="text-gray-500 italic">No definitions found</p> 28 + {% endif %} 29 + </div> 30 + </details> 31 + </div> 32 + </div> 33 + {% endfor %} 34 + </div> 35 + </div> 36 + {% else %} 37 + <div class="bg-white rounded-lg shadow-md p-8 text-center"> 38 + <div class="text-gray-400 text-6xl mb-4">📚</div> 39 + <h3 class="text-xl font-semibold text-gray-800 mb-2">No lexicon definitions found</h3> 40 + <p class="text-gray-600 mb-4"> 41 + Upload lexicon files to see their definitions here. 42 + </p> 43 + <a href="/sync" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> 44 + Upload Lexicons 45 + </a> 46 + </div> 47 + {% endif %} 48 + </div> 49 + {% endblock %}
+98
api/templates/records.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div class="max-w-6xl mx-auto"> 5 + <h1 class="text-3xl font-bold text-gray-800 mb-8">Records</h1> 6 + 7 + <div class="bg-white rounded-lg shadow-md p-6 mb-6"> 8 + <div class="flex justify-between items-center mb-4"> 9 + <h2 class="text-xl font-semibold">Filter Records</h2> 10 + </div> 11 + <form class="grid grid-cols-1 md:grid-cols-3 gap-4" method="get" _="on submit 12 + if #author.value is empty 13 + remove @name from #author 14 + end"> 15 + <div> 16 + <label class="block text-sm font-medium text-gray-700 mb-2">Collection</label> 17 + <select name="collection" class="w-full border border-gray-300 rounded-md px-3 py-2"> 18 + <option value="">Select collection...</option> 19 + {% for available_collection in available_collections %} 20 + <option value="{{ available_collection[0] }}" {% if collection==available_collection[0] %}selected{% 21 + endif %}> 22 + {{ available_collection[0] }} ({{ available_collection[1] }} records) 23 + </option> 24 + {% endfor %} 25 + </select> 26 + </div> 27 + <div> 28 + <label class="block text-sm font-medium text-gray-700 mb-2">Author DID</label> 29 + <input type="text" id="author" name="author" placeholder="did:plc:..." 30 + class="w-full border border-gray-300 rounded-md px-3 py-2"> 31 + </div> 32 + <div class="flex items-end"> 33 + <button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md"> 34 + Filter 35 + </button> 36 + </div> 37 + </form> 38 + </div> 39 + 40 + {% if records %} 41 + <div class="bg-white rounded-lg shadow-md overflow-hidden"> 42 + <div class="px-6 py-4 bg-gray-50 border-b"> 43 + <h3 class="text-lg font-semibold">Found {{ records|length }} records</h3> 44 + </div> 45 + <div class="divide-y divide-gray-200"> 46 + {% for record in records %} 47 + <div class="p-6"> 48 + <div class="flex justify-between items-start mb-2"> 49 + <h4 class="text-sm font-medium text-blue-600">{{ record.uri }}</h4> 50 + <span class="text-xs text-gray-500">{{ record.indexed_at }}</span> 51 + </div> 52 + <div class="space-y-2 text-sm"> 53 + <div> 54 + <span class="font-medium">Collection:</span> 55 + <span class="text-gray-600">{{ record.collection }}</span> 56 + </div> 57 + <div> 58 + <span class="font-medium">Author:</span> 59 + <span class="text-gray-600">{{ record.did }}</span> 60 + </div> 61 + <div> 62 + <span class="font-medium">CID:</span> 63 + <span class="text-gray-600 font-mono text-xs">{{ record.cid }}</span> 64 + </div> 65 + </div> 66 + {% if record.value %} 67 + <div class="mt-4"> 68 + <details class="cursor-pointer"> 69 + <summary class="font-medium text-gray-700 hover:text-gray-900">View Record Data</summary> 70 + <pre 71 + class="mt-2 bg-gray-100 p-3 rounded text-xs overflow-x-auto">{{ record.pretty_value }}</pre> 72 + </details> 73 + </div> 74 + {% endif %} 75 + </div> 76 + {% endfor %} 77 + </div> 78 + </div> 79 + {% else %} 80 + <div class="bg-white rounded-lg shadow-md p-8 text-center"> 81 + <div class="text-gray-400 text-6xl mb-4">📝</div> 82 + <h3 class="text-xl font-semibold text-gray-800 mb-2">No records found</h3> 83 + <p class="text-gray-600 mb-4"> 84 + {% if collection %} 85 + No records found for collection "{{ collection }}". 86 + {% elif available_collections %} 87 + Select a collection from the dropdown above to view records. 88 + {% else %} 89 + No records have been synced yet. Start by syncing some AT Protocol records! 90 + {% endif %} 91 + </p> 92 + <a href="/sync" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> 93 + Sync Some Records 94 + </a> 95 + </div> 96 + {% endif %} 97 + </div> 98 + {% endblock %}
+122
api/templates/sync.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div class="max-w-4xl mx-auto"> 5 + <h1 class="text-3xl font-bold text-gray-800 mb-8">Sync Records</h1> 6 + 7 + <div class="max-w-2xl mx-auto"> 8 + <!-- Lexicon Upload Section --> 9 + <div class="bg-white rounded-lg shadow-md p-6 mb-6"> 10 + <h2 class="text-xl font-semibold text-gray-800 mb-4">📦 Upload Lexicon Files</h2> 11 + <p class="text-gray-600 mb-6">Upload a zip file containing lexicon JSON files to automatically populate collections for syncing.</p> 12 + 13 + <form hx-post="/upload-lexicons" 14 + hx-target="#lexicon-result" 15 + hx-indicator="#upload-button" 16 + enctype="multipart/form-data" 17 + class="space-y-4"> 18 + <div> 19 + <label for="lexicon-file" class="block text-sm font-medium text-gray-700 mb-2">Lexicon Zip File</label> 20 + <input type="file" 21 + id="lexicon-file" 22 + name="lexicon_file" 23 + accept=".zip" 24 + class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-green-500" 25 + required> 26 + <p class="text-xs text-gray-500 mt-1"> 27 + Upload a zip file containing lexicon JSON files. Record definitions will be extracted automatically. 28 + </p> 29 + </div> 30 + 31 + <button type="submit" 32 + id="upload-button" 33 + class="w-full bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-md font-medium"> 34 + <span class="default-text">Parse Lexicons</span> 35 + <span class="htmx-indicator">🔄 Processing...</span> 36 + </button> 37 + </form> 38 + 39 + <div id="lexicon-result" class="mt-4"></div> 40 + </div> 41 + 42 + <!-- Bulk Sync Section --> 43 + <div class="bg-white rounded-lg shadow-md p-6"> 44 + <h2 class="text-xl font-semibold text-gray-800 mb-4">📚 Bulk Sync Collections</h2> 45 + <p class="text-gray-600 mb-6">Sync multiple records from AT Protocol collections. This will fetch records from across the network.</p> 46 + 47 + <form hx-post="/sync" 48 + hx-target="#sync-result" 49 + hx-indicator="#sync-button" 50 + class="space-y-6"> 51 + <div> 52 + <label for="collections" class="block text-sm font-medium text-gray-700 mb-2">Collections to Sync</label> 53 + <input type="text" 54 + id="collections" 55 + name="collections" 56 + placeholder="app.bsky.feed.post, app.bsky.actor.profile" 57 + class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" 58 + required> 59 + <p class="text-xs text-gray-500 mt-1"> 60 + Enter collection names separated by commas. Common collections: app.bsky.feed.post, app.bsky.actor.profile, app.bsky.feed.like 61 + </p> 62 + </div> 63 + 64 + <div> 65 + <label for="repos" class="block text-sm font-medium text-gray-700 mb-2">Specific DIDs (optional)</label> 66 + <textarea id="repos" 67 + name="repos" 68 + placeholder="did:plc:example1&#10;did:plc:example2" 69 + rows="4" 70 + class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea> 71 + <p class="text-xs text-gray-500 mt-1">Optional: Enter specific DIDs (one per line). Leave empty to discover and sync from all repositories that have the specified collections.</p> 72 + </div> 73 + 74 + <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> 75 + <div class="flex"> 76 + <div class="flex-shrink-0"> 77 + <span class="text-yellow-600">⚠️</span> 78 + </div> 79 + <div class="ml-3"> 80 + <h3 class="text-sm font-medium text-yellow-800">Note about bulk sync</h3> 81 + <div class="mt-2 text-sm text-yellow-700"> 82 + <p>Bulk sync can take several minutes and may fetch thousands of records. The operation runs in the foreground, so please be patient.</p> 83 + </div> 84 + </div> 85 + </div> 86 + </div> 87 + 88 + <button type="submit" 89 + id="sync-button" 90 + class="w-full bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-md font-medium"> 91 + <span class="default-text">Start Bulk Sync</span> 92 + <span class="htmx-indicator">🔄 Syncing...</span> 93 + </button> 94 + </form> 95 + 96 + <div id="sync-result" class="mt-6"></div> 97 + </div> 98 + </div> 99 + 100 + <div class="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-6"> 101 + <h3 class="text-lg font-semibold text-blue-800 mb-2">💡 Tips for Bulk Sync</h3> 102 + <ul class="space-y-2 text-blue-700"> 103 + <li class="flex items-start"> 104 + <span class="font-bold mr-2">•</span> 105 + <span>Start with popular collections like <code class="bg-blue-100 px-1 rounded">app.bsky.feed.post</code> for posts</span> 106 + </li> 107 + <li class="flex items-start"> 108 + <span class="font-bold mr-2">•</span> 109 + <span>Leave DIDs empty to discover all repositories automatically</span> 110 + </li> 111 + <li class="flex items-start"> 112 + <span class="font-bold mr-2">•</span> 113 + <span>Use the API endpoint for programmatic access: <code class="bg-blue-100 px-1 rounded">/xrpc/com.indexer.collections.bulkSync</code></span> 114 + </li> 115 + <li class="flex items-start"> 116 + <span class="font-bold mr-2">•</span> 117 + <span>Sync operations may take time - progress is logged to the server console</span> 118 + </li> 119 + </ul> 120 + </div> 121 + </div> 122 + {% endblock %}
+1
frontend/.gitignore
··· 1 + node_modules
+76
frontend/components/Layout.tsx
··· 1 + import { JSX } from "preact"; 2 + 3 + interface LayoutProps { 4 + title?: string; 5 + children: JSX.Element | JSX.Element[]; 6 + } 7 + 8 + export function Layout({ 9 + title = "Slice", 10 + children, 11 + }: LayoutProps) { 12 + return ( 13 + <html lang="en"> 14 + <head> 15 + <meta charSet="UTF-8" /> 16 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 17 + <title>{title}</title> 18 + <script src="https://unpkg.com/htmx.org@1.9.10"></script> 19 + <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> 20 + <script src="https://cdn.tailwindcss.com/3.4.1"></script> 21 + <style 22 + dangerouslySetInnerHTML={{ 23 + __html: ` 24 + .alert { 25 + padding: 1rem; 26 + margin-bottom: 1rem; 27 + border-radius: 0.375rem; 28 + border-width: 1px; 29 + } 30 + 31 + .alert-success { 32 + background-color: #dcfce7; 33 + border-color: #22c55e; 34 + color: #15803d; 35 + } 36 + 37 + .alert-warning { 38 + background-color: #fef3c7; 39 + border-color: #eab308; 40 + color: #a16207; 41 + } 42 + 43 + .alert-error { 44 + background-color: #fecaca; 45 + border-color: #ef4444; 46 + color: #dc2626; 47 + } 48 + 49 + .htmx-indicator { 50 + display: none; 51 + } 52 + 53 + .htmx-request .htmx-indicator { 54 + display: inline; 55 + } 56 + 57 + .htmx-request .default-text { 58 + display: none; 59 + } 60 + `, 61 + }} 62 + /> 63 + </head> 64 + <body className="bg-gray-100 min-h-screen"> 65 + <nav className="bg-blue-600 text-white p-4"> 66 + <div className="container mx-auto flex justify-between items-center"> 67 + <a href="/" className="text-xl font-bold hover:text-blue-200"> 68 + Slice 69 + </a> 70 + </div> 71 + </nav> 72 + <main className="container mx-auto mt-8 px-4">{children}</main> 73 + </body> 74 + </html> 75 + ); 76 + }
+26
frontend/deno.json
··· 1 + { 2 + "tasks": { 3 + "start": "deno run --allow-net main.tsx", 4 + "dev": "deno run --allow-net --watch main.tsx" 5 + }, 6 + "fmt": { 7 + "useTabs": false, 8 + "lineWidth": 80, 9 + "indentWidth": 2, 10 + "semiColons": true, 11 + "singleQuote": false, 12 + "proseWrap": "preserve", 13 + "include": ["src/", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 14 + "exclude": ["node_modules/", "dist/"] 15 + }, 16 + "compilerOptions": { 17 + "jsx": "precompile", 18 + "jsxImportSource": "preact" 19 + }, 20 + "imports": { 21 + "preact": "npm:preact@^10.27.1", 22 + "preact-render-to-string": "npm:preact-render-to-string@^6.5.13", 23 + "typed-htmx": "npm:typed-htmx@^0.3.1" 24 + }, 25 + "nodeModulesDir": "auto" 26 + }
+35
frontend/deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:preact-render-to-string@^6.5.13": "6.5.13_preact@10.27.1", 5 + "npm:preact@^10.27.1": "10.27.1", 6 + "npm:typed-htmx@~0.3.1": "0.3.1" 7 + }, 8 + "npm": { 9 + "preact-render-to-string@6.5.13_preact@10.27.1": { 10 + "integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==", 11 + "dependencies": [ 12 + "preact" 13 + ] 14 + }, 15 + "preact@10.27.1": { 16 + "integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==" 17 + }, 18 + "typed-html@3.0.1": { 19 + "integrity": "sha512-JKCM9zTfPDuPqQqdGZBWSEiItShliKkBFg5c6yOR8zth43v763XkAzTWaOlVqc0Y6p9ee8AaAbipGfUnCsYZUA==" 20 + }, 21 + "typed-htmx@0.3.1": { 22 + "integrity": "sha512-6WSPsukTIOEMsVbx5wzgVSvldLmgBUVcFIm2vJlBpRPtcbDOGC5y1IYrCWNX1yUlNsrv1Ngcw4gGM8jsPyNV7w==", 23 + "dependencies": [ 24 + "typed-html" 25 + ] 26 + } 27 + }, 28 + "workspace": { 29 + "dependencies": [ 30 + "npm:preact-render-to-string@^6.5.13", 31 + "npm:preact@^10.27.1", 32 + "npm:typed-htmx@~0.3.1" 33 + ] 34 + } 35 + }
+7
frontend/globals.d.ts
··· 1 + import "typed-htmx"; 2 + 3 + declare module "preact" { 4 + namespace JSX { 5 + interface HTMLAttributes extends HtmxAttributes {} 6 + } 7 + }
+131
frontend/main.tsx
··· 1 + import { render } from "preact-render-to-string"; 2 + import { IndexPage } from "./pages/IndexPage.tsx"; 3 + import { LoginPage } from "./pages/LoginPage.tsx"; 4 + import { SlicePage } from "./pages/SlicePage.tsx"; 5 + import { SliceCodegenPage } from "./pages/SliceCodegenPage.tsx"; 6 + import { SliceLexiconPage } from "./pages/SliceLexiconPage.tsx"; 7 + import { SliceRecordsPage } from "./pages/SliceRecordsPage.tsx"; 8 + import { SliceSyncPage } from "./pages/SliceSyncPage.tsx"; 9 + 10 + const handler = (req: Request): Response => { 11 + const url = new URL(req.url); 12 + const pathname = url.pathname; 13 + 14 + let html: string; 15 + 16 + // Parse slice routes 17 + const sliceMatch = pathname.match(/^\/slices\/([^\/]+)(.*)$/); 18 + 19 + if (pathname === "/") { 20 + // Slice list page 21 + const indexData = { 22 + slices: [ 23 + { id: "example", name: "Example Slice", createdAt: "2024-01-15T10:00:00Z" }, 24 + { id: "demo", name: "Demo Slice", createdAt: "2024-01-14T15:30:00Z" }, 25 + ], 26 + }; 27 + html = render(<IndexPage slices={indexData.slices} />); 28 + } else if (pathname === "/login") { 29 + // Login page 30 + html = render(<LoginPage />); 31 + } else if (sliceMatch) { 32 + const sliceId = sliceMatch[1]; 33 + const subPath = sliceMatch[2] || ""; 34 + 35 + const mockSliceData = { 36 + sliceId, 37 + sliceName: sliceId === "example" ? "Example Slice" : "Demo Slice", 38 + totalRecords: 1250, 39 + collections: [ 40 + { name: "com.chadtmiller.slice", count: 5 }, 41 + { name: "social.grain.gallery", count: 850 }, 42 + { name: "social.grain.comment", count: 400 }, 43 + ], 44 + }; 45 + 46 + switch (subPath) { 47 + case "": { 48 + // Slice overview page 49 + html = render(<SlicePage {...mockSliceData} currentTab="overview" />); 50 + break; 51 + } 52 + 53 + case "/records": { 54 + // Slice records page 55 + const recordsData = { 56 + ...mockSliceData, 57 + records: [ 58 + { 59 + uri: `at://did:plc:example/com.chadtmiller.slice/3k2a4b5c6d`, 60 + indexed_at: "2024-01-15 12:45:00", 61 + collection: "com.chadtmiller.slice", 62 + did: "did:plc:example", 63 + cid: "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua", 64 + value: true, 65 + pretty_value: `{\n "name": "${mockSliceData.sliceName}",\n "createdAt": "2024-01-15T12:45:00.000Z",\n "$type": "com.chadtmiller.slice"\n}`, 66 + }, 67 + ], 68 + availableCollections: mockSliceData.collections, 69 + }; 70 + html = render(<SliceRecordsPage {...recordsData} />); 71 + break; 72 + } 73 + 74 + case "/sync": { 75 + html = render(<SliceSyncPage {...mockSliceData} />); 76 + break; 77 + } 78 + 79 + case "/lexicon": { 80 + const lexiconData = { 81 + ...mockSliceData, 82 + lexicons: [ 83 + { 84 + nsid: "com.chadtmiller.slice", 85 + updated_at: "2024-01-15 10:30:00", 86 + pretty_definitions: `{\n "lexicon": 1,\n "id": "com.chadtmiller.slice",\n "defs": {\n "main": {\n "type": "record",\n "description": "Slice application record type"\n }\n }\n}`, 87 + }, 88 + ], 89 + }; 90 + html = render(<SliceLexiconPage {...lexiconData} />); 91 + break; 92 + } 93 + 94 + case "/codegen": { 95 + const codegenData = { 96 + ...mockSliceData, 97 + lexicons: [ 98 + { nsid: "com.chadtmiller.slice" }, 99 + { nsid: "social.grain.gallery" }, 100 + ], 101 + }; 102 + html = render(<SliceCodegenPage {...codegenData} />); 103 + break; 104 + } 105 + 106 + default: 107 + // 404 for unknown slice subpaths 108 + return Response.redirect(new URL("/", req.url), 302); 109 + } 110 + } else { 111 + // 404 page - redirect to home for now 112 + return Response.redirect(new URL("/", req.url), 302); 113 + } 114 + 115 + return new Response(`<!DOCTYPE html>${html}`, { 116 + status: 200, 117 + headers: { 118 + "content-type": "text/html", 119 + }, 120 + }); 121 + }; 122 + 123 + Deno.serve( 124 + { 125 + port: 8000, 126 + onListen: () => { 127 + console.log("Frontend server running on http://localhost:8000"); 128 + }, 129 + }, 130 + handler 131 + );
+100
frontend/pages/IndexPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface Slice { 4 + id: string; 5 + name: string; 6 + createdAt: string; 7 + } 8 + 9 + interface IndexPageProps { 10 + slices?: Slice[]; 11 + } 12 + 13 + export function IndexPage({ slices = [] }: IndexPageProps) { 14 + return ( 15 + <Layout title="Slices"> 16 + <div className="max-w-4xl mx-auto"> 17 + <div className="flex justify-between items-center mb-8"> 18 + <h1 className="text-3xl font-bold text-gray-800"> 19 + Slices 20 + </h1> 21 + <button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> 22 + + Create Slice 23 + </button> 24 + </div> 25 + 26 + {slices.length > 0 ? ( 27 + <div className="bg-white rounded-lg shadow-md"> 28 + <div className="px-6 py-4 border-b border-gray-200"> 29 + <h2 className="text-lg font-semibold text-gray-800"> 30 + Your Slices ({slices.length}) 31 + </h2> 32 + </div> 33 + <div className="divide-y divide-gray-200"> 34 + {slices.map((slice) => ( 35 + <a 36 + key={slice.id} 37 + href={`/slices/${slice.id}`} 38 + className="block px-6 py-4 hover:bg-gray-50" 39 + > 40 + <div className="flex justify-between items-center"> 41 + <div> 42 + <h3 className="text-lg font-medium text-gray-900"> 43 + {slice.name} 44 + </h3> 45 + <p className="text-sm text-gray-500"> 46 + Created {new Date(slice.createdAt).toLocaleDateString()} 47 + </p> 48 + </div> 49 + <div className="text-gray-400"> 50 + <svg 51 + className="h-5 w-5" 52 + fill="none" 53 + viewBox="0 0 24 24" 54 + stroke="currentColor" 55 + > 56 + <path 57 + strokeLinecap="round" 58 + strokeLinejoin="round" 59 + strokeWidth={2} 60 + d="M9 5l7 7-7 7" 61 + /> 62 + </svg> 63 + </div> 64 + </div> 65 + </a> 66 + ))} 67 + </div> 68 + </div> 69 + ) : ( 70 + <div className="bg-white rounded-lg shadow-md p-8 text-center"> 71 + <div className="text-gray-400 mb-4"> 72 + <svg 73 + className="mx-auto h-16 w-16" 74 + fill="none" 75 + viewBox="0 0 24 24" 76 + stroke="currentColor" 77 + > 78 + <path 79 + strokeLinecap="round" 80 + strokeLinejoin="round" 81 + strokeWidth={1} 82 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 83 + /> 84 + </svg> 85 + </div> 86 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 87 + No slices yet 88 + </h3> 89 + <p className="text-gray-500 mb-6"> 90 + Create your first slice to get started organizing your AT Protocol data. 91 + </p> 92 + <button className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded"> 93 + Create Your First Slice 94 + </button> 95 + </div> 96 + )} 97 + </div> 98 + </Layout> 99 + ); 100 + }
+83
frontend/pages/LoginPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface LoginPageProps { 4 + error?: string; 5 + } 6 + 7 + export function LoginPage({ error }: LoginPageProps) { 8 + return ( 9 + <Layout title="Login - Slice"> 10 + <div className="max-w-md mx-auto mt-16"> 11 + <div className="bg-white rounded-lg shadow-md p-8"> 12 + <div className="text-center mb-8"> 13 + <h1 className="text-3xl font-bold text-gray-800 mb-2"> 14 + Welcome to Slice 15 + </h1> 16 + <p className="text-gray-600"> 17 + Sign in with your AT Protocol handle 18 + </p> 19 + </div> 20 + 21 + {error && ( 22 + <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6"> 23 + <p className="text-red-700 text-sm">{error}</p> 24 + </div> 25 + )} 26 + 27 + <form method="post" action="/login" className="space-y-6"> 28 + <div> 29 + <label 30 + htmlFor="handle" 31 + className="block text-sm font-medium text-gray-700 mb-2" 32 + > 33 + AT Protocol Handle 34 + </label> 35 + <input 36 + type="text" 37 + id="handle" 38 + name="handle" 39 + placeholder="alice.bsky.social" 40 + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" 41 + required 42 + /> 43 + <p className="text-xs text-gray-500 mt-1"> 44 + Enter your Bluesky handle or custom domain 45 + </p> 46 + </div> 47 + 48 + <button 49 + type="submit" 50 + className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-md transition-colors" 51 + > 52 + Sign In 53 + </button> 54 + </form> 55 + 56 + <div className="mt-8 text-center"> 57 + <p className="text-sm text-gray-500 mb-4"> 58 + Don't have an AT Protocol account? 59 + </p> 60 + <div className="space-y-2"> 61 + <a 62 + href="https://bsky.app" 63 + target="_blank" 64 + rel="noopener noreferrer" 65 + className="block text-blue-600 hover:text-blue-800 text-sm" 66 + > 67 + Create account on Bluesky → 68 + </a> 69 + <a 70 + href="https://atproto.com" 71 + target="_blank" 72 + rel="noopener noreferrer" 73 + className="block text-blue-600 hover:text-blue-800 text-sm" 74 + > 75 + Learn about AT Protocol → 76 + </a> 77 + </div> 78 + </div> 79 + </div> 80 + </div> 81 + </Layout> 82 + ); 83 + }
+167
frontend/pages/SliceCodegenPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface SliceCodegenPageProps { 4 + sliceName?: string; 5 + sliceId?: string; 6 + } 7 + 8 + export function SliceCodegenPage({ 9 + sliceName = "My Slice", 10 + sliceId = "example", 11 + }: SliceCodegenPageProps) { 12 + const tabs = [ 13 + { id: "overview", name: "Overview", href: `/slices/${sliceId}` }, 14 + { id: "records", name: "Records", href: `/slices/${sliceId}/records` }, 15 + { id: "sync", name: "Sync", href: `/slices/${sliceId}/sync` }, 16 + { id: "lexicon", name: "Lexicons", href: `/slices/${sliceId}/lexicon` }, 17 + { id: "codegen", name: "Code Gen", href: `/slices/${sliceId}/codegen` }, 18 + ]; 19 + 20 + return ( 21 + <Layout title={`${sliceName} - Code Generation`}> 22 + <div className="max-w-4xl mx-auto"> 23 + <div className="flex items-center justify-between mb-8"> 24 + <div className="flex items-center"> 25 + <a 26 + href="/" 27 + className="text-blue-600 hover:text-blue-800 mr-4" 28 + > 29 + ← Back to Slices 30 + </a> 31 + <h1 className="text-3xl font-bold text-gray-800"> 32 + {sliceName} 33 + </h1> 34 + </div> 35 + </div> 36 + 37 + {/* Tab Navigation */} 38 + <div className="border-b border-gray-200 mb-8"> 39 + <nav className="-mb-px flex space-x-8"> 40 + {tabs.map((tab) => ( 41 + <a 42 + key={tab.id} 43 + href={tab.href} 44 + className={`py-2 px-1 border-b-2 font-medium text-sm ${ 45 + tab.id === "codegen" 46 + ? "border-blue-500 text-blue-600" 47 + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" 48 + }`} 49 + > 50 + {tab.name} 51 + </a> 52 + ))} 53 + </nav> 54 + </div> 55 + 56 + <div className="bg-white rounded-lg shadow-md p-6 mb-6"> 57 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 58 + Generate TypeScript Client 59 + </h2> 60 + <p className="text-gray-600 mb-6"> 61 + Generate a TypeScript client library from the lexicon definitions in this slice. 62 + </p> 63 + 64 + <form 65 + method="post" 66 + action={`/slices/${sliceId}/codegen/generate`} 67 + className="space-y-4" 68 + > 69 + <div> 70 + <label className="block text-sm font-medium text-gray-700 mb-2"> 71 + Package Name 72 + </label> 73 + <input 74 + type="text" 75 + name="packageName" 76 + value={`@${sliceId}/client`} 77 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 78 + placeholder="@my-slice/client" 79 + /> 80 + </div> 81 + 82 + <div> 83 + <label className="block text-sm font-medium text-gray-700 mb-2"> 84 + Output Format 85 + </label> 86 + <select 87 + name="format" 88 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 89 + > 90 + <option value="typescript">TypeScript</option> 91 + <option value="javascript">JavaScript</option> 92 + </select> 93 + </div> 94 + 95 + <div className="flex items-center"> 96 + <input 97 + type="checkbox" 98 + name="includeXrpc" 99 + id="includeXrpc" 100 + checked 101 + className="h-4 w-4 text-blue-600 border-gray-300 rounded" 102 + /> 103 + <label 104 + htmlFor="includeXrpc" 105 + className="ml-2 text-sm text-gray-700" 106 + > 107 + Include XRPC client methods 108 + </label> 109 + </div> 110 + 111 + <button 112 + type="submit" 113 + className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-md" 114 + > 115 + Generate Client 116 + </button> 117 + </form> 118 + </div> 119 + 120 + <div className="bg-white rounded-lg shadow-md"> 121 + <div className="px-6 py-4 border-b border-gray-200"> 122 + <h2 className="text-lg font-semibold text-gray-800"> 123 + Generated Clients 124 + </h2> 125 + </div> 126 + <div className="p-6"> 127 + <div className="bg-gray-50 rounded-lg p-6 text-center"> 128 + <div className="text-gray-400 mb-4"> 129 + <svg 130 + className="mx-auto h-16 w-16" 131 + fill="none" 132 + viewBox="0 0 24 24" 133 + stroke="currentColor" 134 + > 135 + <path 136 + strokeLinecap="round" 137 + strokeLinejoin="round" 138 + strokeWidth={1} 139 + d="M10 20l4-16m18 4l4 4-4 4M6 16l-4-4 4-4" 140 + /> 141 + </svg> 142 + </div> 143 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 144 + No clients generated yet 145 + </h3> 146 + <p className="text-gray-500"> 147 + Generate a TypeScript client from your slice's lexicon definitions. 148 + </p> 149 + </div> 150 + </div> 151 + </div> 152 + 153 + <div className="mt-6 bg-green-50 border border-green-200 rounded-lg p-6"> 154 + <h3 className="text-lg font-semibold text-green-800 mb-2"> 155 + ⚡ Generated Client Features 156 + </h3> 157 + <ul className="text-green-700 space-y-1 text-sm"> 158 + <li>• Type-safe interfaces for all record types</li> 159 + <li>• XRPC client methods for API endpoints</li> 160 + <li>• Validation helpers for record schemas</li> 161 + <li>• Ready to use in TypeScript/JavaScript projects</li> 162 + </ul> 163 + </div> 164 + </div> 165 + </Layout> 166 + ); 167 + }
+144
frontend/pages/SliceLexiconPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface SliceLexiconPageProps { 4 + sliceName?: string; 5 + sliceId?: string; 6 + } 7 + 8 + export function SliceLexiconPage({ 9 + sliceName = "My Slice", 10 + sliceId = "example", 11 + }: SliceLexiconPageProps) { 12 + const tabs = [ 13 + { id: "overview", name: "Overview", href: `/slices/${sliceId}` }, 14 + { id: "records", name: "Records", href: `/slices/${sliceId}/records` }, 15 + { id: "sync", name: "Sync", href: `/slices/${sliceId}/sync` }, 16 + { id: "lexicon", name: "Lexicons", href: `/slices/${sliceId}/lexicon` }, 17 + { id: "codegen", name: "Code Gen", href: `/slices/${sliceId}/codegen` }, 18 + ]; 19 + 20 + return ( 21 + <Layout title={`${sliceName} - Lexicons`}> 22 + <div className="max-w-4xl mx-auto"> 23 + <div className="flex items-center justify-between mb-8"> 24 + <div className="flex items-center"> 25 + <a 26 + href="/" 27 + className="text-blue-600 hover:text-blue-800 mr-4" 28 + > 29 + ← Back to Slices 30 + </a> 31 + <h1 className="text-3xl font-bold text-gray-800"> 32 + {sliceName} 33 + </h1> 34 + </div> 35 + </div> 36 + 37 + {/* Tab Navigation */} 38 + <div className="border-b border-gray-200 mb-8"> 39 + <nav className="-mb-px flex space-x-8"> 40 + {tabs.map((tab) => ( 41 + <a 42 + key={tab.id} 43 + href={tab.href} 44 + className={`py-2 px-1 border-b-2 font-medium text-sm ${ 45 + tab.id === "lexicon" 46 + ? "border-blue-500 text-blue-600" 47 + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" 48 + }`} 49 + > 50 + {tab.name} 51 + </a> 52 + ))} 53 + </nav> 54 + </div> 55 + 56 + <div className="bg-white rounded-lg shadow-md p-6 mb-6"> 57 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 58 + Upload Lexicon Definitions 59 + </h2> 60 + <p className="text-gray-600 mb-6"> 61 + Upload lexicon schema files to define custom record types for this slice. 62 + </p> 63 + 64 + <form 65 + method="post" 66 + action={`/slices/${sliceId}/lexicon/upload`} 67 + enctype="multipart/form-data" 68 + className="space-y-4" 69 + > 70 + <div> 71 + <label className="block text-sm font-medium text-gray-700 mb-2"> 72 + Lexicon File 73 + </label> 74 + <input 75 + type="file" 76 + name="lexicon" 77 + accept=".zip,.json" 78 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 79 + /> 80 + <p className="text-sm text-gray-500 mt-1"> 81 + Upload a ZIP file containing lexicon definitions or a single JSON file 82 + </p> 83 + </div> 84 + 85 + <button 86 + type="submit" 87 + className="bg-purple-500 hover:bg-purple-600 text-white px-6 py-2 rounded-md" 88 + > 89 + Upload Lexicon 90 + </button> 91 + </form> 92 + </div> 93 + 94 + <div className="bg-white rounded-lg shadow-md"> 95 + <div className="px-6 py-4 border-b border-gray-200"> 96 + <h2 className="text-lg font-semibold text-gray-800"> 97 + Slice Lexicons 98 + </h2> 99 + </div> 100 + <div className="p-6"> 101 + <div className="bg-gray-50 rounded-lg p-6 text-center"> 102 + <div className="text-gray-400 mb-4"> 103 + <svg 104 + className="mx-auto h-16 w-16" 105 + fill="none" 106 + viewBox="0 0 24 24" 107 + stroke="currentColor" 108 + > 109 + <path 110 + strokeLinecap="round" 111 + strokeLinejoin="round" 112 + strokeWidth={1} 113 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 114 + /> 115 + </svg> 116 + </div> 117 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 118 + No lexicons uploaded 119 + </h3> 120 + <p className="text-gray-500"> 121 + Upload lexicon definitions to define custom schemas for this slice. 122 + </p> 123 + </div> 124 + </div> 125 + </div> 126 + 127 + <div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-6"> 128 + <h3 className="text-lg font-semibold text-yellow-800 mb-2"> 129 + 📚 About Lexicons 130 + </h3> 131 + <p className="text-yellow-700 text-sm mb-2"> 132 + Lexicons define the schema for AT Protocol records. They specify: 133 + </p> 134 + <ul className="text-yellow-700 space-y-1 text-sm"> 135 + <li>• Record structure and field types</li> 136 + <li>• Validation rules and constraints</li> 137 + <li>• XRPC endpoint definitions</li> 138 + <li>• Custom collection types</li> 139 + </ul> 140 + </div> 141 + </div> 142 + </Layout> 143 + ); 144 + }
+216
frontend/pages/SlicePage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface Collection { 4 + name: string; 5 + count: number; 6 + } 7 + 8 + interface SlicePageProps { 9 + totalRecords?: number; 10 + collections?: Collection[]; 11 + sliceName?: string; 12 + sliceId?: string; 13 + currentTab?: string; 14 + } 15 + 16 + export function SlicePage({ 17 + totalRecords = 0, 18 + collections = [], 19 + sliceName = "My Slice", 20 + sliceId = "example", 21 + currentTab = "overview", 22 + }: SlicePageProps) { 23 + const tabs = [ 24 + { id: "overview", name: "Overview", href: `/slices/${sliceId}` }, 25 + { id: "records", name: "Records", href: `/slices/${sliceId}/records` }, 26 + { id: "sync", name: "Sync", href: `/slices/${sliceId}/sync` }, 27 + { id: "lexicon", name: "Lexicons", href: `/slices/${sliceId}/lexicon` }, 28 + { id: "codegen", name: "Code Gen", href: `/slices/${sliceId}/codegen` }, 29 + ]; 30 + 31 + return ( 32 + <Layout title={sliceName}> 33 + <div className="max-w-4xl mx-auto"> 34 + <div className="flex items-center justify-between mb-8"> 35 + <div className="flex items-center"> 36 + <a 37 + href="/" 38 + className="text-blue-600 hover:text-blue-800 mr-4" 39 + > 40 + ← Back to Slices 41 + </a> 42 + <h1 className="text-3xl font-bold text-gray-800"> 43 + {sliceName} 44 + </h1> 45 + </div> 46 + </div> 47 + 48 + {/* Tab Navigation */} 49 + <div className="border-b border-gray-200 mb-8"> 50 + <nav className="-mb-px flex space-x-8"> 51 + {tabs.map((tab) => ( 52 + <a 53 + key={tab.id} 54 + href={tab.href} 55 + className={`py-2 px-1 border-b-2 font-medium text-sm ${ 56 + currentTab === tab.id 57 + ? "border-blue-500 text-blue-600" 58 + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" 59 + }`} 60 + > 61 + {tab.name} 62 + </a> 63 + ))} 64 + </nav> 65 + </div> 66 + 67 + {totalRecords > 0 && ( 68 + <div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> 69 + <h2 className="text-xl font-semibold text-blue-800 mb-2"> 70 + 📊 Database Status 71 + </h2> 72 + <p className="text-blue-700"> 73 + Currently indexing <strong>{totalRecords}</strong> records across{" "} 74 + <strong>{collections.length}</strong> collections. 75 + </p> 76 + </div> 77 + )} 78 + 79 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 80 + <div className="bg-white rounded-lg shadow-md p-6"> 81 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 82 + 📝 View Records 83 + </h2> 84 + <p className="text-gray-600 mb-4"> 85 + Browse indexed AT Protocol records by collection. 86 + </p> 87 + {collections.length > 0 ? ( 88 + <a 89 + href={`/slices/${sliceId}/records`} 90 + className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" 91 + > 92 + Browse Records 93 + </a> 94 + ) : ( 95 + <p className="text-gray-500 text-sm"> 96 + No records synced yet. Start by syncing some records! 97 + </p> 98 + )} 99 + </div> 100 + 101 + <div className="bg-white rounded-lg shadow-md p-6"> 102 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 103 + 📚 Lexicon Definitions 104 + </h2> 105 + <p className="text-gray-600 mb-4"> 106 + View lexicon definitions and schemas. 107 + </p> 108 + <a 109 + href={`/slices/${sliceId}/lexicon`} 110 + className="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded" 111 + > 112 + View Lexicons 113 + </a> 114 + </div> 115 + 116 + <div className="bg-white rounded-lg shadow-md p-6"> 117 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 118 + ⚡ Code Generation 119 + </h2> 120 + <p className="text-gray-600 mb-4"> 121 + Generate TypeScript client from your lexicon definitions. 122 + </p> 123 + <a 124 + href={`/slices/${sliceId}/codegen`} 125 + className="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded" 126 + > 127 + Generate Client 128 + </a> 129 + </div> 130 + 131 + <div className="bg-white rounded-lg shadow-md p-6"> 132 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 133 + 🔄 Bulk Sync 134 + </h2> 135 + <p className="text-gray-600 mb-4"> 136 + Sync entire collections from AT Protocol networks. 137 + </p> 138 + <a 139 + href={`/slices/${sliceId}/sync`} 140 + className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded" 141 + > 142 + Start Bulk Sync 143 + </a> 144 + </div> 145 + 146 + {collections.length > 0 ? ( 147 + <div className="bg-white rounded-lg shadow-md p-6"> 148 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 149 + 📊 Synced Collections 150 + </h2> 151 + <p className="text-gray-600 mb-4"> 152 + Collections currently indexed in the database. 153 + </p> 154 + <div className="space-y-2 max-h-40 overflow-y-auto"> 155 + {collections.map((collection) => ( 156 + <a 157 + key={collection.name} 158 + href={`/slices/${sliceId}/records?collection=${collection.name}`} 159 + className="flex justify-between items-center text-blue-600 hover:underline text-sm" 160 + > 161 + <span>{collection.name}</span> 162 + <span className="text-gray-500">{collection.count}</span> 163 + </a> 164 + ))} 165 + </div> 166 + </div> 167 + ) : ( 168 + <div className="bg-white rounded-lg shadow-md p-6"> 169 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 170 + 🌟 Get Started 171 + </h2> 172 + <p className="text-gray-600 mb-4"> 173 + No records indexed yet. Start by syncing some AT Protocol 174 + collections! 175 + </p> 176 + <div className="space-y-2 text-sm"> 177 + <p className="text-gray-500">Try syncing collections like:</p> 178 + <code className="block bg-gray-100 p-2 rounded text-xs"> 179 + app.bsky.feed.post 180 + </code> 181 + <code className="block bg-gray-100 p-2 rounded text-xs"> 182 + app.bsky.actor.profile 183 + </code> 184 + </div> 185 + </div> 186 + )} 187 + </div> 188 + 189 + <div className="mt-12 bg-white rounded-lg shadow-md p-6"> 190 + <h2 className="text-2xl font-semibold text-gray-800 mb-4"> 191 + API Endpoints 192 + </h2> 193 + <div className="space-y-4"> 194 + <div> 195 + <code className="bg-gray-100 px-2 py-1 rounded"> 196 + GET /xrpc/com.indexer.records.list?collection=app.bsky.feed.post 197 + </code> 198 + <p className="text-gray-600 mt-1"> 199 + List records for a collection 200 + </p> 201 + </div> 202 + <div> 203 + <code className="bg-gray-100 px-2 py-1 rounded"> 204 + POST /xrpc/com.indexer.collections.bulkSync 205 + </code> 206 + <p className="text-gray-600 mt-1"> 207 + Bulk sync collections (JSON:{" "} 208 + {`{"collections": ["app.bsky.feed.post"]}`}) 209 + </p> 210 + </div> 211 + </div> 212 + </div> 213 + </div> 214 + </Layout> 215 + ); 216 + }
+238
frontend/pages/SliceRecordsPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface Record { 4 + uri: string; 5 + indexed_at: string; 6 + collection: string; 7 + did: string; 8 + cid: string; 9 + value?: any; 10 + pretty_value?: string; 11 + } 12 + 13 + interface AvailableCollection { 14 + name: string; 15 + count: number; 16 + } 17 + 18 + interface SliceRecordsPageProps { 19 + records?: Record[]; 20 + availableCollections?: AvailableCollection[]; 21 + collection?: string; 22 + author?: string; 23 + sliceName?: string; 24 + sliceId?: string; 25 + } 26 + 27 + export function SliceRecordsPage({ 28 + records = [], 29 + availableCollections = [], 30 + collection = "", 31 + author = "", 32 + sliceName = "My Slice", 33 + sliceId = "example", 34 + }: SliceRecordsPageProps) { 35 + const tabs = [ 36 + { id: "overview", name: "Overview", href: `/slices/${sliceId}` }, 37 + { id: "records", name: "Records", href: `/slices/${sliceId}/records` }, 38 + { id: "sync", name: "Sync", href: `/slices/${sliceId}/sync` }, 39 + { id: "lexicon", name: "Lexicons", href: `/slices/${sliceId}/lexicon` }, 40 + { id: "codegen", name: "Code Gen", href: `/slices/${sliceId}/codegen` }, 41 + ]; 42 + 43 + return ( 44 + <Layout title={`${sliceName} - Records`}> 45 + <div className="max-w-4xl mx-auto"> 46 + <div className="flex items-center justify-between mb-8"> 47 + <div className="flex items-center"> 48 + <a 49 + href="/" 50 + className="text-blue-600 hover:text-blue-800 mr-4" 51 + > 52 + ← Back to Slices 53 + </a> 54 + <h1 className="text-3xl font-bold text-gray-800"> 55 + {sliceName} 56 + </h1> 57 + </div> 58 + </div> 59 + 60 + {/* Tab Navigation */} 61 + <div className="border-b border-gray-200 mb-8"> 62 + <nav className="-mb-px flex space-x-8"> 63 + {tabs.map((tab) => ( 64 + <a 65 + key={tab.id} 66 + href={tab.href} 67 + className={`py-2 px-1 border-b-2 font-medium text-sm ${ 68 + tab.id === "records" 69 + ? "border-blue-500 text-blue-600" 70 + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" 71 + }`} 72 + > 73 + {tab.name} 74 + </a> 75 + ))} 76 + </nav> 77 + </div> 78 + 79 + <div className="bg-white rounded-lg shadow-md p-6 mb-6"> 80 + <div className="flex justify-between items-center mb-4"> 81 + <h2 className="text-xl font-semibold">Filter Records</h2> 82 + </div> 83 + <form 84 + className="grid grid-cols-1 md:grid-cols-3 gap-4" 85 + method="get" 86 + _="on submit 87 + if #author.value is empty 88 + remove @name from #author 89 + end" 90 + > 91 + <div> 92 + <label className="block text-sm font-medium text-gray-700 mb-2"> 93 + Collection 94 + </label> 95 + <select 96 + name="collection" 97 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 98 + > 99 + <option value="">All Collections</option> 100 + {availableCollections.map((coll) => ( 101 + <option 102 + key={coll.name} 103 + value={coll.name} 104 + selected={coll.name === collection} 105 + > 106 + {coll.name} ({coll.count}) 107 + </option> 108 + ))} 109 + </select> 110 + </div> 111 + 112 + <div> 113 + <label className="block text-sm font-medium text-gray-700 mb-2"> 114 + Author DID 115 + </label> 116 + <input 117 + type="text" 118 + name="author" 119 + id="author" 120 + value={author} 121 + placeholder="did:plc:..." 122 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 123 + /> 124 + </div> 125 + 126 + <div className="flex items-end"> 127 + <button 128 + type="submit" 129 + className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md" 130 + > 131 + Filter 132 + </button> 133 + </div> 134 + </form> 135 + </div> 136 + 137 + {records.length > 0 ? ( 138 + <div className="bg-white rounded-lg shadow-md"> 139 + <div className="px-6 py-4 border-b border-gray-200"> 140 + <h2 className="text-lg font-semibold"> 141 + Records ({records.length}) 142 + </h2> 143 + </div> 144 + <div className="divide-y divide-gray-200"> 145 + {records.map((record) => ( 146 + <div key={record.uri} className="p-6"> 147 + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> 148 + <div> 149 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 150 + Metadata 151 + </h3> 152 + <dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm"> 153 + <div className="grid grid-cols-3 gap-4"> 154 + <dt className="font-medium text-gray-500">URI:</dt> 155 + <dd className="col-span-2 text-gray-900 break-all"> 156 + {record.uri} 157 + </dd> 158 + </div> 159 + <div className="grid grid-cols-3 gap-4"> 160 + <dt className="font-medium text-gray-500"> 161 + Collection: 162 + </dt> 163 + <dd className="col-span-2 text-gray-900"> 164 + {record.collection} 165 + </dd> 166 + </div> 167 + <div className="grid grid-cols-3 gap-4"> 168 + <dt className="font-medium text-gray-500">DID:</dt> 169 + <dd className="col-span-2 text-gray-900 break-all"> 170 + {record.did} 171 + </dd> 172 + </div> 173 + <div className="grid grid-cols-3 gap-4"> 174 + <dt className="font-medium text-gray-500">CID:</dt> 175 + <dd className="col-span-2 text-gray-900 break-all"> 176 + {record.cid} 177 + </dd> 178 + </div> 179 + <div className="grid grid-cols-3 gap-4"> 180 + <dt className="font-medium text-gray-500"> 181 + Indexed: 182 + </dt> 183 + <dd className="col-span-2 text-gray-900"> 184 + {new Date(record.indexed_at).toLocaleString()} 185 + </dd> 186 + </div> 187 + </dl> 188 + </div> 189 + <div> 190 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 191 + Record Data 192 + </h3> 193 + <pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64"> 194 + {record.pretty_value || JSON.stringify(record.value, null, 2)} 195 + </pre> 196 + </div> 197 + </div> 198 + </div> 199 + ))} 200 + </div> 201 + </div> 202 + ) : ( 203 + <div className="bg-white rounded-lg shadow-md p-8 text-center"> 204 + <div className="text-gray-400 mb-4"> 205 + <svg 206 + className="mx-auto h-16 w-16" 207 + fill="none" 208 + viewBox="0 0 24 24" 209 + stroke="currentColor" 210 + > 211 + <path 212 + strokeLinecap="round" 213 + strokeLinejoin="round" 214 + strokeWidth={1} 215 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 216 + /> 217 + </svg> 218 + </div> 219 + <h3 className="text-lg font-medium text-gray-900 mb-2"> 220 + No records found 221 + </h3> 222 + <p className="text-gray-500 mb-6"> 223 + {collection || author 224 + ? "Try adjusting your filters or sync some data first." 225 + : "Start by syncing some AT Protocol collections."} 226 + </p> 227 + <a 228 + href={`/slices/${sliceId}/sync`} 229 + className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded" 230 + > 231 + Go to Sync 232 + </a> 233 + </div> 234 + )} 235 + </div> 236 + </Layout> 237 + ); 238 + }
+133
frontend/pages/SliceSyncPage.tsx
··· 1 + import { Layout } from "../components/Layout.tsx"; 2 + 3 + interface SliceSyncPageProps { 4 + sliceName?: string; 5 + sliceId?: string; 6 + } 7 + 8 + export function SliceSyncPage({ 9 + sliceName = "My Slice", 10 + sliceId = "example", 11 + }: SliceSyncPageProps) { 12 + const tabs = [ 13 + { id: "overview", name: "Overview", href: `/slices/${sliceId}` }, 14 + { id: "records", name: "Records", href: `/slices/${sliceId}/records` }, 15 + { id: "sync", name: "Sync", href: `/slices/${sliceId}/sync` }, 16 + { id: "lexicon", name: "Lexicons", href: `/slices/${sliceId}/lexicon` }, 17 + { id: "codegen", name: "Code Gen", href: `/slices/${sliceId}/codegen` }, 18 + ]; 19 + 20 + return ( 21 + <Layout title={`${sliceName} - Sync`}> 22 + <div className="max-w-4xl mx-auto"> 23 + <div className="flex items-center justify-between mb-8"> 24 + <div className="flex items-center"> 25 + <a 26 + href="/" 27 + className="text-blue-600 hover:text-blue-800 mr-4" 28 + > 29 + ← Back to Slices 30 + </a> 31 + <h1 className="text-3xl font-bold text-gray-800"> 32 + {sliceName} 33 + </h1> 34 + </div> 35 + </div> 36 + 37 + {/* Tab Navigation */} 38 + <div className="border-b border-gray-200 mb-8"> 39 + <nav className="-mb-px flex space-x-8"> 40 + {tabs.map((tab) => ( 41 + <a 42 + key={tab.id} 43 + href={tab.href} 44 + className={`py-2 px-1 border-b-2 font-medium text-sm ${ 45 + tab.id === "sync" 46 + ? "border-blue-500 text-blue-600" 47 + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" 48 + }`} 49 + > 50 + {tab.name} 51 + </a> 52 + ))} 53 + </nav> 54 + </div> 55 + 56 + <div className="bg-white rounded-lg shadow-md p-6 mb-6"> 57 + <h2 className="text-xl font-semibold text-gray-800 mb-4"> 58 + Bulk Sync Collections 59 + </h2> 60 + <p className="text-gray-600 mb-6"> 61 + Sync entire collections from AT Protocol networks to this slice. 62 + </p> 63 + 64 + <form 65 + method="post" 66 + action={`/slices/${sliceId}/sync`} 67 + className="space-y-4" 68 + > 69 + <div> 70 + <label className="block text-sm font-medium text-gray-700 mb-2"> 71 + Collections to Sync 72 + </label> 73 + <textarea 74 + name="collections" 75 + rows={6} 76 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 77 + placeholder="Enter collections, one per line or comma-separated: 78 + 79 + app.bsky.feed.post 80 + app.bsky.actor.profile 81 + social.grain.gallery" 82 + /> 83 + </div> 84 + 85 + <div> 86 + <label className="block text-sm font-medium text-gray-700 mb-2"> 87 + Specific Repositories (Optional) 88 + </label> 89 + <textarea 90 + name="repos" 91 + rows={4} 92 + className="block w-full border border-gray-300 rounded-md px-3 py-2" 93 + placeholder="Leave empty to sync all repositories, or specify DIDs: 94 + 95 + did:plc:example1 96 + did:plc:example2" 97 + /> 98 + </div> 99 + 100 + <div className="flex space-x-4"> 101 + <button 102 + type="submit" 103 + className="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-md" 104 + > 105 + Start Bulk Sync 106 + </button> 107 + <button 108 + type="button" 109 + className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-md" 110 + _="on click 111 + set #collections.value to 'app.bsky.feed.post, app.bsky.actor.profile'" 112 + > 113 + Use Popular Collections 114 + </button> 115 + </div> 116 + </form> 117 + </div> 118 + 119 + <div className="bg-blue-50 border border-blue-200 rounded-lg p-6"> 120 + <h3 className="text-lg font-semibold text-blue-800 mb-2"> 121 + 💡 Tips for Syncing 122 + </h3> 123 + <ul className="text-blue-700 space-y-1 text-sm"> 124 + <li>• Start with popular collections like <code>app.bsky.feed.post</code></li> 125 + <li>• Large syncs may take several minutes to complete</li> 126 + <li>• Leave repositories empty to sync from all available users</li> 127 + <li>• Use the Records tab to browse synced data</li> 128 + </ul> 129 + </div> 130 + </div> 131 + </Layout> 132 + ); 133 + }
+27
lexicons/com/chadtmiller/slice.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.chadtmiller.slice", 4 + "description": "Slice application record type", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["name", "createdAt"], 12 + "properties": { 13 + "name": { 14 + "type": "string", 15 + "description": "Name of the slice", 16 + "maxLength": 256 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime", 21 + "description": "When the slice was created" 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }