interactive intro to open social at-me.zzstoatzz.io

Compare changes

Choose any two refs to compare.

+6 -2
.gitignore
··· 1 - /target 2 - sandbox/
··· 1 + node_modules/ 2 + dist/ 3 + .env 4 + .env.local 5 + *.log 6 + *.tmp
+9
.pre-commit-config.yaml
···
··· 1 + repos: 2 + - repo: local 3 + hooks: 4 + - id: vite-build 5 + name: vite build 6 + entry: bun run build 7 + language: system 8 + types: [javascript] 9 + pass_filenames: false
-20
.tangled/workflows/check.yaml
··· 1 - engine: nixery 2 - 3 - when: 4 - - event: ["push", "pull_request"] 5 - 6 - dependencies: 7 - nixpkgs: 8 - - rustc 9 - - cargo 10 - - rustfmt 11 - - clippy 12 - 13 - steps: 14 - - name: check formatting 15 - command: | 16 - cargo fmt --check 17 - 18 - - name: run clippy 19 - command: | 20 - cargo clippy -- -D warnings
···
+27
.tangled/workflows/ci.yaml
···
··· 1 + when: 2 + - event: ["push"] 3 + branch: main 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - bun 10 + - curl 11 + 12 + environment: 13 + WISP_DID: "did:plc:xbtmt2zjwlrfegqvch7fboei" 14 + WISP_SITE_NAME: "at-me" 15 + 16 + steps: 17 + - name: install dependencies 18 + command: bun install 19 + 20 + - name: build 21 + command: bun run build 22 + 23 + - name: deploy to wisp 24 + command: | 25 + curl -sSL https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 26 + chmod +x wisp-cli 27 + ./wisp-cli deploy "$WISP_DID" --path ./dist --site "$WISP_SITE_NAME" --password "$WISP_APP_PASSWORD"
-14
.tangled/workflows/deploy.yaml
··· 1 - engine: nixery 2 - 3 - when: 4 - - event: ["push"] 5 - branch: ["main"] 6 - 7 - dependencies: 8 - nixpkgs: 9 - - flyctl 10 - 11 - steps: 12 - - name: Deploy to fly.io 13 - command: | 14 - flyctl deploy --remote-only
···
+28
CLAUDE.md
···
··· 1 + # at-me 2 + 3 + ATProto PDS visualization tool - shows your identity, apps, and data collections. 4 + 5 + ## Tech Stack 6 + 7 + - Pure client-side JavaScript (no backend) 8 + - Vite for development and building 9 + - Direct ATProto API calls (PDS, PLC directory, Bluesky AppView) 10 + - Jetstream WebSocket for firehose streaming 11 + - Client-side MST (Merkle Search Tree) visualization 12 + 13 + ## Development 14 + 15 + - Use `bun run dev` for local development with hot reloading 16 + - `bun run build` to build for production 17 + - `bun run preview` to preview the production build 18 + 19 + ## Key files 20 + 21 + - `index.html` - Landing page with handle search and atmosphere visualization 22 + - `view.html` - Main visualization page showing PDS data, MST visualization 23 + - `public/` - Static assets (favicon, OG image, OAuth metadata) 24 + - `public/oauth-client-metadata.json` - OAuth client configuration for public clients 25 + 26 + ## Critical reminders 27 + 28 + - Never deploy without explicit user request
-3650
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 = "actix-codec" 7 - version = "0.5.2" 8 - source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 - dependencies = [ 11 - "bitflags", 12 - "bytes", 13 - "futures-core", 14 - "futures-sink", 15 - "memchr", 16 - "pin-project-lite", 17 - "tokio", 18 - "tokio-util", 19 - "tracing", 20 - ] 21 - 22 - [[package]] 23 - name = "actix-files" 24 - version = "0.6.8" 25 - source = "registry+https://github.com/rust-lang/crates.io-index" 26 - checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576" 27 - dependencies = [ 28 - "actix-http", 29 - "actix-service", 30 - "actix-utils", 31 - "actix-web", 32 - "bitflags", 33 - "bytes", 34 - "derive_more 2.0.1", 35 - "futures-core", 36 - "http-range", 37 - "log", 38 - "mime", 39 - "mime_guess", 40 - "percent-encoding", 41 - "pin-project-lite", 42 - "v_htmlescape", 43 - ] 44 - 45 - [[package]] 46 - name = "actix-http" 47 - version = "3.11.2" 48 - source = "registry+https://github.com/rust-lang/crates.io-index" 49 - checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" 50 - dependencies = [ 51 - "actix-codec", 52 - "actix-rt", 53 - "actix-service", 54 - "actix-utils", 55 - "base64 0.22.1", 56 - "bitflags", 57 - "brotli", 58 - "bytes", 59 - "bytestring", 60 - "derive_more 2.0.1", 61 - "encoding_rs", 62 - "flate2", 63 - "foldhash", 64 - "futures-core", 65 - "h2", 66 - "http 0.2.12", 67 - "httparse", 68 - "httpdate", 69 - "itoa", 70 - "language-tags", 71 - "local-channel", 72 - "mime", 73 - "percent-encoding", 74 - "pin-project-lite", 75 - "rand 0.9.2", 76 - "sha1", 77 - "smallvec", 78 - "tokio", 79 - "tokio-util", 80 - "tracing", 81 - "zstd", 82 - ] 83 - 84 - [[package]] 85 - name = "actix-macros" 86 - version = "0.2.4" 87 - source = "registry+https://github.com/rust-lang/crates.io-index" 88 - checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 89 - dependencies = [ 90 - "quote", 91 - "syn 2.0.106", 92 - ] 93 - 94 - [[package]] 95 - name = "actix-router" 96 - version = "0.5.3" 97 - source = "registry+https://github.com/rust-lang/crates.io-index" 98 - checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" 99 - dependencies = [ 100 - "bytestring", 101 - "cfg-if", 102 - "http 0.2.12", 103 - "regex", 104 - "regex-lite", 105 - "serde", 106 - "tracing", 107 - ] 108 - 109 - [[package]] 110 - name = "actix-rt" 111 - version = "2.11.0" 112 - source = "registry+https://github.com/rust-lang/crates.io-index" 113 - checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" 114 - dependencies = [ 115 - "futures-core", 116 - "tokio", 117 - ] 118 - 119 - [[package]] 120 - name = "actix-server" 121 - version = "2.6.0" 122 - source = "registry+https://github.com/rust-lang/crates.io-index" 123 - checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" 124 - dependencies = [ 125 - "actix-rt", 126 - "actix-service", 127 - "actix-utils", 128 - "futures-core", 129 - "futures-util", 130 - "mio", 131 - "socket2 0.5.10", 132 - "tokio", 133 - "tracing", 134 - ] 135 - 136 - [[package]] 137 - name = "actix-service" 138 - version = "2.0.3" 139 - source = "registry+https://github.com/rust-lang/crates.io-index" 140 - checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" 141 - dependencies = [ 142 - "futures-core", 143 - "pin-project-lite", 144 - ] 145 - 146 - [[package]] 147 - name = "actix-session" 148 - version = "0.10.1" 149 - source = "registry+https://github.com/rust-lang/crates.io-index" 150 - checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f" 151 - dependencies = [ 152 - "actix-service", 153 - "actix-utils", 154 - "actix-web", 155 - "anyhow", 156 - "derive_more 1.0.0", 157 - "rand 0.8.5", 158 - "serde", 159 - "serde_json", 160 - "tracing", 161 - ] 162 - 163 - [[package]] 164 - name = "actix-utils" 165 - version = "3.0.1" 166 - source = "registry+https://github.com/rust-lang/crates.io-index" 167 - checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 168 - dependencies = [ 169 - "local-waker", 170 - "pin-project-lite", 171 - ] 172 - 173 - [[package]] 174 - name = "actix-web" 175 - version = "4.11.0" 176 - source = "registry+https://github.com/rust-lang/crates.io-index" 177 - checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" 178 - dependencies = [ 179 - "actix-codec", 180 - "actix-http", 181 - "actix-macros", 182 - "actix-router", 183 - "actix-rt", 184 - "actix-server", 185 - "actix-service", 186 - "actix-utils", 187 - "actix-web-codegen", 188 - "bytes", 189 - "bytestring", 190 - "cfg-if", 191 - "cookie", 192 - "derive_more 2.0.1", 193 - "encoding_rs", 194 - "foldhash", 195 - "futures-core", 196 - "futures-util", 197 - "impl-more", 198 - "itoa", 199 - "language-tags", 200 - "log", 201 - "mime", 202 - "once_cell", 203 - "pin-project-lite", 204 - "regex", 205 - "regex-lite", 206 - "serde", 207 - "serde_json", 208 - "serde_urlencoded", 209 - "smallvec", 210 - "socket2 0.5.10", 211 - "time", 212 - "tracing", 213 - "url", 214 - ] 215 - 216 - [[package]] 217 - name = "actix-web-codegen" 218 - version = "4.3.0" 219 - source = "registry+https://github.com/rust-lang/crates.io-index" 220 - checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" 221 - dependencies = [ 222 - "actix-router", 223 - "proc-macro2", 224 - "quote", 225 - "syn 2.0.106", 226 - ] 227 - 228 - [[package]] 229 - name = "addr2line" 230 - version = "0.25.1" 231 - source = "registry+https://github.com/rust-lang/crates.io-index" 232 - checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 233 - dependencies = [ 234 - "gimli", 235 - ] 236 - 237 - [[package]] 238 - name = "adler2" 239 - version = "2.0.1" 240 - source = "registry+https://github.com/rust-lang/crates.io-index" 241 - checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 242 - 243 - [[package]] 244 - name = "aead" 245 - version = "0.5.2" 246 - source = "registry+https://github.com/rust-lang/crates.io-index" 247 - checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 248 - dependencies = [ 249 - "crypto-common", 250 - "generic-array", 251 - ] 252 - 253 - [[package]] 254 - name = "aes" 255 - version = "0.8.4" 256 - source = "registry+https://github.com/rust-lang/crates.io-index" 257 - checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 258 - dependencies = [ 259 - "cfg-if", 260 - "cipher", 261 - "cpufeatures", 262 - ] 263 - 264 - [[package]] 265 - name = "aes-gcm" 266 - version = "0.10.3" 267 - source = "registry+https://github.com/rust-lang/crates.io-index" 268 - checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 269 - dependencies = [ 270 - "aead", 271 - "aes", 272 - "cipher", 273 - "ctr", 274 - "ghash", 275 - "subtle", 276 - ] 277 - 278 - [[package]] 279 - name = "aho-corasick" 280 - version = "1.1.3" 281 - source = "registry+https://github.com/rust-lang/crates.io-index" 282 - checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 283 - dependencies = [ 284 - "memchr", 285 - ] 286 - 287 - [[package]] 288 - name = "alloc-no-stdlib" 289 - version = "2.0.4" 290 - source = "registry+https://github.com/rust-lang/crates.io-index" 291 - checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 292 - 293 - [[package]] 294 - name = "alloc-stdlib" 295 - version = "0.2.2" 296 - source = "registry+https://github.com/rust-lang/crates.io-index" 297 - checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 298 - dependencies = [ 299 - "alloc-no-stdlib", 300 - ] 301 - 302 - [[package]] 303 - name = "allocator-api2" 304 - version = "0.2.21" 305 - source = "registry+https://github.com/rust-lang/crates.io-index" 306 - checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 307 - 308 - [[package]] 309 - name = "android_system_properties" 310 - version = "0.1.5" 311 - source = "registry+https://github.com/rust-lang/crates.io-index" 312 - checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 313 - dependencies = [ 314 - "libc", 315 - ] 316 - 317 - [[package]] 318 - name = "anstream" 319 - version = "0.6.21" 320 - source = "registry+https://github.com/rust-lang/crates.io-index" 321 - checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 322 - dependencies = [ 323 - "anstyle", 324 - "anstyle-parse", 325 - "anstyle-query", 326 - "anstyle-wincon", 327 - "colorchoice", 328 - "is_terminal_polyfill", 329 - "utf8parse", 330 - ] 331 - 332 - [[package]] 333 - name = "anstyle" 334 - version = "1.0.13" 335 - source = "registry+https://github.com/rust-lang/crates.io-index" 336 - checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 337 - 338 - [[package]] 339 - name = "anstyle-parse" 340 - version = "0.2.7" 341 - source = "registry+https://github.com/rust-lang/crates.io-index" 342 - checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 343 - dependencies = [ 344 - "utf8parse", 345 - ] 346 - 347 - [[package]] 348 - name = "anstyle-query" 349 - version = "1.1.4" 350 - source = "registry+https://github.com/rust-lang/crates.io-index" 351 - checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 352 - dependencies = [ 353 - "windows-sys 0.60.2", 354 - ] 355 - 356 - [[package]] 357 - name = "anstyle-wincon" 358 - version = "3.0.10" 359 - source = "registry+https://github.com/rust-lang/crates.io-index" 360 - checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 361 - dependencies = [ 362 - "anstyle", 363 - "once_cell_polyfill", 364 - "windows-sys 0.60.2", 365 - ] 366 - 367 - [[package]] 368 - name = "anyhow" 369 - version = "1.0.100" 370 - source = "registry+https://github.com/rust-lang/crates.io-index" 371 - checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 372 - 373 - [[package]] 374 - name = "async-compression" 375 - version = "0.4.32" 376 - source = "registry+https://github.com/rust-lang/crates.io-index" 377 - checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" 378 - dependencies = [ 379 - "compression-codecs", 380 - "compression-core", 381 - "futures-core", 382 - "pin-project-lite", 383 - "tokio", 384 - ] 385 - 386 - [[package]] 387 - name = "async-lock" 388 - version = "3.4.1" 389 - source = "registry+https://github.com/rust-lang/crates.io-index" 390 - checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 391 - dependencies = [ 392 - "event-listener", 393 - "event-listener-strategy", 394 - "pin-project-lite", 395 - ] 396 - 397 - [[package]] 398 - name = "async-trait" 399 - version = "0.1.89" 400 - source = "registry+https://github.com/rust-lang/crates.io-index" 401 - checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 402 - dependencies = [ 403 - "proc-macro2", 404 - "quote", 405 - "syn 2.0.106", 406 - ] 407 - 408 - [[package]] 409 - name = "at-me" 410 - version = "0.1.0" 411 - dependencies = [ 412 - "actix-files", 413 - "actix-session", 414 - "actix-web", 415 - "atrium-api", 416 - "atrium-common", 417 - "atrium-identity", 418 - "atrium-oauth", 419 - "env_logger", 420 - "hickory-resolver", 421 - "log", 422 - "serde", 423 - "serde_json", 424 - "tokio", 425 - ] 426 - 427 - [[package]] 428 - name = "atomic-waker" 429 - version = "1.1.2" 430 - source = "registry+https://github.com/rust-lang/crates.io-index" 431 - checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 432 - 433 - [[package]] 434 - name = "atrium-api" 435 - version = "0.25.6" 436 - source = "registry+https://github.com/rust-lang/crates.io-index" 437 - checksum = "ef9d5e9352fd27d99383ae1db2b6a6aa239e683a7e750e8d73a73996d82b1fd2" 438 - dependencies = [ 439 - "atrium-common", 440 - "atrium-xrpc", 441 - "chrono", 442 - "http 1.3.1", 443 - "ipld-core", 444 - "langtag", 445 - "regex", 446 - "serde", 447 - "serde_bytes", 448 - "serde_json", 449 - "thiserror", 450 - "tokio", 451 - "trait-variant", 452 - ] 453 - 454 - [[package]] 455 - name = "atrium-common" 456 - version = "0.1.2" 457 - source = "registry+https://github.com/rust-lang/crates.io-index" 458 - checksum = "9ed5610654043faa396a5a15afac0ac646d76aebe45aebd7cef4f8b96b0ab7f4" 459 - dependencies = [ 460 - "dashmap", 461 - "lru", 462 - "moka", 463 - "thiserror", 464 - "tokio", 465 - "trait-variant", 466 - "web-time", 467 - ] 468 - 469 - [[package]] 470 - name = "atrium-identity" 471 - version = "0.1.7" 472 - source = "registry+https://github.com/rust-lang/crates.io-index" 473 - checksum = "4d3a56cd2bb695308cb078be80a46a7a2caf79203eda27803f13ee6a38b98378" 474 - dependencies = [ 475 - "atrium-api", 476 - "atrium-common", 477 - "atrium-xrpc", 478 - "serde", 479 - "serde_html_form", 480 - "serde_json", 481 - "thiserror", 482 - "trait-variant", 483 - ] 484 - 485 - [[package]] 486 - name = "atrium-oauth" 487 - version = "0.1.5" 488 - source = "registry+https://github.com/rust-lang/crates.io-index" 489 - checksum = "6969f29ff0a4100d05d3988f012504385ff1d7c9db82410e26830ded8da621fb" 490 - dependencies = [ 491 - "atrium-api", 492 - "atrium-common", 493 - "atrium-identity", 494 - "atrium-xrpc", 495 - "base64 0.22.1", 496 - "chrono", 497 - "dashmap", 498 - "ecdsa", 499 - "elliptic-curve", 500 - "jose-jwa", 501 - "jose-jwk", 502 - "p256", 503 - "rand 0.8.5", 504 - "reqwest", 505 - "serde", 506 - "serde_html_form", 507 - "serde_json", 508 - "sha2", 509 - "thiserror", 510 - "tokio", 511 - "trait-variant", 512 - ] 513 - 514 - [[package]] 515 - name = "atrium-xrpc" 516 - version = "0.12.3" 517 - source = "registry+https://github.com/rust-lang/crates.io-index" 518 - checksum = "0216ad50ce34e9ff982e171c3659e65dedaa2ed5ac2994524debdc9a9647ffa8" 519 - dependencies = [ 520 - "http 1.3.1", 521 - "serde", 522 - "serde_html_form", 523 - "serde_json", 524 - "thiserror", 525 - "trait-variant", 526 - ] 527 - 528 - [[package]] 529 - name = "autocfg" 530 - version = "1.5.0" 531 - source = "registry+https://github.com/rust-lang/crates.io-index" 532 - checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 533 - 534 - [[package]] 535 - name = "backtrace" 536 - version = "0.3.76" 537 - source = "registry+https://github.com/rust-lang/crates.io-index" 538 - checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 539 - dependencies = [ 540 - "addr2line", 541 - "cfg-if", 542 - "libc", 543 - "miniz_oxide", 544 - "object", 545 - "rustc-demangle", 546 - "windows-link", 547 - ] 548 - 549 - [[package]] 550 - name = "base-x" 551 - version = "0.2.11" 552 - source = "registry+https://github.com/rust-lang/crates.io-index" 553 - checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 554 - 555 - [[package]] 556 - name = "base16ct" 557 - version = "0.2.0" 558 - source = "registry+https://github.com/rust-lang/crates.io-index" 559 - checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 560 - 561 - [[package]] 562 - name = "base256emoji" 563 - version = "1.0.2" 564 - source = "registry+https://github.com/rust-lang/crates.io-index" 565 - checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 566 - dependencies = [ 567 - "const-str", 568 - "match-lookup", 569 - ] 570 - 571 - [[package]] 572 - name = "base64" 573 - version = "0.20.0" 574 - source = "registry+https://github.com/rust-lang/crates.io-index" 575 - checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" 576 - 577 - [[package]] 578 - name = "base64" 579 - version = "0.22.1" 580 - source = "registry+https://github.com/rust-lang/crates.io-index" 581 - checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 582 - 583 - [[package]] 584 - name = "base64ct" 585 - version = "1.8.0" 586 - source = "registry+https://github.com/rust-lang/crates.io-index" 587 - checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 588 - 589 - [[package]] 590 - name = "bitflags" 591 - version = "2.9.4" 592 - source = "registry+https://github.com/rust-lang/crates.io-index" 593 - checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 594 - 595 - [[package]] 596 - name = "block-buffer" 597 - version = "0.10.4" 598 - source = "registry+https://github.com/rust-lang/crates.io-index" 599 - checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 600 - dependencies = [ 601 - "generic-array", 602 - ] 603 - 604 - [[package]] 605 - name = "brotli" 606 - version = "8.0.2" 607 - source = "registry+https://github.com/rust-lang/crates.io-index" 608 - checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" 609 - dependencies = [ 610 - "alloc-no-stdlib", 611 - "alloc-stdlib", 612 - "brotli-decompressor", 613 - ] 614 - 615 - [[package]] 616 - name = "brotli-decompressor" 617 - version = "5.0.0" 618 - source = "registry+https://github.com/rust-lang/crates.io-index" 619 - checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 620 - dependencies = [ 621 - "alloc-no-stdlib", 622 - "alloc-stdlib", 623 - ] 624 - 625 - [[package]] 626 - name = "bumpalo" 627 - version = "3.19.0" 628 - source = "registry+https://github.com/rust-lang/crates.io-index" 629 - checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 630 - 631 - [[package]] 632 - name = "bytes" 633 - version = "1.10.1" 634 - source = "registry+https://github.com/rust-lang/crates.io-index" 635 - checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 636 - 637 - [[package]] 638 - name = "bytestring" 639 - version = "1.5.0" 640 - source = "registry+https://github.com/rust-lang/crates.io-index" 641 - checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" 642 - dependencies = [ 643 - "bytes", 644 - ] 645 - 646 - [[package]] 647 - name = "cc" 648 - version = "1.2.40" 649 - source = "registry+https://github.com/rust-lang/crates.io-index" 650 - checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" 651 - dependencies = [ 652 - "find-msvc-tools", 653 - "jobserver", 654 - "libc", 655 - "shlex", 656 - ] 657 - 658 - [[package]] 659 - name = "cfg-if" 660 - version = "1.0.3" 661 - source = "registry+https://github.com/rust-lang/crates.io-index" 662 - checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 663 - 664 - [[package]] 665 - name = "chrono" 666 - version = "0.4.42" 667 - source = "registry+https://github.com/rust-lang/crates.io-index" 668 - checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 669 - dependencies = [ 670 - "iana-time-zone", 671 - "js-sys", 672 - "num-traits", 673 - "serde", 674 - "wasm-bindgen", 675 - "windows-link", 676 - ] 677 - 678 - [[package]] 679 - name = "cid" 680 - version = "0.11.1" 681 - source = "registry+https://github.com/rust-lang/crates.io-index" 682 - checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 683 - dependencies = [ 684 - "core2", 685 - "multibase", 686 - "multihash", 687 - "serde", 688 - "serde_bytes", 689 - "unsigned-varint", 690 - ] 691 - 692 - [[package]] 693 - name = "cipher" 694 - version = "0.4.4" 695 - source = "registry+https://github.com/rust-lang/crates.io-index" 696 - checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 697 - dependencies = [ 698 - "crypto-common", 699 - "inout", 700 - ] 701 - 702 - [[package]] 703 - name = "colorchoice" 704 - version = "1.0.4" 705 - source = "registry+https://github.com/rust-lang/crates.io-index" 706 - checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 707 - 708 - [[package]] 709 - name = "compression-codecs" 710 - version = "0.4.31" 711 - source = "registry+https://github.com/rust-lang/crates.io-index" 712 - checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" 713 - dependencies = [ 714 - "compression-core", 715 - "flate2", 716 - "memchr", 717 - ] 718 - 719 - [[package]] 720 - name = "compression-core" 721 - version = "0.4.29" 722 - source = "registry+https://github.com/rust-lang/crates.io-index" 723 - checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 724 - 725 - [[package]] 726 - name = "concurrent-queue" 727 - version = "2.5.0" 728 - source = "registry+https://github.com/rust-lang/crates.io-index" 729 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 730 - dependencies = [ 731 - "crossbeam-utils", 732 - ] 733 - 734 - [[package]] 735 - name = "const-oid" 736 - version = "0.9.6" 737 - source = "registry+https://github.com/rust-lang/crates.io-index" 738 - checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 739 - 740 - [[package]] 741 - name = "const-str" 742 - version = "0.4.3" 743 - source = "registry+https://github.com/rust-lang/crates.io-index" 744 - checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 745 - 746 - [[package]] 747 - name = "cookie" 748 - version = "0.16.2" 749 - source = "registry+https://github.com/rust-lang/crates.io-index" 750 - checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 751 - dependencies = [ 752 - "aes-gcm", 753 - "base64 0.20.0", 754 - "hkdf", 755 - "hmac", 756 - "percent-encoding", 757 - "rand 0.8.5", 758 - "sha2", 759 - "subtle", 760 - "time", 761 - "version_check", 762 - ] 763 - 764 - [[package]] 765 - name = "core-foundation" 766 - version = "0.9.4" 767 - source = "registry+https://github.com/rust-lang/crates.io-index" 768 - checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 769 - dependencies = [ 770 - "core-foundation-sys", 771 - "libc", 772 - ] 773 - 774 - [[package]] 775 - name = "core-foundation-sys" 776 - version = "0.8.7" 777 - source = "registry+https://github.com/rust-lang/crates.io-index" 778 - checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 779 - 780 - [[package]] 781 - name = "core2" 782 - version = "0.4.0" 783 - source = "registry+https://github.com/rust-lang/crates.io-index" 784 - checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 785 - dependencies = [ 786 - "memchr", 787 - ] 788 - 789 - [[package]] 790 - name = "cpufeatures" 791 - version = "0.2.17" 792 - source = "registry+https://github.com/rust-lang/crates.io-index" 793 - checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 794 - dependencies = [ 795 - "libc", 796 - ] 797 - 798 - [[package]] 799 - name = "crc32fast" 800 - version = "1.5.0" 801 - source = "registry+https://github.com/rust-lang/crates.io-index" 802 - checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 803 - dependencies = [ 804 - "cfg-if", 805 - ] 806 - 807 - [[package]] 808 - name = "crossbeam-channel" 809 - version = "0.5.15" 810 - source = "registry+https://github.com/rust-lang/crates.io-index" 811 - checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 812 - dependencies = [ 813 - "crossbeam-utils", 814 - ] 815 - 816 - [[package]] 817 - name = "crossbeam-epoch" 818 - version = "0.9.18" 819 - source = "registry+https://github.com/rust-lang/crates.io-index" 820 - checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 821 - dependencies = [ 822 - "crossbeam-utils", 823 - ] 824 - 825 - [[package]] 826 - name = "crossbeam-utils" 827 - version = "0.8.21" 828 - source = "registry+https://github.com/rust-lang/crates.io-index" 829 - checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 830 - 831 - [[package]] 832 - name = "crypto-bigint" 833 - version = "0.5.5" 834 - source = "registry+https://github.com/rust-lang/crates.io-index" 835 - checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 836 - dependencies = [ 837 - "generic-array", 838 - "rand_core 0.6.4", 839 - "subtle", 840 - "zeroize", 841 - ] 842 - 843 - [[package]] 844 - name = "crypto-common" 845 - version = "0.1.6" 846 - source = "registry+https://github.com/rust-lang/crates.io-index" 847 - checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 848 - dependencies = [ 849 - "generic-array", 850 - "rand_core 0.6.4", 851 - "typenum", 852 - ] 853 - 854 - [[package]] 855 - name = "ctr" 856 - version = "0.9.2" 857 - source = "registry+https://github.com/rust-lang/crates.io-index" 858 - checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 859 - dependencies = [ 860 - "cipher", 861 - ] 862 - 863 - [[package]] 864 - name = "dashmap" 865 - version = "6.1.0" 866 - source = "registry+https://github.com/rust-lang/crates.io-index" 867 - checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 868 - dependencies = [ 869 - "cfg-if", 870 - "crossbeam-utils", 871 - "hashbrown 0.14.5", 872 - "lock_api", 873 - "once_cell", 874 - "parking_lot_core", 875 - ] 876 - 877 - [[package]] 878 - name = "data-encoding" 879 - version = "2.9.0" 880 - source = "registry+https://github.com/rust-lang/crates.io-index" 881 - checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 882 - 883 - [[package]] 884 - name = "data-encoding-macro" 885 - version = "0.1.18" 886 - source = "registry+https://github.com/rust-lang/crates.io-index" 887 - checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 888 - dependencies = [ 889 - "data-encoding", 890 - "data-encoding-macro-internal", 891 - ] 892 - 893 - [[package]] 894 - name = "data-encoding-macro-internal" 895 - version = "0.1.16" 896 - source = "registry+https://github.com/rust-lang/crates.io-index" 897 - checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 898 - dependencies = [ 899 - "data-encoding", 900 - "syn 2.0.106", 901 - ] 902 - 903 - [[package]] 904 - name = "der" 905 - version = "0.7.10" 906 - source = "registry+https://github.com/rust-lang/crates.io-index" 907 - checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 908 - dependencies = [ 909 - "const-oid", 910 - "zeroize", 911 - ] 912 - 913 - [[package]] 914 - name = "deranged" 915 - version = "0.5.4" 916 - source = "registry+https://github.com/rust-lang/crates.io-index" 917 - checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" 918 - dependencies = [ 919 - "powerfmt", 920 - ] 921 - 922 - [[package]] 923 - name = "derive_more" 924 - version = "1.0.0" 925 - source = "registry+https://github.com/rust-lang/crates.io-index" 926 - checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 927 - dependencies = [ 928 - "derive_more-impl 1.0.0", 929 - ] 930 - 931 - [[package]] 932 - name = "derive_more" 933 - version = "2.0.1" 934 - source = "registry+https://github.com/rust-lang/crates.io-index" 935 - checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 936 - dependencies = [ 937 - "derive_more-impl 2.0.1", 938 - ] 939 - 940 - [[package]] 941 - name = "derive_more-impl" 942 - version = "1.0.0" 943 - source = "registry+https://github.com/rust-lang/crates.io-index" 944 - checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 945 - dependencies = [ 946 - "proc-macro2", 947 - "quote", 948 - "syn 2.0.106", 949 - "unicode-xid", 950 - ] 951 - 952 - [[package]] 953 - name = "derive_more-impl" 954 - version = "2.0.1" 955 - source = "registry+https://github.com/rust-lang/crates.io-index" 956 - checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 957 - dependencies = [ 958 - "proc-macro2", 959 - "quote", 960 - "syn 2.0.106", 961 - "unicode-xid", 962 - ] 963 - 964 - [[package]] 965 - name = "digest" 966 - version = "0.10.7" 967 - source = "registry+https://github.com/rust-lang/crates.io-index" 968 - checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 969 - dependencies = [ 970 - "block-buffer", 971 - "const-oid", 972 - "crypto-common", 973 - "subtle", 974 - ] 975 - 976 - [[package]] 977 - name = "displaydoc" 978 - version = "0.2.5" 979 - source = "registry+https://github.com/rust-lang/crates.io-index" 980 - checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 981 - dependencies = [ 982 - "proc-macro2", 983 - "quote", 984 - "syn 2.0.106", 985 - ] 986 - 987 - [[package]] 988 - name = "ecdsa" 989 - version = "0.16.9" 990 - source = "registry+https://github.com/rust-lang/crates.io-index" 991 - checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 992 - dependencies = [ 993 - "der", 994 - "digest", 995 - "elliptic-curve", 996 - "rfc6979", 997 - "signature", 998 - ] 999 - 1000 - [[package]] 1001 - name = "elliptic-curve" 1002 - version = "0.13.8" 1003 - source = "registry+https://github.com/rust-lang/crates.io-index" 1004 - checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 1005 - dependencies = [ 1006 - "base16ct", 1007 - "crypto-bigint", 1008 - "digest", 1009 - "ff", 1010 - "generic-array", 1011 - "group", 1012 - "rand_core 0.6.4", 1013 - "sec1", 1014 - "subtle", 1015 - "zeroize", 1016 - ] 1017 - 1018 - [[package]] 1019 - name = "encoding_rs" 1020 - version = "0.8.35" 1021 - source = "registry+https://github.com/rust-lang/crates.io-index" 1022 - checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 1023 - dependencies = [ 1024 - "cfg-if", 1025 - ] 1026 - 1027 - [[package]] 1028 - name = "enum-as-inner" 1029 - version = "0.6.1" 1030 - source = "registry+https://github.com/rust-lang/crates.io-index" 1031 - checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 1032 - dependencies = [ 1033 - "heck", 1034 - "proc-macro2", 1035 - "quote", 1036 - "syn 2.0.106", 1037 - ] 1038 - 1039 - [[package]] 1040 - name = "env_filter" 1041 - version = "0.1.3" 1042 - source = "registry+https://github.com/rust-lang/crates.io-index" 1043 - checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 1044 - dependencies = [ 1045 - "log", 1046 - "regex", 1047 - ] 1048 - 1049 - [[package]] 1050 - name = "env_logger" 1051 - version = "0.11.8" 1052 - source = "registry+https://github.com/rust-lang/crates.io-index" 1053 - checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 1054 - dependencies = [ 1055 - "anstream", 1056 - "anstyle", 1057 - "env_filter", 1058 - "jiff", 1059 - "log", 1060 - ] 1061 - 1062 - [[package]] 1063 - name = "equivalent" 1064 - version = "1.0.2" 1065 - source = "registry+https://github.com/rust-lang/crates.io-index" 1066 - checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 1067 - 1068 - [[package]] 1069 - name = "errno" 1070 - version = "0.3.14" 1071 - source = "registry+https://github.com/rust-lang/crates.io-index" 1072 - checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 1073 - dependencies = [ 1074 - "libc", 1075 - "windows-sys 0.61.1", 1076 - ] 1077 - 1078 - [[package]] 1079 - name = "event-listener" 1080 - version = "5.4.1" 1081 - source = "registry+https://github.com/rust-lang/crates.io-index" 1082 - checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 1083 - dependencies = [ 1084 - "concurrent-queue", 1085 - "parking", 1086 - "pin-project-lite", 1087 - ] 1088 - 1089 - [[package]] 1090 - name = "event-listener-strategy" 1091 - version = "0.5.4" 1092 - source = "registry+https://github.com/rust-lang/crates.io-index" 1093 - checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 1094 - dependencies = [ 1095 - "event-listener", 1096 - "pin-project-lite", 1097 - ] 1098 - 1099 - [[package]] 1100 - name = "fastrand" 1101 - version = "2.3.0" 1102 - source = "registry+https://github.com/rust-lang/crates.io-index" 1103 - checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1104 - 1105 - [[package]] 1106 - name = "ff" 1107 - version = "0.13.1" 1108 - source = "registry+https://github.com/rust-lang/crates.io-index" 1109 - checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1110 - dependencies = [ 1111 - "rand_core 0.6.4", 1112 - "subtle", 1113 - ] 1114 - 1115 - [[package]] 1116 - name = "find-msvc-tools" 1117 - version = "0.1.3" 1118 - source = "registry+https://github.com/rust-lang/crates.io-index" 1119 - checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" 1120 - 1121 - [[package]] 1122 - name = "flate2" 1123 - version = "1.1.4" 1124 - source = "registry+https://github.com/rust-lang/crates.io-index" 1125 - checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" 1126 - dependencies = [ 1127 - "crc32fast", 1128 - "miniz_oxide", 1129 - ] 1130 - 1131 - [[package]] 1132 - name = "fnv" 1133 - version = "1.0.7" 1134 - source = "registry+https://github.com/rust-lang/crates.io-index" 1135 - checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 1136 - 1137 - [[package]] 1138 - name = "foldhash" 1139 - version = "0.1.5" 1140 - source = "registry+https://github.com/rust-lang/crates.io-index" 1141 - checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1142 - 1143 - [[package]] 1144 - name = "foreign-types" 1145 - version = "0.3.2" 1146 - source = "registry+https://github.com/rust-lang/crates.io-index" 1147 - checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 1148 - dependencies = [ 1149 - "foreign-types-shared", 1150 - ] 1151 - 1152 - [[package]] 1153 - name = "foreign-types-shared" 1154 - version = "0.1.1" 1155 - source = "registry+https://github.com/rust-lang/crates.io-index" 1156 - checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 1157 - 1158 - [[package]] 1159 - name = "form_urlencoded" 1160 - version = "1.2.2" 1161 - source = "registry+https://github.com/rust-lang/crates.io-index" 1162 - checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 1163 - dependencies = [ 1164 - "percent-encoding", 1165 - ] 1166 - 1167 - [[package]] 1168 - name = "futures-channel" 1169 - version = "0.3.31" 1170 - source = "registry+https://github.com/rust-lang/crates.io-index" 1171 - checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 1172 - dependencies = [ 1173 - "futures-core", 1174 - ] 1175 - 1176 - [[package]] 1177 - name = "futures-core" 1178 - version = "0.3.31" 1179 - source = "registry+https://github.com/rust-lang/crates.io-index" 1180 - checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 1181 - 1182 - [[package]] 1183 - name = "futures-io" 1184 - version = "0.3.31" 1185 - source = "registry+https://github.com/rust-lang/crates.io-index" 1186 - checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1187 - 1188 - [[package]] 1189 - name = "futures-macro" 1190 - version = "0.3.31" 1191 - source = "registry+https://github.com/rust-lang/crates.io-index" 1192 - checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 1193 - dependencies = [ 1194 - "proc-macro2", 1195 - "quote", 1196 - "syn 2.0.106", 1197 - ] 1198 - 1199 - [[package]] 1200 - name = "futures-sink" 1201 - version = "0.3.31" 1202 - source = "registry+https://github.com/rust-lang/crates.io-index" 1203 - checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 1204 - 1205 - [[package]] 1206 - name = "futures-task" 1207 - version = "0.3.31" 1208 - source = "registry+https://github.com/rust-lang/crates.io-index" 1209 - checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 1210 - 1211 - [[package]] 1212 - name = "futures-util" 1213 - version = "0.3.31" 1214 - source = "registry+https://github.com/rust-lang/crates.io-index" 1215 - checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 1216 - dependencies = [ 1217 - "futures-core", 1218 - "futures-macro", 1219 - "futures-task", 1220 - "pin-project-lite", 1221 - "pin-utils", 1222 - "slab", 1223 - ] 1224 - 1225 - [[package]] 1226 - name = "generic-array" 1227 - version = "0.14.7" 1228 - source = "registry+https://github.com/rust-lang/crates.io-index" 1229 - checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 1230 - dependencies = [ 1231 - "typenum", 1232 - "version_check", 1233 - "zeroize", 1234 - ] 1235 - 1236 - [[package]] 1237 - name = "getrandom" 1238 - version = "0.2.16" 1239 - source = "registry+https://github.com/rust-lang/crates.io-index" 1240 - checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1241 - dependencies = [ 1242 - "cfg-if", 1243 - "libc", 1244 - "wasi 0.11.1+wasi-snapshot-preview1", 1245 - ] 1246 - 1247 - [[package]] 1248 - name = "getrandom" 1249 - version = "0.3.3" 1250 - source = "registry+https://github.com/rust-lang/crates.io-index" 1251 - checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1252 - dependencies = [ 1253 - "cfg-if", 1254 - "libc", 1255 - "r-efi", 1256 - "wasi 0.14.7+wasi-0.2.4", 1257 - ] 1258 - 1259 - [[package]] 1260 - name = "ghash" 1261 - version = "0.5.1" 1262 - source = "registry+https://github.com/rust-lang/crates.io-index" 1263 - checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" 1264 - dependencies = [ 1265 - "opaque-debug", 1266 - "polyval", 1267 - ] 1268 - 1269 - [[package]] 1270 - name = "gimli" 1271 - version = "0.32.3" 1272 - source = "registry+https://github.com/rust-lang/crates.io-index" 1273 - checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 1274 - 1275 - [[package]] 1276 - name = "group" 1277 - version = "0.13.0" 1278 - source = "registry+https://github.com/rust-lang/crates.io-index" 1279 - checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1280 - dependencies = [ 1281 - "ff", 1282 - "rand_core 0.6.4", 1283 - "subtle", 1284 - ] 1285 - 1286 - [[package]] 1287 - name = "h2" 1288 - version = "0.3.27" 1289 - source = "registry+https://github.com/rust-lang/crates.io-index" 1290 - checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" 1291 - dependencies = [ 1292 - "bytes", 1293 - "fnv", 1294 - "futures-core", 1295 - "futures-sink", 1296 - "futures-util", 1297 - "http 0.2.12", 1298 - "indexmap", 1299 - "slab", 1300 - "tokio", 1301 - "tokio-util", 1302 - "tracing", 1303 - ] 1304 - 1305 - [[package]] 1306 - name = "hashbrown" 1307 - version = "0.14.5" 1308 - source = "registry+https://github.com/rust-lang/crates.io-index" 1309 - checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 1310 - 1311 - [[package]] 1312 - name = "hashbrown" 1313 - version = "0.15.5" 1314 - source = "registry+https://github.com/rust-lang/crates.io-index" 1315 - checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1316 - dependencies = [ 1317 - "allocator-api2", 1318 - "equivalent", 1319 - "foldhash", 1320 - ] 1321 - 1322 - [[package]] 1323 - name = "hashbrown" 1324 - version = "0.16.0" 1325 - source = "registry+https://github.com/rust-lang/crates.io-index" 1326 - checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 1327 - 1328 - [[package]] 1329 - name = "heck" 1330 - version = "0.5.0" 1331 - source = "registry+https://github.com/rust-lang/crates.io-index" 1332 - checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1333 - 1334 - [[package]] 1335 - name = "hickory-proto" 1336 - version = "0.24.4" 1337 - source = "registry+https://github.com/rust-lang/crates.io-index" 1338 - checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" 1339 - dependencies = [ 1340 - "async-trait", 1341 - "cfg-if", 1342 - "data-encoding", 1343 - "enum-as-inner", 1344 - "futures-channel", 1345 - "futures-io", 1346 - "futures-util", 1347 - "idna", 1348 - "ipnet", 1349 - "once_cell", 1350 - "rand 0.8.5", 1351 - "thiserror", 1352 - "tinyvec", 1353 - "tokio", 1354 - "tracing", 1355 - "url", 1356 - ] 1357 - 1358 - [[package]] 1359 - name = "hickory-resolver" 1360 - version = "0.24.4" 1361 - source = "registry+https://github.com/rust-lang/crates.io-index" 1362 - checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" 1363 - dependencies = [ 1364 - "cfg-if", 1365 - "futures-util", 1366 - "hickory-proto", 1367 - "ipconfig", 1368 - "lru-cache", 1369 - "once_cell", 1370 - "parking_lot", 1371 - "rand 0.8.5", 1372 - "resolv-conf", 1373 - "smallvec", 1374 - "thiserror", 1375 - "tokio", 1376 - "tracing", 1377 - ] 1378 - 1379 - [[package]] 1380 - name = "hkdf" 1381 - version = "0.12.4" 1382 - source = "registry+https://github.com/rust-lang/crates.io-index" 1383 - checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1384 - dependencies = [ 1385 - "hmac", 1386 - ] 1387 - 1388 - [[package]] 1389 - name = "hmac" 1390 - version = "0.12.1" 1391 - source = "registry+https://github.com/rust-lang/crates.io-index" 1392 - checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1393 - dependencies = [ 1394 - "digest", 1395 - ] 1396 - 1397 - [[package]] 1398 - name = "http" 1399 - version = "0.2.12" 1400 - source = "registry+https://github.com/rust-lang/crates.io-index" 1401 - checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 1402 - dependencies = [ 1403 - "bytes", 1404 - "fnv", 1405 - "itoa", 1406 - ] 1407 - 1408 - [[package]] 1409 - name = "http" 1410 - version = "1.3.1" 1411 - source = "registry+https://github.com/rust-lang/crates.io-index" 1412 - checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1413 - dependencies = [ 1414 - "bytes", 1415 - "fnv", 1416 - "itoa", 1417 - ] 1418 - 1419 - [[package]] 1420 - name = "http-body" 1421 - version = "1.0.1" 1422 - source = "registry+https://github.com/rust-lang/crates.io-index" 1423 - checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1424 - dependencies = [ 1425 - "bytes", 1426 - "http 1.3.1", 1427 - ] 1428 - 1429 - [[package]] 1430 - name = "http-body-util" 1431 - version = "0.1.3" 1432 - source = "registry+https://github.com/rust-lang/crates.io-index" 1433 - checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1434 - dependencies = [ 1435 - "bytes", 1436 - "futures-core", 1437 - "http 1.3.1", 1438 - "http-body", 1439 - "pin-project-lite", 1440 - ] 1441 - 1442 - [[package]] 1443 - name = "http-range" 1444 - version = "0.1.5" 1445 - source = "registry+https://github.com/rust-lang/crates.io-index" 1446 - checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" 1447 - 1448 - [[package]] 1449 - name = "httparse" 1450 - version = "1.10.1" 1451 - source = "registry+https://github.com/rust-lang/crates.io-index" 1452 - checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1453 - 1454 - [[package]] 1455 - name = "httpdate" 1456 - version = "1.0.3" 1457 - source = "registry+https://github.com/rust-lang/crates.io-index" 1458 - checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1459 - 1460 - [[package]] 1461 - name = "hyper" 1462 - version = "1.7.0" 1463 - source = "registry+https://github.com/rust-lang/crates.io-index" 1464 - checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 1465 - dependencies = [ 1466 - "atomic-waker", 1467 - "bytes", 1468 - "futures-channel", 1469 - "futures-core", 1470 - "http 1.3.1", 1471 - "http-body", 1472 - "httparse", 1473 - "itoa", 1474 - "pin-project-lite", 1475 - "pin-utils", 1476 - "smallvec", 1477 - "tokio", 1478 - "want", 1479 - ] 1480 - 1481 - [[package]] 1482 - name = "hyper-tls" 1483 - version = "0.6.0" 1484 - source = "registry+https://github.com/rust-lang/crates.io-index" 1485 - checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1486 - dependencies = [ 1487 - "bytes", 1488 - "http-body-util", 1489 - "hyper", 1490 - "hyper-util", 1491 - "native-tls", 1492 - "tokio", 1493 - "tokio-native-tls", 1494 - "tower-service", 1495 - ] 1496 - 1497 - [[package]] 1498 - name = "hyper-util" 1499 - version = "0.1.17" 1500 - source = "registry+https://github.com/rust-lang/crates.io-index" 1501 - checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 1502 - dependencies = [ 1503 - "base64 0.22.1", 1504 - "bytes", 1505 - "futures-channel", 1506 - "futures-core", 1507 - "futures-util", 1508 - "http 1.3.1", 1509 - "http-body", 1510 - "hyper", 1511 - "ipnet", 1512 - "libc", 1513 - "percent-encoding", 1514 - "pin-project-lite", 1515 - "socket2 0.6.0", 1516 - "tokio", 1517 - "tower-service", 1518 - "tracing", 1519 - ] 1520 - 1521 - [[package]] 1522 - name = "iana-time-zone" 1523 - version = "0.1.64" 1524 - source = "registry+https://github.com/rust-lang/crates.io-index" 1525 - checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 1526 - dependencies = [ 1527 - "android_system_properties", 1528 - "core-foundation-sys", 1529 - "iana-time-zone-haiku", 1530 - "js-sys", 1531 - "log", 1532 - "wasm-bindgen", 1533 - "windows-core", 1534 - ] 1535 - 1536 - [[package]] 1537 - name = "iana-time-zone-haiku" 1538 - version = "0.1.2" 1539 - source = "registry+https://github.com/rust-lang/crates.io-index" 1540 - checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1541 - dependencies = [ 1542 - "cc", 1543 - ] 1544 - 1545 - [[package]] 1546 - name = "icu_collections" 1547 - version = "2.0.0" 1548 - source = "registry+https://github.com/rust-lang/crates.io-index" 1549 - checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1550 - dependencies = [ 1551 - "displaydoc", 1552 - "potential_utf", 1553 - "yoke", 1554 - "zerofrom", 1555 - "zerovec", 1556 - ] 1557 - 1558 - [[package]] 1559 - name = "icu_locale_core" 1560 - version = "2.0.0" 1561 - source = "registry+https://github.com/rust-lang/crates.io-index" 1562 - checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1563 - dependencies = [ 1564 - "displaydoc", 1565 - "litemap", 1566 - "tinystr", 1567 - "writeable", 1568 - "zerovec", 1569 - ] 1570 - 1571 - [[package]] 1572 - name = "icu_normalizer" 1573 - version = "2.0.0" 1574 - source = "registry+https://github.com/rust-lang/crates.io-index" 1575 - checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1576 - dependencies = [ 1577 - "displaydoc", 1578 - "icu_collections", 1579 - "icu_normalizer_data", 1580 - "icu_properties", 1581 - "icu_provider", 1582 - "smallvec", 1583 - "zerovec", 1584 - ] 1585 - 1586 - [[package]] 1587 - name = "icu_normalizer_data" 1588 - version = "2.0.0" 1589 - source = "registry+https://github.com/rust-lang/crates.io-index" 1590 - checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1591 - 1592 - [[package]] 1593 - name = "icu_properties" 1594 - version = "2.0.1" 1595 - source = "registry+https://github.com/rust-lang/crates.io-index" 1596 - checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1597 - dependencies = [ 1598 - "displaydoc", 1599 - "icu_collections", 1600 - "icu_locale_core", 1601 - "icu_properties_data", 1602 - "icu_provider", 1603 - "potential_utf", 1604 - "zerotrie", 1605 - "zerovec", 1606 - ] 1607 - 1608 - [[package]] 1609 - name = "icu_properties_data" 1610 - version = "2.0.1" 1611 - source = "registry+https://github.com/rust-lang/crates.io-index" 1612 - checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1613 - 1614 - [[package]] 1615 - name = "icu_provider" 1616 - version = "2.0.0" 1617 - source = "registry+https://github.com/rust-lang/crates.io-index" 1618 - checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1619 - dependencies = [ 1620 - "displaydoc", 1621 - "icu_locale_core", 1622 - "stable_deref_trait", 1623 - "tinystr", 1624 - "writeable", 1625 - "yoke", 1626 - "zerofrom", 1627 - "zerotrie", 1628 - "zerovec", 1629 - ] 1630 - 1631 - [[package]] 1632 - name = "idna" 1633 - version = "1.1.0" 1634 - source = "registry+https://github.com/rust-lang/crates.io-index" 1635 - checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1636 - dependencies = [ 1637 - "idna_adapter", 1638 - "smallvec", 1639 - "utf8_iter", 1640 - ] 1641 - 1642 - [[package]] 1643 - name = "idna_adapter" 1644 - version = "1.2.1" 1645 - source = "registry+https://github.com/rust-lang/crates.io-index" 1646 - checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1647 - dependencies = [ 1648 - "icu_normalizer", 1649 - "icu_properties", 1650 - ] 1651 - 1652 - [[package]] 1653 - name = "impl-more" 1654 - version = "0.1.9" 1655 - source = "registry+https://github.com/rust-lang/crates.io-index" 1656 - checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" 1657 - 1658 - [[package]] 1659 - name = "indexmap" 1660 - version = "2.11.4" 1661 - source = "registry+https://github.com/rust-lang/crates.io-index" 1662 - checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 1663 - dependencies = [ 1664 - "equivalent", 1665 - "hashbrown 0.16.0", 1666 - ] 1667 - 1668 - [[package]] 1669 - name = "inout" 1670 - version = "0.1.4" 1671 - source = "registry+https://github.com/rust-lang/crates.io-index" 1672 - checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 1673 - dependencies = [ 1674 - "generic-array", 1675 - ] 1676 - 1677 - [[package]] 1678 - name = "io-uring" 1679 - version = "0.7.10" 1680 - source = "registry+https://github.com/rust-lang/crates.io-index" 1681 - checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 1682 - dependencies = [ 1683 - "bitflags", 1684 - "cfg-if", 1685 - "libc", 1686 - ] 1687 - 1688 - [[package]] 1689 - name = "ipconfig" 1690 - version = "0.3.2" 1691 - source = "registry+https://github.com/rust-lang/crates.io-index" 1692 - checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1693 - dependencies = [ 1694 - "socket2 0.5.10", 1695 - "widestring", 1696 - "windows-sys 0.48.0", 1697 - "winreg", 1698 - ] 1699 - 1700 - [[package]] 1701 - name = "ipld-core" 1702 - version = "0.4.2" 1703 - source = "registry+https://github.com/rust-lang/crates.io-index" 1704 - checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1705 - dependencies = [ 1706 - "cid", 1707 - "serde", 1708 - "serde_bytes", 1709 - ] 1710 - 1711 - [[package]] 1712 - name = "ipnet" 1713 - version = "2.11.0" 1714 - source = "registry+https://github.com/rust-lang/crates.io-index" 1715 - checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1716 - 1717 - [[package]] 1718 - name = "iri-string" 1719 - version = "0.7.8" 1720 - source = "registry+https://github.com/rust-lang/crates.io-index" 1721 - checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1722 - dependencies = [ 1723 - "memchr", 1724 - "serde", 1725 - ] 1726 - 1727 - [[package]] 1728 - name = "is_terminal_polyfill" 1729 - version = "1.70.1" 1730 - source = "registry+https://github.com/rust-lang/crates.io-index" 1731 - checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1732 - 1733 - [[package]] 1734 - name = "itoa" 1735 - version = "1.0.15" 1736 - source = "registry+https://github.com/rust-lang/crates.io-index" 1737 - checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1738 - 1739 - [[package]] 1740 - name = "jiff" 1741 - version = "0.2.15" 1742 - source = "registry+https://github.com/rust-lang/crates.io-index" 1743 - checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" 1744 - dependencies = [ 1745 - "jiff-static", 1746 - "log", 1747 - "portable-atomic", 1748 - "portable-atomic-util", 1749 - "serde", 1750 - ] 1751 - 1752 - [[package]] 1753 - name = "jiff-static" 1754 - version = "0.2.15" 1755 - source = "registry+https://github.com/rust-lang/crates.io-index" 1756 - checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" 1757 - dependencies = [ 1758 - "proc-macro2", 1759 - "quote", 1760 - "syn 2.0.106", 1761 - ] 1762 - 1763 - [[package]] 1764 - name = "jobserver" 1765 - version = "0.1.34" 1766 - source = "registry+https://github.com/rust-lang/crates.io-index" 1767 - checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1768 - dependencies = [ 1769 - "getrandom 0.3.3", 1770 - "libc", 1771 - ] 1772 - 1773 - [[package]] 1774 - name = "jose-b64" 1775 - version = "0.1.2" 1776 - source = "registry+https://github.com/rust-lang/crates.io-index" 1777 - checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" 1778 - dependencies = [ 1779 - "base64ct", 1780 - "serde", 1781 - "subtle", 1782 - "zeroize", 1783 - ] 1784 - 1785 - [[package]] 1786 - name = "jose-jwa" 1787 - version = "0.1.2" 1788 - source = "registry+https://github.com/rust-lang/crates.io-index" 1789 - checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" 1790 - dependencies = [ 1791 - "serde", 1792 - ] 1793 - 1794 - [[package]] 1795 - name = "jose-jwk" 1796 - version = "0.1.2" 1797 - source = "registry+https://github.com/rust-lang/crates.io-index" 1798 - checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" 1799 - dependencies = [ 1800 - "jose-b64", 1801 - "jose-jwa", 1802 - "p256", 1803 - "serde", 1804 - "zeroize", 1805 - ] 1806 - 1807 - [[package]] 1808 - name = "js-sys" 1809 - version = "0.3.81" 1810 - source = "registry+https://github.com/rust-lang/crates.io-index" 1811 - checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 1812 - dependencies = [ 1813 - "once_cell", 1814 - "wasm-bindgen", 1815 - ] 1816 - 1817 - [[package]] 1818 - name = "langtag" 1819 - version = "0.3.4" 1820 - source = "registry+https://github.com/rust-lang/crates.io-index" 1821 - checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805" 1822 - dependencies = [ 1823 - "serde", 1824 - ] 1825 - 1826 - [[package]] 1827 - name = "language-tags" 1828 - version = "0.3.2" 1829 - source = "registry+https://github.com/rust-lang/crates.io-index" 1830 - checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 1831 - 1832 - [[package]] 1833 - name = "libc" 1834 - version = "0.2.176" 1835 - source = "registry+https://github.com/rust-lang/crates.io-index" 1836 - checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 1837 - 1838 - [[package]] 1839 - name = "linked-hash-map" 1840 - version = "0.5.6" 1841 - source = "registry+https://github.com/rust-lang/crates.io-index" 1842 - checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 1843 - 1844 - [[package]] 1845 - name = "linux-raw-sys" 1846 - version = "0.11.0" 1847 - source = "registry+https://github.com/rust-lang/crates.io-index" 1848 - checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 1849 - 1850 - [[package]] 1851 - name = "litemap" 1852 - version = "0.8.0" 1853 - source = "registry+https://github.com/rust-lang/crates.io-index" 1854 - checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1855 - 1856 - [[package]] 1857 - name = "local-channel" 1858 - version = "0.1.5" 1859 - source = "registry+https://github.com/rust-lang/crates.io-index" 1860 - checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" 1861 - dependencies = [ 1862 - "futures-core", 1863 - "futures-sink", 1864 - "local-waker", 1865 - ] 1866 - 1867 - [[package]] 1868 - name = "local-waker" 1869 - version = "0.1.4" 1870 - source = "registry+https://github.com/rust-lang/crates.io-index" 1871 - checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" 1872 - 1873 - [[package]] 1874 - name = "lock_api" 1875 - version = "0.4.14" 1876 - source = "registry+https://github.com/rust-lang/crates.io-index" 1877 - checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1878 - dependencies = [ 1879 - "scopeguard", 1880 - ] 1881 - 1882 - [[package]] 1883 - name = "log" 1884 - version = "0.4.28" 1885 - source = "registry+https://github.com/rust-lang/crates.io-index" 1886 - checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1887 - 1888 - [[package]] 1889 - name = "lru" 1890 - version = "0.12.5" 1891 - source = "registry+https://github.com/rust-lang/crates.io-index" 1892 - checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 1893 - dependencies = [ 1894 - "hashbrown 0.15.5", 1895 - ] 1896 - 1897 - [[package]] 1898 - name = "lru-cache" 1899 - version = "0.1.2" 1900 - source = "registry+https://github.com/rust-lang/crates.io-index" 1901 - checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 1902 - dependencies = [ 1903 - "linked-hash-map", 1904 - ] 1905 - 1906 - [[package]] 1907 - name = "match-lookup" 1908 - version = "0.1.1" 1909 - source = "registry+https://github.com/rust-lang/crates.io-index" 1910 - checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 1911 - dependencies = [ 1912 - "proc-macro2", 1913 - "quote", 1914 - "syn 1.0.109", 1915 - ] 1916 - 1917 - [[package]] 1918 - name = "memchr" 1919 - version = "2.7.6" 1920 - source = "registry+https://github.com/rust-lang/crates.io-index" 1921 - checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1922 - 1923 - [[package]] 1924 - name = "mime" 1925 - version = "0.3.17" 1926 - source = "registry+https://github.com/rust-lang/crates.io-index" 1927 - checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1928 - 1929 - [[package]] 1930 - name = "mime_guess" 1931 - version = "2.0.5" 1932 - source = "registry+https://github.com/rust-lang/crates.io-index" 1933 - checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1934 - dependencies = [ 1935 - "mime", 1936 - "unicase", 1937 - ] 1938 - 1939 - [[package]] 1940 - name = "miniz_oxide" 1941 - version = "0.8.9" 1942 - source = "registry+https://github.com/rust-lang/crates.io-index" 1943 - checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1944 - dependencies = [ 1945 - "adler2", 1946 - "simd-adler32", 1947 - ] 1948 - 1949 - [[package]] 1950 - name = "mio" 1951 - version = "1.0.4" 1952 - source = "registry+https://github.com/rust-lang/crates.io-index" 1953 - checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1954 - dependencies = [ 1955 - "libc", 1956 - "log", 1957 - "wasi 0.11.1+wasi-snapshot-preview1", 1958 - "windows-sys 0.59.0", 1959 - ] 1960 - 1961 - [[package]] 1962 - name = "moka" 1963 - version = "0.12.11" 1964 - source = "registry+https://github.com/rust-lang/crates.io-index" 1965 - checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" 1966 - dependencies = [ 1967 - "async-lock", 1968 - "crossbeam-channel", 1969 - "crossbeam-epoch", 1970 - "crossbeam-utils", 1971 - "equivalent", 1972 - "event-listener", 1973 - "futures-util", 1974 - "parking_lot", 1975 - "portable-atomic", 1976 - "rustc_version", 1977 - "smallvec", 1978 - "tagptr", 1979 - "uuid", 1980 - ] 1981 - 1982 - [[package]] 1983 - name = "multibase" 1984 - version = "0.9.2" 1985 - source = "registry+https://github.com/rust-lang/crates.io-index" 1986 - checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1987 - dependencies = [ 1988 - "base-x", 1989 - "base256emoji", 1990 - "data-encoding", 1991 - "data-encoding-macro", 1992 - ] 1993 - 1994 - [[package]] 1995 - name = "multihash" 1996 - version = "0.19.3" 1997 - source = "registry+https://github.com/rust-lang/crates.io-index" 1998 - checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1999 - dependencies = [ 2000 - "core2", 2001 - "serde", 2002 - "unsigned-varint", 2003 - ] 2004 - 2005 - [[package]] 2006 - name = "native-tls" 2007 - version = "0.2.14" 2008 - source = "registry+https://github.com/rust-lang/crates.io-index" 2009 - checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 2010 - dependencies = [ 2011 - "libc", 2012 - "log", 2013 - "openssl", 2014 - "openssl-probe", 2015 - "openssl-sys", 2016 - "schannel", 2017 - "security-framework", 2018 - "security-framework-sys", 2019 - "tempfile", 2020 - ] 2021 - 2022 - [[package]] 2023 - name = "num-conv" 2024 - version = "0.1.0" 2025 - source = "registry+https://github.com/rust-lang/crates.io-index" 2026 - checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 2027 - 2028 - [[package]] 2029 - name = "num-traits" 2030 - version = "0.2.19" 2031 - source = "registry+https://github.com/rust-lang/crates.io-index" 2032 - checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 2033 - dependencies = [ 2034 - "autocfg", 2035 - ] 2036 - 2037 - [[package]] 2038 - name = "object" 2039 - version = "0.37.3" 2040 - source = "registry+https://github.com/rust-lang/crates.io-index" 2041 - checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 2042 - dependencies = [ 2043 - "memchr", 2044 - ] 2045 - 2046 - [[package]] 2047 - name = "once_cell" 2048 - version = "1.21.3" 2049 - source = "registry+https://github.com/rust-lang/crates.io-index" 2050 - checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2051 - 2052 - [[package]] 2053 - name = "once_cell_polyfill" 2054 - version = "1.70.1" 2055 - source = "registry+https://github.com/rust-lang/crates.io-index" 2056 - checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 2057 - 2058 - [[package]] 2059 - name = "opaque-debug" 2060 - version = "0.3.1" 2061 - source = "registry+https://github.com/rust-lang/crates.io-index" 2062 - checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" 2063 - 2064 - [[package]] 2065 - name = "openssl" 2066 - version = "0.10.73" 2067 - source = "registry+https://github.com/rust-lang/crates.io-index" 2068 - checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 2069 - dependencies = [ 2070 - "bitflags", 2071 - "cfg-if", 2072 - "foreign-types", 2073 - "libc", 2074 - "once_cell", 2075 - "openssl-macros", 2076 - "openssl-sys", 2077 - ] 2078 - 2079 - [[package]] 2080 - name = "openssl-macros" 2081 - version = "0.1.1" 2082 - source = "registry+https://github.com/rust-lang/crates.io-index" 2083 - checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 2084 - dependencies = [ 2085 - "proc-macro2", 2086 - "quote", 2087 - "syn 2.0.106", 2088 - ] 2089 - 2090 - [[package]] 2091 - name = "openssl-probe" 2092 - version = "0.1.6" 2093 - source = "registry+https://github.com/rust-lang/crates.io-index" 2094 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2095 - 2096 - [[package]] 2097 - name = "openssl-sys" 2098 - version = "0.9.109" 2099 - source = "registry+https://github.com/rust-lang/crates.io-index" 2100 - checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 2101 - dependencies = [ 2102 - "cc", 2103 - "libc", 2104 - "pkg-config", 2105 - "vcpkg", 2106 - ] 2107 - 2108 - [[package]] 2109 - name = "p256" 2110 - version = "0.13.2" 2111 - source = "registry+https://github.com/rust-lang/crates.io-index" 2112 - checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2113 - dependencies = [ 2114 - "ecdsa", 2115 - "elliptic-curve", 2116 - "primeorder", 2117 - "sha2", 2118 - ] 2119 - 2120 - [[package]] 2121 - name = "parking" 2122 - version = "2.2.1" 2123 - source = "registry+https://github.com/rust-lang/crates.io-index" 2124 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2125 - 2126 - [[package]] 2127 - name = "parking_lot" 2128 - version = "0.12.5" 2129 - source = "registry+https://github.com/rust-lang/crates.io-index" 2130 - checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 2131 - dependencies = [ 2132 - "lock_api", 2133 - "parking_lot_core", 2134 - ] 2135 - 2136 - [[package]] 2137 - name = "parking_lot_core" 2138 - version = "0.9.12" 2139 - source = "registry+https://github.com/rust-lang/crates.io-index" 2140 - checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 2141 - dependencies = [ 2142 - "cfg-if", 2143 - "libc", 2144 - "redox_syscall", 2145 - "smallvec", 2146 - "windows-link", 2147 - ] 2148 - 2149 - [[package]] 2150 - name = "percent-encoding" 2151 - version = "2.3.2" 2152 - source = "registry+https://github.com/rust-lang/crates.io-index" 2153 - checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 2154 - 2155 - [[package]] 2156 - name = "pin-project-lite" 2157 - version = "0.2.16" 2158 - source = "registry+https://github.com/rust-lang/crates.io-index" 2159 - checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 2160 - 2161 - [[package]] 2162 - name = "pin-utils" 2163 - version = "0.1.0" 2164 - source = "registry+https://github.com/rust-lang/crates.io-index" 2165 - checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2166 - 2167 - [[package]] 2168 - name = "pkg-config" 2169 - version = "0.3.32" 2170 - source = "registry+https://github.com/rust-lang/crates.io-index" 2171 - checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2172 - 2173 - [[package]] 2174 - name = "polyval" 2175 - version = "0.6.2" 2176 - source = "registry+https://github.com/rust-lang/crates.io-index" 2177 - checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" 2178 - dependencies = [ 2179 - "cfg-if", 2180 - "cpufeatures", 2181 - "opaque-debug", 2182 - "universal-hash", 2183 - ] 2184 - 2185 - [[package]] 2186 - name = "portable-atomic" 2187 - version = "1.11.1" 2188 - source = "registry+https://github.com/rust-lang/crates.io-index" 2189 - checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2190 - 2191 - [[package]] 2192 - name = "portable-atomic-util" 2193 - version = "0.2.4" 2194 - source = "registry+https://github.com/rust-lang/crates.io-index" 2195 - checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 2196 - dependencies = [ 2197 - "portable-atomic", 2198 - ] 2199 - 2200 - [[package]] 2201 - name = "potential_utf" 2202 - version = "0.1.3" 2203 - source = "registry+https://github.com/rust-lang/crates.io-index" 2204 - checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 2205 - dependencies = [ 2206 - "zerovec", 2207 - ] 2208 - 2209 - [[package]] 2210 - name = "powerfmt" 2211 - version = "0.2.0" 2212 - source = "registry+https://github.com/rust-lang/crates.io-index" 2213 - checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 2214 - 2215 - [[package]] 2216 - name = "ppv-lite86" 2217 - version = "0.2.21" 2218 - source = "registry+https://github.com/rust-lang/crates.io-index" 2219 - checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 2220 - dependencies = [ 2221 - "zerocopy", 2222 - ] 2223 - 2224 - [[package]] 2225 - name = "primeorder" 2226 - version = "0.13.6" 2227 - source = "registry+https://github.com/rust-lang/crates.io-index" 2228 - checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2229 - dependencies = [ 2230 - "elliptic-curve", 2231 - ] 2232 - 2233 - [[package]] 2234 - name = "proc-macro2" 2235 - version = "1.0.101" 2236 - source = "registry+https://github.com/rust-lang/crates.io-index" 2237 - checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 2238 - dependencies = [ 2239 - "unicode-ident", 2240 - ] 2241 - 2242 - [[package]] 2243 - name = "quote" 2244 - version = "1.0.41" 2245 - source = "registry+https://github.com/rust-lang/crates.io-index" 2246 - checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 2247 - dependencies = [ 2248 - "proc-macro2", 2249 - ] 2250 - 2251 - [[package]] 2252 - name = "r-efi" 2253 - version = "5.3.0" 2254 - source = "registry+https://github.com/rust-lang/crates.io-index" 2255 - checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 2256 - 2257 - [[package]] 2258 - name = "rand" 2259 - version = "0.8.5" 2260 - source = "registry+https://github.com/rust-lang/crates.io-index" 2261 - checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2262 - dependencies = [ 2263 - "libc", 2264 - "rand_chacha 0.3.1", 2265 - "rand_core 0.6.4", 2266 - ] 2267 - 2268 - [[package]] 2269 - name = "rand" 2270 - version = "0.9.2" 2271 - source = "registry+https://github.com/rust-lang/crates.io-index" 2272 - checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 2273 - dependencies = [ 2274 - "rand_chacha 0.9.0", 2275 - "rand_core 0.9.3", 2276 - ] 2277 - 2278 - [[package]] 2279 - name = "rand_chacha" 2280 - version = "0.3.1" 2281 - source = "registry+https://github.com/rust-lang/crates.io-index" 2282 - checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2283 - dependencies = [ 2284 - "ppv-lite86", 2285 - "rand_core 0.6.4", 2286 - ] 2287 - 2288 - [[package]] 2289 - name = "rand_chacha" 2290 - version = "0.9.0" 2291 - source = "registry+https://github.com/rust-lang/crates.io-index" 2292 - checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2293 - dependencies = [ 2294 - "ppv-lite86", 2295 - "rand_core 0.9.3", 2296 - ] 2297 - 2298 - [[package]] 2299 - name = "rand_core" 2300 - version = "0.6.4" 2301 - source = "registry+https://github.com/rust-lang/crates.io-index" 2302 - checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2303 - dependencies = [ 2304 - "getrandom 0.2.16", 2305 - ] 2306 - 2307 - [[package]] 2308 - name = "rand_core" 2309 - version = "0.9.3" 2310 - source = "registry+https://github.com/rust-lang/crates.io-index" 2311 - checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2312 - dependencies = [ 2313 - "getrandom 0.3.3", 2314 - ] 2315 - 2316 - [[package]] 2317 - name = "redox_syscall" 2318 - version = "0.5.18" 2319 - source = "registry+https://github.com/rust-lang/crates.io-index" 2320 - checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2321 - dependencies = [ 2322 - "bitflags", 2323 - ] 2324 - 2325 - [[package]] 2326 - name = "regex" 2327 - version = "1.11.3" 2328 - source = "registry+https://github.com/rust-lang/crates.io-index" 2329 - checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" 2330 - dependencies = [ 2331 - "aho-corasick", 2332 - "memchr", 2333 - "regex-automata", 2334 - "regex-syntax", 2335 - ] 2336 - 2337 - [[package]] 2338 - name = "regex-automata" 2339 - version = "0.4.11" 2340 - source = "registry+https://github.com/rust-lang/crates.io-index" 2341 - checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" 2342 - dependencies = [ 2343 - "aho-corasick", 2344 - "memchr", 2345 - "regex-syntax", 2346 - ] 2347 - 2348 - [[package]] 2349 - name = "regex-lite" 2350 - version = "0.1.7" 2351 - source = "registry+https://github.com/rust-lang/crates.io-index" 2352 - checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" 2353 - 2354 - [[package]] 2355 - name = "regex-syntax" 2356 - version = "0.8.6" 2357 - source = "registry+https://github.com/rust-lang/crates.io-index" 2358 - checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 2359 - 2360 - [[package]] 2361 - name = "reqwest" 2362 - version = "0.12.23" 2363 - source = "registry+https://github.com/rust-lang/crates.io-index" 2364 - checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 2365 - dependencies = [ 2366 - "async-compression", 2367 - "base64 0.22.1", 2368 - "bytes", 2369 - "futures-core", 2370 - "futures-util", 2371 - "http 1.3.1", 2372 - "http-body", 2373 - "http-body-util", 2374 - "hyper", 2375 - "hyper-tls", 2376 - "hyper-util", 2377 - "js-sys", 2378 - "log", 2379 - "native-tls", 2380 - "percent-encoding", 2381 - "pin-project-lite", 2382 - "rustls-pki-types", 2383 - "serde", 2384 - "serde_json", 2385 - "serde_urlencoded", 2386 - "sync_wrapper", 2387 - "tokio", 2388 - "tokio-native-tls", 2389 - "tokio-util", 2390 - "tower", 2391 - "tower-http", 2392 - "tower-service", 2393 - "url", 2394 - "wasm-bindgen", 2395 - "wasm-bindgen-futures", 2396 - "web-sys", 2397 - ] 2398 - 2399 - [[package]] 2400 - name = "resolv-conf" 2401 - version = "0.7.5" 2402 - source = "registry+https://github.com/rust-lang/crates.io-index" 2403 - checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" 2404 - 2405 - [[package]] 2406 - name = "rfc6979" 2407 - version = "0.4.0" 2408 - source = "registry+https://github.com/rust-lang/crates.io-index" 2409 - checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 2410 - dependencies = [ 2411 - "hmac", 2412 - "subtle", 2413 - ] 2414 - 2415 - [[package]] 2416 - name = "rustc-demangle" 2417 - version = "0.1.26" 2418 - source = "registry+https://github.com/rust-lang/crates.io-index" 2419 - checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 2420 - 2421 - [[package]] 2422 - name = "rustc_version" 2423 - version = "0.4.1" 2424 - source = "registry+https://github.com/rust-lang/crates.io-index" 2425 - checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2426 - dependencies = [ 2427 - "semver", 2428 - ] 2429 - 2430 - [[package]] 2431 - name = "rustix" 2432 - version = "1.1.2" 2433 - source = "registry+https://github.com/rust-lang/crates.io-index" 2434 - checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 2435 - dependencies = [ 2436 - "bitflags", 2437 - "errno", 2438 - "libc", 2439 - "linux-raw-sys", 2440 - "windows-sys 0.61.1", 2441 - ] 2442 - 2443 - [[package]] 2444 - name = "rustls-pki-types" 2445 - version = "1.12.0" 2446 - source = "registry+https://github.com/rust-lang/crates.io-index" 2447 - checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 2448 - dependencies = [ 2449 - "zeroize", 2450 - ] 2451 - 2452 - [[package]] 2453 - name = "rustversion" 2454 - version = "1.0.22" 2455 - source = "registry+https://github.com/rust-lang/crates.io-index" 2456 - checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2457 - 2458 - [[package]] 2459 - name = "ryu" 2460 - version = "1.0.20" 2461 - source = "registry+https://github.com/rust-lang/crates.io-index" 2462 - checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 2463 - 2464 - [[package]] 2465 - name = "schannel" 2466 - version = "0.1.28" 2467 - source = "registry+https://github.com/rust-lang/crates.io-index" 2468 - checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 2469 - dependencies = [ 2470 - "windows-sys 0.61.1", 2471 - ] 2472 - 2473 - [[package]] 2474 - name = "scopeguard" 2475 - version = "1.2.0" 2476 - source = "registry+https://github.com/rust-lang/crates.io-index" 2477 - checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2478 - 2479 - [[package]] 2480 - name = "sec1" 2481 - version = "0.7.3" 2482 - source = "registry+https://github.com/rust-lang/crates.io-index" 2483 - checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2484 - dependencies = [ 2485 - "base16ct", 2486 - "der", 2487 - "generic-array", 2488 - "subtle", 2489 - "zeroize", 2490 - ] 2491 - 2492 - [[package]] 2493 - name = "security-framework" 2494 - version = "2.11.1" 2495 - source = "registry+https://github.com/rust-lang/crates.io-index" 2496 - checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2497 - dependencies = [ 2498 - "bitflags", 2499 - "core-foundation", 2500 - "core-foundation-sys", 2501 - "libc", 2502 - "security-framework-sys", 2503 - ] 2504 - 2505 - [[package]] 2506 - name = "security-framework-sys" 2507 - version = "2.15.0" 2508 - source = "registry+https://github.com/rust-lang/crates.io-index" 2509 - checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 2510 - dependencies = [ 2511 - "core-foundation-sys", 2512 - "libc", 2513 - ] 2514 - 2515 - [[package]] 2516 - name = "semver" 2517 - version = "1.0.27" 2518 - source = "registry+https://github.com/rust-lang/crates.io-index" 2519 - checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 2520 - 2521 - [[package]] 2522 - name = "serde" 2523 - version = "1.0.228" 2524 - source = "registry+https://github.com/rust-lang/crates.io-index" 2525 - checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2526 - dependencies = [ 2527 - "serde_core", 2528 - "serde_derive", 2529 - ] 2530 - 2531 - [[package]] 2532 - name = "serde_bytes" 2533 - version = "0.11.19" 2534 - source = "registry+https://github.com/rust-lang/crates.io-index" 2535 - checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2536 - dependencies = [ 2537 - "serde", 2538 - "serde_core", 2539 - ] 2540 - 2541 - [[package]] 2542 - name = "serde_core" 2543 - version = "1.0.228" 2544 - source = "registry+https://github.com/rust-lang/crates.io-index" 2545 - checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2546 - dependencies = [ 2547 - "serde_derive", 2548 - ] 2549 - 2550 - [[package]] 2551 - name = "serde_derive" 2552 - version = "1.0.228" 2553 - source = "registry+https://github.com/rust-lang/crates.io-index" 2554 - checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2555 - dependencies = [ 2556 - "proc-macro2", 2557 - "quote", 2558 - "syn 2.0.106", 2559 - ] 2560 - 2561 - [[package]] 2562 - name = "serde_html_form" 2563 - version = "0.2.8" 2564 - source = "registry+https://github.com/rust-lang/crates.io-index" 2565 - checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 2566 - dependencies = [ 2567 - "form_urlencoded", 2568 - "indexmap", 2569 - "itoa", 2570 - "ryu", 2571 - "serde_core", 2572 - ] 2573 - 2574 - [[package]] 2575 - name = "serde_json" 2576 - version = "1.0.145" 2577 - source = "registry+https://github.com/rust-lang/crates.io-index" 2578 - checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 2579 - dependencies = [ 2580 - "itoa", 2581 - "memchr", 2582 - "ryu", 2583 - "serde", 2584 - "serde_core", 2585 - ] 2586 - 2587 - [[package]] 2588 - name = "serde_urlencoded" 2589 - version = "0.7.1" 2590 - source = "registry+https://github.com/rust-lang/crates.io-index" 2591 - checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2592 - dependencies = [ 2593 - "form_urlencoded", 2594 - "itoa", 2595 - "ryu", 2596 - "serde", 2597 - ] 2598 - 2599 - [[package]] 2600 - name = "sha1" 2601 - version = "0.10.6" 2602 - source = "registry+https://github.com/rust-lang/crates.io-index" 2603 - checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2604 - dependencies = [ 2605 - "cfg-if", 2606 - "cpufeatures", 2607 - "digest", 2608 - ] 2609 - 2610 - [[package]] 2611 - name = "sha2" 2612 - version = "0.10.9" 2613 - source = "registry+https://github.com/rust-lang/crates.io-index" 2614 - checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2615 - dependencies = [ 2616 - "cfg-if", 2617 - "cpufeatures", 2618 - "digest", 2619 - ] 2620 - 2621 - [[package]] 2622 - name = "shlex" 2623 - version = "1.3.0" 2624 - source = "registry+https://github.com/rust-lang/crates.io-index" 2625 - checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2626 - 2627 - [[package]] 2628 - name = "signal-hook-registry" 2629 - version = "1.4.6" 2630 - source = "registry+https://github.com/rust-lang/crates.io-index" 2631 - checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 2632 - dependencies = [ 2633 - "libc", 2634 - ] 2635 - 2636 - [[package]] 2637 - name = "signature" 2638 - version = "2.2.0" 2639 - source = "registry+https://github.com/rust-lang/crates.io-index" 2640 - checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2641 - dependencies = [ 2642 - "digest", 2643 - "rand_core 0.6.4", 2644 - ] 2645 - 2646 - [[package]] 2647 - name = "simd-adler32" 2648 - version = "0.3.7" 2649 - source = "registry+https://github.com/rust-lang/crates.io-index" 2650 - checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 2651 - 2652 - [[package]] 2653 - name = "slab" 2654 - version = "0.4.11" 2655 - source = "registry+https://github.com/rust-lang/crates.io-index" 2656 - checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 2657 - 2658 - [[package]] 2659 - name = "smallvec" 2660 - version = "1.15.1" 2661 - source = "registry+https://github.com/rust-lang/crates.io-index" 2662 - checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2663 - 2664 - [[package]] 2665 - name = "socket2" 2666 - version = "0.5.10" 2667 - source = "registry+https://github.com/rust-lang/crates.io-index" 2668 - checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 2669 - dependencies = [ 2670 - "libc", 2671 - "windows-sys 0.52.0", 2672 - ] 2673 - 2674 - [[package]] 2675 - name = "socket2" 2676 - version = "0.6.0" 2677 - source = "registry+https://github.com/rust-lang/crates.io-index" 2678 - checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 2679 - dependencies = [ 2680 - "libc", 2681 - "windows-sys 0.59.0", 2682 - ] 2683 - 2684 - [[package]] 2685 - name = "stable_deref_trait" 2686 - version = "1.2.0" 2687 - source = "registry+https://github.com/rust-lang/crates.io-index" 2688 - checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2689 - 2690 - [[package]] 2691 - name = "subtle" 2692 - version = "2.6.1" 2693 - source = "registry+https://github.com/rust-lang/crates.io-index" 2694 - checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2695 - 2696 - [[package]] 2697 - name = "syn" 2698 - version = "1.0.109" 2699 - source = "registry+https://github.com/rust-lang/crates.io-index" 2700 - checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 2701 - dependencies = [ 2702 - "proc-macro2", 2703 - "quote", 2704 - "unicode-ident", 2705 - ] 2706 - 2707 - [[package]] 2708 - name = "syn" 2709 - version = "2.0.106" 2710 - source = "registry+https://github.com/rust-lang/crates.io-index" 2711 - checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 2712 - dependencies = [ 2713 - "proc-macro2", 2714 - "quote", 2715 - "unicode-ident", 2716 - ] 2717 - 2718 - [[package]] 2719 - name = "sync_wrapper" 2720 - version = "1.0.2" 2721 - source = "registry+https://github.com/rust-lang/crates.io-index" 2722 - checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2723 - dependencies = [ 2724 - "futures-core", 2725 - ] 2726 - 2727 - [[package]] 2728 - name = "synstructure" 2729 - version = "0.13.2" 2730 - source = "registry+https://github.com/rust-lang/crates.io-index" 2731 - checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2732 - dependencies = [ 2733 - "proc-macro2", 2734 - "quote", 2735 - "syn 2.0.106", 2736 - ] 2737 - 2738 - [[package]] 2739 - name = "tagptr" 2740 - version = "0.2.0" 2741 - source = "registry+https://github.com/rust-lang/crates.io-index" 2742 - checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 2743 - 2744 - [[package]] 2745 - name = "tempfile" 2746 - version = "3.23.0" 2747 - source = "registry+https://github.com/rust-lang/crates.io-index" 2748 - checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 2749 - dependencies = [ 2750 - "fastrand", 2751 - "getrandom 0.3.3", 2752 - "once_cell", 2753 - "rustix", 2754 - "windows-sys 0.61.1", 2755 - ] 2756 - 2757 - [[package]] 2758 - name = "thiserror" 2759 - version = "1.0.69" 2760 - source = "registry+https://github.com/rust-lang/crates.io-index" 2761 - checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2762 - dependencies = [ 2763 - "thiserror-impl", 2764 - ] 2765 - 2766 - [[package]] 2767 - name = "thiserror-impl" 2768 - version = "1.0.69" 2769 - source = "registry+https://github.com/rust-lang/crates.io-index" 2770 - checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2771 - dependencies = [ 2772 - "proc-macro2", 2773 - "quote", 2774 - "syn 2.0.106", 2775 - ] 2776 - 2777 - [[package]] 2778 - name = "time" 2779 - version = "0.3.44" 2780 - source = "registry+https://github.com/rust-lang/crates.io-index" 2781 - checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 2782 - dependencies = [ 2783 - "deranged", 2784 - "itoa", 2785 - "num-conv", 2786 - "powerfmt", 2787 - "serde", 2788 - "time-core", 2789 - "time-macros", 2790 - ] 2791 - 2792 - [[package]] 2793 - name = "time-core" 2794 - version = "0.1.6" 2795 - source = "registry+https://github.com/rust-lang/crates.io-index" 2796 - checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 2797 - 2798 - [[package]] 2799 - name = "time-macros" 2800 - version = "0.2.24" 2801 - source = "registry+https://github.com/rust-lang/crates.io-index" 2802 - checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 2803 - dependencies = [ 2804 - "num-conv", 2805 - "time-core", 2806 - ] 2807 - 2808 - [[package]] 2809 - name = "tinystr" 2810 - version = "0.8.1" 2811 - source = "registry+https://github.com/rust-lang/crates.io-index" 2812 - checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2813 - dependencies = [ 2814 - "displaydoc", 2815 - "zerovec", 2816 - ] 2817 - 2818 - [[package]] 2819 - name = "tinyvec" 2820 - version = "1.10.0" 2821 - source = "registry+https://github.com/rust-lang/crates.io-index" 2822 - checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 2823 - dependencies = [ 2824 - "tinyvec_macros", 2825 - ] 2826 - 2827 - [[package]] 2828 - name = "tinyvec_macros" 2829 - version = "0.1.1" 2830 - source = "registry+https://github.com/rust-lang/crates.io-index" 2831 - checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2832 - 2833 - [[package]] 2834 - name = "tokio" 2835 - version = "1.47.1" 2836 - source = "registry+https://github.com/rust-lang/crates.io-index" 2837 - checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 2838 - dependencies = [ 2839 - "backtrace", 2840 - "bytes", 2841 - "io-uring", 2842 - "libc", 2843 - "mio", 2844 - "parking_lot", 2845 - "pin-project-lite", 2846 - "signal-hook-registry", 2847 - "slab", 2848 - "socket2 0.6.0", 2849 - "tokio-macros", 2850 - "windows-sys 0.59.0", 2851 - ] 2852 - 2853 - [[package]] 2854 - name = "tokio-macros" 2855 - version = "2.5.0" 2856 - source = "registry+https://github.com/rust-lang/crates.io-index" 2857 - checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2858 - dependencies = [ 2859 - "proc-macro2", 2860 - "quote", 2861 - "syn 2.0.106", 2862 - ] 2863 - 2864 - [[package]] 2865 - name = "tokio-native-tls" 2866 - version = "0.3.1" 2867 - source = "registry+https://github.com/rust-lang/crates.io-index" 2868 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2869 - dependencies = [ 2870 - "native-tls", 2871 - "tokio", 2872 - ] 2873 - 2874 - [[package]] 2875 - name = "tokio-util" 2876 - version = "0.7.16" 2877 - source = "registry+https://github.com/rust-lang/crates.io-index" 2878 - checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 2879 - dependencies = [ 2880 - "bytes", 2881 - "futures-core", 2882 - "futures-sink", 2883 - "pin-project-lite", 2884 - "tokio", 2885 - ] 2886 - 2887 - [[package]] 2888 - name = "tower" 2889 - version = "0.5.2" 2890 - source = "registry+https://github.com/rust-lang/crates.io-index" 2891 - checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2892 - dependencies = [ 2893 - "futures-core", 2894 - "futures-util", 2895 - "pin-project-lite", 2896 - "sync_wrapper", 2897 - "tokio", 2898 - "tower-layer", 2899 - "tower-service", 2900 - ] 2901 - 2902 - [[package]] 2903 - name = "tower-http" 2904 - version = "0.6.6" 2905 - source = "registry+https://github.com/rust-lang/crates.io-index" 2906 - checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2907 - dependencies = [ 2908 - "bitflags", 2909 - "bytes", 2910 - "futures-util", 2911 - "http 1.3.1", 2912 - "http-body", 2913 - "iri-string", 2914 - "pin-project-lite", 2915 - "tower", 2916 - "tower-layer", 2917 - "tower-service", 2918 - ] 2919 - 2920 - [[package]] 2921 - name = "tower-layer" 2922 - version = "0.3.3" 2923 - source = "registry+https://github.com/rust-lang/crates.io-index" 2924 - checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2925 - 2926 - [[package]] 2927 - name = "tower-service" 2928 - version = "0.3.3" 2929 - source = "registry+https://github.com/rust-lang/crates.io-index" 2930 - checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2931 - 2932 - [[package]] 2933 - name = "tracing" 2934 - version = "0.1.41" 2935 - source = "registry+https://github.com/rust-lang/crates.io-index" 2936 - checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2937 - dependencies = [ 2938 - "log", 2939 - "pin-project-lite", 2940 - "tracing-attributes", 2941 - "tracing-core", 2942 - ] 2943 - 2944 - [[package]] 2945 - name = "tracing-attributes" 2946 - version = "0.1.30" 2947 - source = "registry+https://github.com/rust-lang/crates.io-index" 2948 - checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 2949 - dependencies = [ 2950 - "proc-macro2", 2951 - "quote", 2952 - "syn 2.0.106", 2953 - ] 2954 - 2955 - [[package]] 2956 - name = "tracing-core" 2957 - version = "0.1.34" 2958 - source = "registry+https://github.com/rust-lang/crates.io-index" 2959 - checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2960 - dependencies = [ 2961 - "once_cell", 2962 - ] 2963 - 2964 - [[package]] 2965 - name = "trait-variant" 2966 - version = "0.1.2" 2967 - source = "registry+https://github.com/rust-lang/crates.io-index" 2968 - checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 2969 - dependencies = [ 2970 - "proc-macro2", 2971 - "quote", 2972 - "syn 2.0.106", 2973 - ] 2974 - 2975 - [[package]] 2976 - name = "try-lock" 2977 - version = "0.2.5" 2978 - source = "registry+https://github.com/rust-lang/crates.io-index" 2979 - checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2980 - 2981 - [[package]] 2982 - name = "typenum" 2983 - version = "1.19.0" 2984 - source = "registry+https://github.com/rust-lang/crates.io-index" 2985 - checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 2986 - 2987 - [[package]] 2988 - name = "unicase" 2989 - version = "2.8.1" 2990 - source = "registry+https://github.com/rust-lang/crates.io-index" 2991 - checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2992 - 2993 - [[package]] 2994 - name = "unicode-ident" 2995 - version = "1.0.19" 2996 - source = "registry+https://github.com/rust-lang/crates.io-index" 2997 - checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 2998 - 2999 - [[package]] 3000 - name = "unicode-xid" 3001 - version = "0.2.6" 3002 - source = "registry+https://github.com/rust-lang/crates.io-index" 3003 - checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 3004 - 3005 - [[package]] 3006 - name = "universal-hash" 3007 - version = "0.5.1" 3008 - source = "registry+https://github.com/rust-lang/crates.io-index" 3009 - checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 3010 - dependencies = [ 3011 - "crypto-common", 3012 - "subtle", 3013 - ] 3014 - 3015 - [[package]] 3016 - name = "unsigned-varint" 3017 - version = "0.8.0" 3018 - source = "registry+https://github.com/rust-lang/crates.io-index" 3019 - checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 3020 - 3021 - [[package]] 3022 - name = "url" 3023 - version = "2.5.7" 3024 - source = "registry+https://github.com/rust-lang/crates.io-index" 3025 - checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 3026 - dependencies = [ 3027 - "form_urlencoded", 3028 - "idna", 3029 - "percent-encoding", 3030 - "serde", 3031 - ] 3032 - 3033 - [[package]] 3034 - name = "utf8_iter" 3035 - version = "1.0.4" 3036 - source = "registry+https://github.com/rust-lang/crates.io-index" 3037 - checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 3038 - 3039 - [[package]] 3040 - name = "utf8parse" 3041 - version = "0.2.2" 3042 - source = "registry+https://github.com/rust-lang/crates.io-index" 3043 - checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3044 - 3045 - [[package]] 3046 - name = "uuid" 3047 - version = "1.18.1" 3048 - source = "registry+https://github.com/rust-lang/crates.io-index" 3049 - checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 3050 - dependencies = [ 3051 - "getrandom 0.3.3", 3052 - "js-sys", 3053 - "wasm-bindgen", 3054 - ] 3055 - 3056 - [[package]] 3057 - name = "v_htmlescape" 3058 - version = "0.15.8" 3059 - source = "registry+https://github.com/rust-lang/crates.io-index" 3060 - checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" 3061 - 3062 - [[package]] 3063 - name = "vcpkg" 3064 - version = "0.2.15" 3065 - source = "registry+https://github.com/rust-lang/crates.io-index" 3066 - checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 3067 - 3068 - [[package]] 3069 - name = "version_check" 3070 - version = "0.9.5" 3071 - source = "registry+https://github.com/rust-lang/crates.io-index" 3072 - checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3073 - 3074 - [[package]] 3075 - name = "want" 3076 - version = "0.3.1" 3077 - source = "registry+https://github.com/rust-lang/crates.io-index" 3078 - checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 3079 - dependencies = [ 3080 - "try-lock", 3081 - ] 3082 - 3083 - [[package]] 3084 - name = "wasi" 3085 - version = "0.11.1+wasi-snapshot-preview1" 3086 - source = "registry+https://github.com/rust-lang/crates.io-index" 3087 - checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3088 - 3089 - [[package]] 3090 - name = "wasi" 3091 - version = "0.14.7+wasi-0.2.4" 3092 - source = "registry+https://github.com/rust-lang/crates.io-index" 3093 - checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" 3094 - dependencies = [ 3095 - "wasip2", 3096 - ] 3097 - 3098 - [[package]] 3099 - name = "wasip2" 3100 - version = "1.0.1+wasi-0.2.4" 3101 - source = "registry+https://github.com/rust-lang/crates.io-index" 3102 - checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 3103 - dependencies = [ 3104 - "wit-bindgen", 3105 - ] 3106 - 3107 - [[package]] 3108 - name = "wasm-bindgen" 3109 - version = "0.2.104" 3110 - source = "registry+https://github.com/rust-lang/crates.io-index" 3111 - checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 3112 - dependencies = [ 3113 - "cfg-if", 3114 - "once_cell", 3115 - "rustversion", 3116 - "wasm-bindgen-macro", 3117 - "wasm-bindgen-shared", 3118 - ] 3119 - 3120 - [[package]] 3121 - name = "wasm-bindgen-backend" 3122 - version = "0.2.104" 3123 - source = "registry+https://github.com/rust-lang/crates.io-index" 3124 - checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 3125 - dependencies = [ 3126 - "bumpalo", 3127 - "log", 3128 - "proc-macro2", 3129 - "quote", 3130 - "syn 2.0.106", 3131 - "wasm-bindgen-shared", 3132 - ] 3133 - 3134 - [[package]] 3135 - name = "wasm-bindgen-futures" 3136 - version = "0.4.54" 3137 - source = "registry+https://github.com/rust-lang/crates.io-index" 3138 - checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" 3139 - dependencies = [ 3140 - "cfg-if", 3141 - "js-sys", 3142 - "once_cell", 3143 - "wasm-bindgen", 3144 - "web-sys", 3145 - ] 3146 - 3147 - [[package]] 3148 - name = "wasm-bindgen-macro" 3149 - version = "0.2.104" 3150 - source = "registry+https://github.com/rust-lang/crates.io-index" 3151 - checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 3152 - dependencies = [ 3153 - "quote", 3154 - "wasm-bindgen-macro-support", 3155 - ] 3156 - 3157 - [[package]] 3158 - name = "wasm-bindgen-macro-support" 3159 - version = "0.2.104" 3160 - source = "registry+https://github.com/rust-lang/crates.io-index" 3161 - checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 3162 - dependencies = [ 3163 - "proc-macro2", 3164 - "quote", 3165 - "syn 2.0.106", 3166 - "wasm-bindgen-backend", 3167 - "wasm-bindgen-shared", 3168 - ] 3169 - 3170 - [[package]] 3171 - name = "wasm-bindgen-shared" 3172 - version = "0.2.104" 3173 - source = "registry+https://github.com/rust-lang/crates.io-index" 3174 - checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 3175 - dependencies = [ 3176 - "unicode-ident", 3177 - ] 3178 - 3179 - [[package]] 3180 - name = "web-sys" 3181 - version = "0.3.81" 3182 - source = "registry+https://github.com/rust-lang/crates.io-index" 3183 - checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" 3184 - dependencies = [ 3185 - "js-sys", 3186 - "wasm-bindgen", 3187 - ] 3188 - 3189 - [[package]] 3190 - name = "web-time" 3191 - version = "1.1.0" 3192 - source = "registry+https://github.com/rust-lang/crates.io-index" 3193 - checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 3194 - dependencies = [ 3195 - "js-sys", 3196 - "wasm-bindgen", 3197 - ] 3198 - 3199 - [[package]] 3200 - name = "widestring" 3201 - version = "1.2.0" 3202 - source = "registry+https://github.com/rust-lang/crates.io-index" 3203 - checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 3204 - 3205 - [[package]] 3206 - name = "windows-core" 3207 - version = "0.62.1" 3208 - source = "registry+https://github.com/rust-lang/crates.io-index" 3209 - checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" 3210 - dependencies = [ 3211 - "windows-implement", 3212 - "windows-interface", 3213 - "windows-link", 3214 - "windows-result", 3215 - "windows-strings", 3216 - ] 3217 - 3218 - [[package]] 3219 - name = "windows-implement" 3220 - version = "0.60.1" 3221 - source = "registry+https://github.com/rust-lang/crates.io-index" 3222 - checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" 3223 - dependencies = [ 3224 - "proc-macro2", 3225 - "quote", 3226 - "syn 2.0.106", 3227 - ] 3228 - 3229 - [[package]] 3230 - name = "windows-interface" 3231 - version = "0.59.2" 3232 - source = "registry+https://github.com/rust-lang/crates.io-index" 3233 - checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" 3234 - dependencies = [ 3235 - "proc-macro2", 3236 - "quote", 3237 - "syn 2.0.106", 3238 - ] 3239 - 3240 - [[package]] 3241 - name = "windows-link" 3242 - version = "0.2.0" 3243 - source = "registry+https://github.com/rust-lang/crates.io-index" 3244 - checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 3245 - 3246 - [[package]] 3247 - name = "windows-result" 3248 - version = "0.4.0" 3249 - source = "registry+https://github.com/rust-lang/crates.io-index" 3250 - checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" 3251 - dependencies = [ 3252 - "windows-link", 3253 - ] 3254 - 3255 - [[package]] 3256 - name = "windows-strings" 3257 - version = "0.5.0" 3258 - source = "registry+https://github.com/rust-lang/crates.io-index" 3259 - checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" 3260 - dependencies = [ 3261 - "windows-link", 3262 - ] 3263 - 3264 - [[package]] 3265 - name = "windows-sys" 3266 - version = "0.48.0" 3267 - source = "registry+https://github.com/rust-lang/crates.io-index" 3268 - checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3269 - dependencies = [ 3270 - "windows-targets 0.48.5", 3271 - ] 3272 - 3273 - [[package]] 3274 - name = "windows-sys" 3275 - version = "0.52.0" 3276 - source = "registry+https://github.com/rust-lang/crates.io-index" 3277 - checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3278 - dependencies = [ 3279 - "windows-targets 0.52.6", 3280 - ] 3281 - 3282 - [[package]] 3283 - name = "windows-sys" 3284 - version = "0.59.0" 3285 - source = "registry+https://github.com/rust-lang/crates.io-index" 3286 - checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3287 - dependencies = [ 3288 - "windows-targets 0.52.6", 3289 - ] 3290 - 3291 - [[package]] 3292 - name = "windows-sys" 3293 - version = "0.60.2" 3294 - source = "registry+https://github.com/rust-lang/crates.io-index" 3295 - checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3296 - dependencies = [ 3297 - "windows-targets 0.53.4", 3298 - ] 3299 - 3300 - [[package]] 3301 - name = "windows-sys" 3302 - version = "0.61.1" 3303 - source = "registry+https://github.com/rust-lang/crates.io-index" 3304 - checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" 3305 - dependencies = [ 3306 - "windows-link", 3307 - ] 3308 - 3309 - [[package]] 3310 - name = "windows-targets" 3311 - version = "0.48.5" 3312 - source = "registry+https://github.com/rust-lang/crates.io-index" 3313 - checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3314 - dependencies = [ 3315 - "windows_aarch64_gnullvm 0.48.5", 3316 - "windows_aarch64_msvc 0.48.5", 3317 - "windows_i686_gnu 0.48.5", 3318 - "windows_i686_msvc 0.48.5", 3319 - "windows_x86_64_gnu 0.48.5", 3320 - "windows_x86_64_gnullvm 0.48.5", 3321 - "windows_x86_64_msvc 0.48.5", 3322 - ] 3323 - 3324 - [[package]] 3325 - name = "windows-targets" 3326 - version = "0.52.6" 3327 - source = "registry+https://github.com/rust-lang/crates.io-index" 3328 - checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3329 - dependencies = [ 3330 - "windows_aarch64_gnullvm 0.52.6", 3331 - "windows_aarch64_msvc 0.52.6", 3332 - "windows_i686_gnu 0.52.6", 3333 - "windows_i686_gnullvm 0.52.6", 3334 - "windows_i686_msvc 0.52.6", 3335 - "windows_x86_64_gnu 0.52.6", 3336 - "windows_x86_64_gnullvm 0.52.6", 3337 - "windows_x86_64_msvc 0.52.6", 3338 - ] 3339 - 3340 - [[package]] 3341 - name = "windows-targets" 3342 - version = "0.53.4" 3343 - source = "registry+https://github.com/rust-lang/crates.io-index" 3344 - checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" 3345 - dependencies = [ 3346 - "windows-link", 3347 - "windows_aarch64_gnullvm 0.53.0", 3348 - "windows_aarch64_msvc 0.53.0", 3349 - "windows_i686_gnu 0.53.0", 3350 - "windows_i686_gnullvm 0.53.0", 3351 - "windows_i686_msvc 0.53.0", 3352 - "windows_x86_64_gnu 0.53.0", 3353 - "windows_x86_64_gnullvm 0.53.0", 3354 - "windows_x86_64_msvc 0.53.0", 3355 - ] 3356 - 3357 - [[package]] 3358 - name = "windows_aarch64_gnullvm" 3359 - version = "0.48.5" 3360 - source = "registry+https://github.com/rust-lang/crates.io-index" 3361 - checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3362 - 3363 - [[package]] 3364 - name = "windows_aarch64_gnullvm" 3365 - version = "0.52.6" 3366 - source = "registry+https://github.com/rust-lang/crates.io-index" 3367 - checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3368 - 3369 - [[package]] 3370 - name = "windows_aarch64_gnullvm" 3371 - version = "0.53.0" 3372 - source = "registry+https://github.com/rust-lang/crates.io-index" 3373 - checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 3374 - 3375 - [[package]] 3376 - name = "windows_aarch64_msvc" 3377 - version = "0.48.5" 3378 - source = "registry+https://github.com/rust-lang/crates.io-index" 3379 - checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3380 - 3381 - [[package]] 3382 - name = "windows_aarch64_msvc" 3383 - version = "0.52.6" 3384 - source = "registry+https://github.com/rust-lang/crates.io-index" 3385 - checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3386 - 3387 - [[package]] 3388 - name = "windows_aarch64_msvc" 3389 - version = "0.53.0" 3390 - source = "registry+https://github.com/rust-lang/crates.io-index" 3391 - checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 3392 - 3393 - [[package]] 3394 - name = "windows_i686_gnu" 3395 - version = "0.48.5" 3396 - source = "registry+https://github.com/rust-lang/crates.io-index" 3397 - checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3398 - 3399 - [[package]] 3400 - name = "windows_i686_gnu" 3401 - version = "0.52.6" 3402 - source = "registry+https://github.com/rust-lang/crates.io-index" 3403 - checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3404 - 3405 - [[package]] 3406 - name = "windows_i686_gnu" 3407 - version = "0.53.0" 3408 - source = "registry+https://github.com/rust-lang/crates.io-index" 3409 - checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 3410 - 3411 - [[package]] 3412 - name = "windows_i686_gnullvm" 3413 - version = "0.52.6" 3414 - source = "registry+https://github.com/rust-lang/crates.io-index" 3415 - checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3416 - 3417 - [[package]] 3418 - name = "windows_i686_gnullvm" 3419 - version = "0.53.0" 3420 - source = "registry+https://github.com/rust-lang/crates.io-index" 3421 - checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 3422 - 3423 - [[package]] 3424 - name = "windows_i686_msvc" 3425 - version = "0.48.5" 3426 - source = "registry+https://github.com/rust-lang/crates.io-index" 3427 - checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3428 - 3429 - [[package]] 3430 - name = "windows_i686_msvc" 3431 - version = "0.52.6" 3432 - source = "registry+https://github.com/rust-lang/crates.io-index" 3433 - checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3434 - 3435 - [[package]] 3436 - name = "windows_i686_msvc" 3437 - version = "0.53.0" 3438 - source = "registry+https://github.com/rust-lang/crates.io-index" 3439 - checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 3440 - 3441 - [[package]] 3442 - name = "windows_x86_64_gnu" 3443 - version = "0.48.5" 3444 - source = "registry+https://github.com/rust-lang/crates.io-index" 3445 - checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3446 - 3447 - [[package]] 3448 - name = "windows_x86_64_gnu" 3449 - version = "0.52.6" 3450 - source = "registry+https://github.com/rust-lang/crates.io-index" 3451 - checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3452 - 3453 - [[package]] 3454 - name = "windows_x86_64_gnu" 3455 - version = "0.53.0" 3456 - source = "registry+https://github.com/rust-lang/crates.io-index" 3457 - checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 3458 - 3459 - [[package]] 3460 - name = "windows_x86_64_gnullvm" 3461 - version = "0.48.5" 3462 - source = "registry+https://github.com/rust-lang/crates.io-index" 3463 - checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3464 - 3465 - [[package]] 3466 - name = "windows_x86_64_gnullvm" 3467 - version = "0.52.6" 3468 - source = "registry+https://github.com/rust-lang/crates.io-index" 3469 - checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3470 - 3471 - [[package]] 3472 - name = "windows_x86_64_gnullvm" 3473 - version = "0.53.0" 3474 - source = "registry+https://github.com/rust-lang/crates.io-index" 3475 - checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 3476 - 3477 - [[package]] 3478 - name = "windows_x86_64_msvc" 3479 - version = "0.48.5" 3480 - source = "registry+https://github.com/rust-lang/crates.io-index" 3481 - checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3482 - 3483 - [[package]] 3484 - name = "windows_x86_64_msvc" 3485 - version = "0.52.6" 3486 - source = "registry+https://github.com/rust-lang/crates.io-index" 3487 - checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3488 - 3489 - [[package]] 3490 - name = "windows_x86_64_msvc" 3491 - version = "0.53.0" 3492 - source = "registry+https://github.com/rust-lang/crates.io-index" 3493 - checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 3494 - 3495 - [[package]] 3496 - name = "winreg" 3497 - version = "0.50.0" 3498 - source = "registry+https://github.com/rust-lang/crates.io-index" 3499 - checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 3500 - dependencies = [ 3501 - "cfg-if", 3502 - "windows-sys 0.48.0", 3503 - ] 3504 - 3505 - [[package]] 3506 - name = "wit-bindgen" 3507 - version = "0.46.0" 3508 - source = "registry+https://github.com/rust-lang/crates.io-index" 3509 - checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 3510 - 3511 - [[package]] 3512 - name = "writeable" 3513 - version = "0.6.1" 3514 - source = "registry+https://github.com/rust-lang/crates.io-index" 3515 - checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 3516 - 3517 - [[package]] 3518 - name = "yoke" 3519 - version = "0.8.0" 3520 - source = "registry+https://github.com/rust-lang/crates.io-index" 3521 - checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 3522 - dependencies = [ 3523 - "serde", 3524 - "stable_deref_trait", 3525 - "yoke-derive", 3526 - "zerofrom", 3527 - ] 3528 - 3529 - [[package]] 3530 - name = "yoke-derive" 3531 - version = "0.8.0" 3532 - source = "registry+https://github.com/rust-lang/crates.io-index" 3533 - checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 3534 - dependencies = [ 3535 - "proc-macro2", 3536 - "quote", 3537 - "syn 2.0.106", 3538 - "synstructure", 3539 - ] 3540 - 3541 - [[package]] 3542 - name = "zerocopy" 3543 - version = "0.8.27" 3544 - source = "registry+https://github.com/rust-lang/crates.io-index" 3545 - checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 3546 - dependencies = [ 3547 - "zerocopy-derive", 3548 - ] 3549 - 3550 - [[package]] 3551 - name = "zerocopy-derive" 3552 - version = "0.8.27" 3553 - source = "registry+https://github.com/rust-lang/crates.io-index" 3554 - checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 3555 - dependencies = [ 3556 - "proc-macro2", 3557 - "quote", 3558 - "syn 2.0.106", 3559 - ] 3560 - 3561 - [[package]] 3562 - name = "zerofrom" 3563 - version = "0.1.6" 3564 - source = "registry+https://github.com/rust-lang/crates.io-index" 3565 - checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3566 - dependencies = [ 3567 - "zerofrom-derive", 3568 - ] 3569 - 3570 - [[package]] 3571 - name = "zerofrom-derive" 3572 - version = "0.1.6" 3573 - source = "registry+https://github.com/rust-lang/crates.io-index" 3574 - checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3575 - dependencies = [ 3576 - "proc-macro2", 3577 - "quote", 3578 - "syn 2.0.106", 3579 - "synstructure", 3580 - ] 3581 - 3582 - [[package]] 3583 - name = "zeroize" 3584 - version = "1.8.2" 3585 - source = "registry+https://github.com/rust-lang/crates.io-index" 3586 - checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 3587 - dependencies = [ 3588 - "serde", 3589 - ] 3590 - 3591 - [[package]] 3592 - name = "zerotrie" 3593 - version = "0.2.2" 3594 - source = "registry+https://github.com/rust-lang/crates.io-index" 3595 - checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 3596 - dependencies = [ 3597 - "displaydoc", 3598 - "yoke", 3599 - "zerofrom", 3600 - ] 3601 - 3602 - [[package]] 3603 - name = "zerovec" 3604 - version = "0.11.4" 3605 - source = "registry+https://github.com/rust-lang/crates.io-index" 3606 - checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 3607 - dependencies = [ 3608 - "yoke", 3609 - "zerofrom", 3610 - "zerovec-derive", 3611 - ] 3612 - 3613 - [[package]] 3614 - name = "zerovec-derive" 3615 - version = "0.11.1" 3616 - source = "registry+https://github.com/rust-lang/crates.io-index" 3617 - checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 3618 - dependencies = [ 3619 - "proc-macro2", 3620 - "quote", 3621 - "syn 2.0.106", 3622 - ] 3623 - 3624 - [[package]] 3625 - name = "zstd" 3626 - version = "0.13.3" 3627 - source = "registry+https://github.com/rust-lang/crates.io-index" 3628 - checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 3629 - dependencies = [ 3630 - "zstd-safe", 3631 - ] 3632 - 3633 - [[package]] 3634 - name = "zstd-safe" 3635 - version = "7.2.4" 3636 - source = "registry+https://github.com/rust-lang/crates.io-index" 3637 - checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 3638 - dependencies = [ 3639 - "zstd-sys", 3640 - ] 3641 - 3642 - [[package]] 3643 - name = "zstd-sys" 3644 - version = "2.0.16+zstd.1.5.7" 3645 - source = "registry+https://github.com/rust-lang/crates.io-index" 3646 - checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 3647 - dependencies = [ 3648 - "cc", 3649 - "pkg-config", 3650 - ]
···
-19
Cargo.toml
··· 1 - [package] 2 - name = "at-me" 3 - version = "0.1.0" 4 - edition = "2021" 5 - 6 - [dependencies] 7 - actix-web = "4.10" 8 - actix-files = "0.6" 9 - actix-session = { version = "0.10", features = ["cookie-session"] } 10 - atrium-api = "0.25" 11 - atrium-common = "0.1" 12 - atrium-oauth = "0.1.0" 13 - atrium-identity = "0.1.3" 14 - serde = { version = "1.0", features = ["derive"] } 15 - serde_json = "1.0" 16 - tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 17 - hickory-resolver = "0.24" 18 - env_logger = "0.11" 19 - log = "0.4"
···
-43
Dockerfile
··· 1 - # Build stage 2 - FROM rustlang/rust:nightly-slim AS builder 3 - 4 - # Install build dependencies 5 - RUN apt-get update && apt-get install -y \ 6 - pkg-config \ 7 - libssl-dev \ 8 - && rm -rf /var/lib/apt/lists/* 9 - 10 - WORKDIR /app 11 - 12 - # Copy manifests 13 - COPY Cargo.toml Cargo.lock ./ 14 - 15 - # Copy source code 16 - COPY src ./src 17 - COPY static ./static 18 - 19 - # Build for release 20 - RUN cargo build --release 21 - 22 - # Runtime stage 23 - FROM debian:bookworm-slim 24 - 25 - # Install runtime dependencies 26 - RUN apt-get update && apt-get install -y \ 27 - ca-certificates \ 28 - libssl3 \ 29 - && rm -rf /var/lib/apt/lists/* 30 - 31 - WORKDIR /app 32 - 33 - # Copy the built binary 34 - COPY --from=builder /app/target/release/at-me /app/at-me 35 - 36 - # Copy static files 37 - COPY --from=builder /app/static /app/static 38 - 39 - # Expose port 40 - EXPOSE 8080 41 - 42 - # Run the binary 43 - CMD ["./at-me"]
···
+21 -6
README.md
··· 1 # @me 2 3 - an accessible visualization of how your atproto identity connects to third-party apps. 4 5 - [at-me.fly.dev](https://at-me.fly.dev/) 6 7 ## what is this 8 9 - in decentralized social networks, you own your identity and your data lives in your personal data server. third-party applications create records in your repository using different lexicons (data schemas). 10 11 - @me shows this visually: your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what record types it stores, then click a record type to view the actual data. 12 13 inspired by [pdsls.dev](https://pdsls.dev). 14 15 ## running locally 16 17 ```bash 18 - cargo run 19 ``` 20 21 - then visit http://localhost:8080 and sign in with any atproto handle.
··· 1 # @me 2 3 + an accessible visualization of how your atproto identity connects to atproto apps. 4 5 + [at-me.wisp.place](https://at-me.wisp.place/) 6 7 ## what is this 8 9 + in decentralized social networks, you own your identity and your data lives in your personal data server. atproto applications create records in your repository using different lexicons (data schemas). 10 11 + @me shows this visually: your identity at the center, surrounded by the atproto apps that have created data for you. click an app to see what record types it stores, then click a record type to view the actual data. 12 13 inspired by [pdsls.dev](https://pdsls.dev). 14 15 ## running locally 16 17 ```bash 18 + bun install 19 + bun run dev 20 ``` 21 22 + visit http://localhost:3030 to explore any atproto handle. 23 + 24 + ## commands 25 + 26 + - `bun run dev` - start dev server with hot reloading 27 + - `bun run build` - build for production (outputs to `dist/`) 28 + - `bun run preview` - preview production build locally 29 + 30 + ## tech stack 31 + 32 + - pure client-side javascript (no backend) 33 + - vite for development and building 34 + - direct atproto API calls (PDS, PLC directory, Bluesky AppView) 35 + - jetstream websocket for live firehose streaming 36 + - client-side MST (merkle search tree) visualization
bun.lockb

This is a binary file and will not be displayed.

+107
docs/_artifacts/COPY_IMPROVEMENTS.md
···
··· 1 + # copy improvements 2 + 3 + ## the problem 4 + 5 + the original copy throughout @me was too technical and jargon-heavy for people unfamiliar with atproto. terms like "silos," "atproto identity," "repository," and "Personal Data Server" appeared without context or explanation. this created barriers for the primary audience: regular social media users who might be curious about decentralized social but don't yet understand the tech. 6 + 7 + more importantly, the copy focused on **how the technology works** rather than **why it matters** to users. people don't care about protocols - they care about not losing their followers when platforms change. 8 + 9 + ## the philosophy 10 + 11 + drawing from [overreacted.io/open-social](https://overreacted.io/open-social/), we adopted these principles: 12 + 13 + 1. **lead with relatable problems** - "built 10k followers? if you leave, you lose them all" 14 + 2. **use familiar analogies** - "what if social media worked like email?" 15 + 3. **focus on benefits, not technology** - "switch apps anytime, take everything with you" 16 + 4. **provide breadcrumbs** - link every technical term to official docs so curious users can learn more 17 + 18 + the key insight: if you can't leave without losing something important, the platform has no incentive to respect you. that's the message that resonates with regular users, not "merkle search trees" or "decentralized identity." 19 + 20 + ## what we changed 21 + 22 + ### logged-out experience (login page "what is this?") 23 + 24 + **before:** 25 + - "visualize your atproto identity" 26 + - "the problem with silos" 27 + - "the atproto solution" 28 + - heavy use of jargon, abstract concepts 29 + 30 + **after:** 31 + - "your posts should be yours" - opens with the actual problem people face 32 + - "what if social media worked like email?" - uses an analogy everyone understands 33 + - "see it in action" - simple call to action 34 + - every technical term links to [atproto.com](https://atproto.com) documentation 35 + 36 + **why:** logged-out users know nothing about atproto. this is our chance to make them care before introducing any technical concepts. 37 + 38 + ### logged-in experience (? button modal) 39 + 40 + **before:** 41 + - "@me - your repository" 42 + - focused on platform switching 43 + - generic language about ownership 44 + 45 + **after:** 46 + - "this is your data" - personal and direct 47 + - explains what they're looking at: "you're looking at your Personal Data Server - where your social data actually lives" 48 + - concrete examples: "bluesky for microblogging. whitewind for long-form posts" 49 + - defines "open social" in plain terms: "if you don't like an app, switch" 50 + - ends with clear instructions on how to use the tool 51 + 52 + **why:** once someone is logged in, they're ready for slightly deeper concepts. but we still prioritize clarity over accuracy, using the visualization to teach what a PDS does. 53 + 54 + ### identity/PDS panel (clicking @ in center) 55 + 56 + **before:** 57 + - title: "your repository" 58 + - subtitle: "what you've built" 59 + - comparison boxes about traditional vs atproto platforms 60 + - technical details at bottom 61 + 62 + **after:** 63 + - title: "your personal data server" 64 + - subtitle: "where your social data lives" 65 + - **"your pds location"** box - explicitly states where the PDS is hosted and what's stored there 66 + - **"explore your data"** box - links to `pdsls.dev/{pds-domain}` as a next step 67 + - removed redundant platform comparison (already covered in modals) 68 + - kept technical details (DID, handle) at bottom 69 + 70 + **why:** this panel should immediately answer "what is this thing in the center?" and "where is my data actually stored?" the pdsls.dev link gives power users an immediate action item. 71 + 72 + ## the pattern 73 + 74 + every piece of copy now follows this structure: 75 + 76 + 1. **hook** - relatable problem or question 77 + 2. **explain** - use familiar analogies 78 + 3. **breadcrumb** - link technical terms to docs 79 + 4. **action** - give them something to do or explore 80 + 81 + examples: 82 + - login page: problem โ†’ email analogy โ†’ linked "Personal Data Server" โ†’ "explore demo" 83 + - info modal: "this is your data" โ†’ concrete examples โ†’ linked "open social" โ†’ "how to explore" 84 + - pds panel: "where your social data lives" โ†’ linked PDS location โ†’ pdsls.dev tool โ†’ technical details 85 + 86 + ## success metrics 87 + 88 + we'll know this worked if: 89 + 90 + 1. **bounce rate decreases** on login page 91 + 2. **demo mode usage increases** (people want to see it work) 92 + 3. **pdsls.dev referrals** show users are exploring further 93 + 4. **fewer confused questions** from new users 94 + 95 + more importantly: can you explain this to your non-technical friend and have them understand why they should care? that's the test. 96 + 97 + ## files modified 98 + 99 + - `src/templates.rs` - login page info section, logged-in info modal 100 + - `static/app.js` - identity/PDS panel on @ click 101 + 102 + ## resources 103 + 104 + - [overreacted.io/open-social](https://overreacted.io/open-social/) - the philosophical foundation 105 + - [atproto.com/guides/data-repos](https://atproto.com/guides/data-repos) - what is a PDS 106 + - [atproto.com](https://atproto.com) - protocol overview 107 + - [pdsls.dev](https://pdsls.dev) - tool for exploring PDS contents
+92
docs/firehose.md
···
··· 1 + # real-time updates via firehose 2 + 3 + at-me visualizes your atproto activity in real-time using the jetstream firehose. 4 + 5 + ## what is the firehose? 6 + 7 + the [atproto firehose](https://docs.bsky.app/docs/advanced-guides/firehose) is a WebSocket stream of all repository events across the network. when you create, update, or delete records in your PDS, these events flow through the firehose. 8 + 9 + we use [jetstream](https://github.com/ericvolp12/jetstream), a more efficient firehose consumer that filters and transforms events. 10 + 11 + ## architecture 12 + 13 + ### backend: rust + server-sent events 14 + 15 + **firehose manager** (`src/firehose.rs`) 16 + - maintains WebSocket connections to jetstream 17 + - one broadcaster per DID being watched 18 + - smart reconnection with exponential backoff 19 + - thread-safe using `tokio` and `Arc<Mutex>` 20 + 21 + **dynamic collection registration** 22 + - when you click "watch live", we fetch your repo's collections via `com.atproto.repo.describeRepo` 23 + - registers event ingesters for ALL collections (not just bluesky) 24 + - this means whitewind, tangled, guestbook, and any future app automatically work 25 + 26 + **event broadcasting** (`src/routes.rs:firehose_watch`) 27 + - server-sent events (SSE) endpoint at `/api/firehose/watch?did=<your-did>` 28 + - filters jetstream events to only those matching your DID and collections 29 + - broadcasts as JSON: `{action, collection, namespace, did, rkey}` 30 + 31 + ### frontend: particles + circles 32 + 33 + **WebSocket to SSE bridge** (`static/app.js`) 34 + - `EventSource` connects to SSE endpoint 35 + - parses incoming events 36 + - creates particle animations 37 + - shows toast notifications 38 + 39 + **particle system** 40 + - creates colored particles (green=create, blue=update, red=delete) 41 + - animates from app circle โ†’ identity (your PDS) 42 + - uses `requestAnimationFrame` for smooth 60fps 43 + - easing with cubic bezier for natural motion 44 + 45 + **dynamic circle management** 46 + - new app? โ†’ `addAppCircle()` creates it on the fly 47 + - delete event? โ†’ `removeAppCircle()` cleans up when particle completes 48 + - circles automatically reposition to maintain even spacing 49 + 50 + ## event flow 51 + 52 + ``` 53 + 1. you create a post in bluesky 54 + 2. bluesky writes to your PDS 55 + 3. your PDS emits event to firehose 56 + 4. jetstream filters and forwards to our backend 57 + 5. backend matches your DID + collection 58 + 6. SSE pushes event to your browser 59 + 7. particle animates from bluesky circle to center 60 + 8. identity pulses when particle arrives 61 + 9. toast shows "created post: hello world..." 62 + ``` 63 + 64 + ## why it works for any app 65 + 66 + traditional approaches hardcode collections like `app.bsky.feed.post`. we don't. 67 + 68 + instead, we: 69 + 1. call `describeRepo` to get YOUR actual collections 70 + 2. register ingesters for everything you have 71 + 3. dynamically create/remove app circles as events flow 72 + 73 + this means if you use: 74 + - whitewind โ†’ see blog posts flow in 75 + - tangled โ†’ see commits flow in 76 + - at-me guestbook โ†’ see signatures flow in 77 + - future apps โ†’ automatically supported 78 + 79 + ## performance notes 80 + 81 + - **caching**: DID resolution cached for 1 hour (`constants::CACHE_TTL`) 82 + - **buffer**: broadcast channel with 100-event buffer 83 + - **reconnection**: 5-second delay between retries 84 + - **cleanup**: connections close when SSE client disconnects 85 + 86 + ## code references 87 + 88 + - firehose manager: `src/firehose.rs` 89 + - SSE endpoint: `src/routes.rs:951` (`firehose_watch`) 90 + - dynamic registration: `src/routes.rs:985` (fetch collections via `describeRepo`) 91 + - particle animation: `static/app.js:1037` (`animateFirehoseParticles`) 92 + - circle lifecycle: `static/app.js:1419` (`addAppCircle`), `static/app.js:1646` (`removeAppCircle`)
+51
docs/lexicon.md
···
··· 1 + # lexicon 2 + 3 + ## `app.at-me.visit` 4 + 5 + **status**: unofficial, experimental 6 + 7 + this is the record type created when users opt-in to "sign the guestbook" on at-me. 8 + 9 + ### namespace rationale 10 + 11 + we use `app.at-me.visit` rather than a domain-based namespace (like `io.zzstoatzz.*`) because: 12 + 13 + 1. the app is hosted at `at-me.fly.dev`, not under a domain we control 14 + 2. using a personal domain namespace would incorrectly suggest this is an official/owned lexicon 15 + 3. `app.at-me.*` clearly associates records with this specific application 16 + 17 + this is an **unofficial lexicon** - there is no formal schema definition served at a URL. it's a simple, unvalidated record type for analytics/engagement tracking. 18 + 19 + ### record structure 20 + 21 + ```json 22 + { 23 + "$type": "app.at-me.visit", 24 + "timestamp": "2025-10-25T22:30:00Z", 25 + "createdAt": "2025-10-25T22:30:00Z", 26 + "text": "optional message from the visitor" 27 + } 28 + ``` 29 + 30 + **fields:** 31 + - `timestamp` (required): ISO 8601 timestamp of when the signature was created 32 + - `createdAt` (required): ISO 8601 timestamp of when the record was created (typically same as timestamp) 33 + - `text` (optional): a message left by the visitor, max 280 characters 34 + 35 + ### privacy 36 + 37 + - users must explicitly authenticate and click "sign guestbook" to create these records 38 + - records are written to the user's own PDS, which they control 39 + - the app does not store or aggregate this data 40 + - users can delete these records at any time through their PDS 41 + 42 + ### philosophy 43 + 44 + this approach aligns with atproto's principles: 45 + - user data sovereignty (records live in user's PDS) 46 + - transparency (users see exactly what's being written) 47 + - opt-in participation (no tracking without explicit consent) 48 + 49 + ### acknowledgments 50 + 51 + thanks to [@thisismissem.social](https://bsky.app/profile/thisismissem.social) for putting [lexicon-guestbook](https://github.com/FujoWebDev/lexicon-guestbook) on our radar! [@essentialrandom.bsky.social](https://bsky.app/profile/essentialrandom.bsky.social)'s work on that project - a more fully-featured implementation with per-user guestbooks, moderation, and an appview - helped inform the addition of optional text messages to our simpler global guestbook.
+27
docs/oauth.md
···
··· 1 + # oauth 2 + 3 + at-me uses atproto oauth for authentication. 4 + 5 + ## flow 6 + 7 + 1. user enters handle on landing page 8 + 2. app resolves handle โ†’ DID โ†’ authorization server via did document 9 + 3. authorization server redirects to user's pds for consent 10 + 4. user approves, receives redirect back with auth code 11 + 5. app exchanges code for access token 12 + 6. token stored in session, used for authenticated api calls 13 + 14 + ## scopes 15 + 16 + ```rust 17 + Scope::Known(KnownScope::Atproto), 18 + Scope::Unknown("repo:app.at-me.visit".to_string()), 19 + ``` 20 + 21 + the granular scope `repo:app.at-me.visit` limits write access to only guestbook records. 22 + 23 + ## session management 24 + 25 + sessions use actix-web's cookie-based session middleware. authenticated agents cached in-memory by DID for performance (`AGENT_CACHE`). 26 + 27 + see `src/oauth.rs` for implementation.
-20
fly.toml
··· 1 - app = "at-me" 2 - primary_region = "ord" 3 - 4 - [build] 5 - 6 - [env] 7 - OAUTH_REDIRECT_URI = "https://at-me.fly.dev/oauth/callback" 8 - 9 - [http_service] 10 - internal_port = 8080 11 - force_https = true 12 - auto_stop_machines = "suspend" 13 - auto_start_machines = true 14 - min_machines_running = 0 15 - processes = ["app"] 16 - 17 - [[vm]] 18 - memory = "256mb" 19 - cpu_kind = "shared" 20 - cpus = 1
···
+70
index.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>@me - explore your atproto identity</title> 7 + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 8 + 9 + <!-- Open Graph / Facebook --> 10 + <meta property="og:type" content="website"> 11 + <meta property="og:url" content="https://at-me.wisp.place/"> 12 + <meta property="og:title" content="@me - explore your atproto identity"> 13 + <meta property="og:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 14 + <meta property="og:image" content="https://at-me.wisp.place/og-image.png"> 15 + 16 + <!-- Twitter --> 17 + <meta property="twitter:card" content="summary_large_image"> 18 + <meta property="twitter:url" content="https://at-me.wisp.place/"> 19 + <meta property="twitter:title" content="@me - explore your atproto identity"> 20 + <meta property="twitter:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 21 + <meta property="twitter:image" content="https://at-me.wisp.place/og-image.png"> 22 + </head> 23 + <body> 24 + <div class="atmosphere" id="atmosphere"></div> 25 + 26 + <div class="container"> 27 + <div class="search-card"> 28 + <h1>@me</h1> 29 + <div class="subtitle">explore the atmosphere</div> 30 + <form id="searchForm" onsubmit="event.preventDefault(); handleSearch();"> 31 + <div class="input-wrapper"> 32 + <input type="text" id="handleInput" placeholder="enter any handle" autofocus autocomplete="off" autocapitalize="off" spellcheck="false"> 33 + <span class="search-spinner" id="searchSpinner" style="display: none;">...</span> 34 + <div class="autocomplete-results" id="autocompleteResults"></div> 35 + </div> 36 + <button type="submit">explore</button> 37 + </form> 38 + 39 + <div class="divider">try these</div> 40 + <div class="suggestions"> 41 + <button class="suggestion-btn" onclick="viewHandle('why.bsky.team')">why.bsky.team</button> 42 + <button class="suggestion-btn" onclick="viewHandle('baileytownsend.dev')">baileytownsend.dev</button> 43 + <button class="suggestion-btn" onclick="viewHandle('bad-example.com')">bad-example.com</button> 44 + <button class="suggestion-btn" onclick="viewHandle('void.comind.network')">void.comind.network</button> 45 + </div> 46 + 47 + <button type="button" class="info-toggle" onclick="toggleInfo()">what is this?</button> 48 + 49 + <div class="info-content" id="infoContent"> 50 + <div class="info-section"> 51 + <h3>you should own your data</h3> 52 + <p>today's social platforms own your data. built 10k followers? wrote years of posts? if you leave, you lose it all. you don't own your network - they do.</p> 53 + 54 + <h3>what if social media worked like email?</h3> 55 + <p>with email, you can switch from gmail to protonmail and keep your contacts. same idea here: your posts and followers live on your own server (called a <a href="https://atproto.com/guides/data-repos" target="_blank" rel="noopener noreferrer">Personal Data Server</a>). apps like <a href="https://bsky.app" target="_blank" rel="noopener noreferrer">bluesky</a> just connect to it.</p> 56 + 57 + <h3>see it in action</h3> 58 + <p>enter any handle above to see how <a href="https://atproto.com" target="_blank" rel="noopener noreferrer">atproto</a> stores their social data.</p> 59 + </div> 60 + </div> 61 + </div> 62 + </div> 63 + 64 + <div class="footer"> 65 + by <a href="https://bsky.app/profile/zzstoatzz.io" target="_blank" rel="noopener noreferrer">@zzstoatzz.io</a> 66 + </div> 67 + 68 + <script type="module" src="/src/landing/main.js"></script> 69 + </body> 70 + </html>
-7
justfile
··· 1 - # deploy to fly.io 2 - deploy: 3 - fly deploy 4 - 5 - # run locally 6 - dev: 7 - cargo run
···
+19
package.json
···
··· 1 + { 2 + "name": "at-me", 3 + "version": "1.0.0", 4 + "description": "ATProto PDS visualization tool", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "vite build", 9 + "preview": "vite preview" 10 + }, 11 + "dependencies": { 12 + "@atcute/client": "^2.0.0", 13 + "@atcute/oauth-browser-client": "^1.0.0", 14 + "@skyware/firehose": "^0.3.0" 15 + }, 16 + "devDependencies": { 17 + "vite": "^6.0.0" 18 + } 19 + }
+1
public/_redirects
···
··· 1 + # Redirects handled by view/index.html directory structure
+4
public/favicon.svg
···
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> 2 + <rect width="100" height="100" fill="#0a0a0a"/> 3 + <text x="50" y="75" font-family="monospace" font-size="70" font-weight="600" fill="#e0e0e0" text-anchor="middle">@</text> 4 + </svg>
+12
public/oauth-client-metadata.json
···
··· 1 + { 2 + "client_id": "https://at-me.fly.dev/oauth-client-metadata.json", 3 + "client_name": "at-me", 4 + "client_uri": "https://at-me.fly.dev", 5 + "redirect_uris": ["https://at-me.fly.dev/app.html"], 6 + "scope": "atproto transition:generic", 7 + "grant_types": ["authorization_code", "refresh_token"], 8 + "response_types": ["code"], 9 + "token_endpoint_auth_method": "none", 10 + "application_type": "web", 11 + "dpop_bound_access_tokens": true 12 + }
public/og-image.png

This is a binary file and will not be displayed.

+338
src/landing/main.js
···
··· 1 + // ============================================================================ 2 + // LANDING PAGE - Main Entry Point 3 + // ============================================================================ 4 + 5 + import './styles.css'; 6 + 7 + // ============================================================================ 8 + // ATPROTO UTILITIES 9 + // ============================================================================ 10 + 11 + // Resolve a handle to a DID 12 + async function resolveHandle(handle) { 13 + try { 14 + const response = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); 15 + if (response.ok) { 16 + const data = await response.json(); 17 + return data.did; 18 + } 19 + } catch (e) { 20 + console.error('Failed to resolve handle via Bluesky:', e); 21 + } 22 + return null; 23 + } 24 + 25 + // Get profile from Bluesky AppView 26 + async function getProfile(handleOrDid) { 27 + try { 28 + const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`); 29 + if (response.ok) { 30 + return await response.json(); 31 + } 32 + } catch (e) { 33 + console.error('Failed to get profile:', e); 34 + } 35 + return null; 36 + } 37 + 38 + // Search for handles using Bluesky's search 39 + async function searchHandles(query) { 40 + try { 41 + const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=8`); 42 + if (response.ok) { 43 + const data = await response.json(); 44 + return data.actors.map(actor => ({ 45 + handle: actor.handle, 46 + displayName: actor.displayName || actor.handle, 47 + avatarUrl: actor.avatar || null 48 + })); 49 + } 50 + } catch (e) { 51 + console.error('Search failed:', e); 52 + } 53 + return []; 54 + } 55 + 56 + // Get app avatar from the namespace's well-known profile 57 + async function getAppAvatar(namespace) { 58 + const domain = namespace.split('.').reverse().join('.'); 59 + try { 60 + const profile = await getProfile(domain); 61 + if (profile && profile.avatar) { 62 + return profile.avatar; 63 + } 64 + } catch (e) { 65 + // Silently fail 66 + } 67 + return null; 68 + } 69 + 70 + // Batch fetch app avatars 71 + async function fetchAppAvatars(namespaces) { 72 + const avatars = {}; 73 + const promises = namespaces.map(async (ns) => { 74 + const avatar = await getAppAvatar(ns); 75 + if (avatar) { 76 + avatars[ns] = avatar; 77 + } 78 + }); 79 + await Promise.all(promises); 80 + return avatars; 81 + } 82 + 83 + // ============================================================================ 84 + // SEARCH & NAVIGATION 85 + // ============================================================================ 86 + 87 + let searchTimeout = null; 88 + let autocompleteResults = []; 89 + 90 + function handleSearch() { 91 + const handle = document.getElementById('handleInput').value.trim(); 92 + if (handle) { 93 + viewHandle(handle); 94 + } 95 + } 96 + 97 + function viewHandle(handle) { 98 + window.location.href = `./view/?handle=${encodeURIComponent(handle)}`; 99 + } 100 + 101 + function toggleInfo() { 102 + document.getElementById('infoContent').classList.toggle('expanded'); 103 + } 104 + 105 + // Expose to window for onclick handlers 106 + window.handleSearch = handleSearch; 107 + window.viewHandle = viewHandle; 108 + window.toggleInfo = toggleInfo; 109 + 110 + // ============================================================================ 111 + // AUTOCOMPLETE 112 + // ============================================================================ 113 + 114 + function escapeHtml(text) { 115 + const div = document.createElement('div'); 116 + div.textContent = text; 117 + return div.innerHTML; 118 + } 119 + 120 + function hideResults() { 121 + document.getElementById('autocompleteResults').classList.remove('show'); 122 + } 123 + 124 + function selectHandle(handle) { 125 + document.getElementById('handleInput').value = handle; 126 + autocompleteResults = []; 127 + hideResults(); 128 + viewHandle(handle); 129 + } 130 + 131 + // Expose to window for onclick handlers 132 + window.selectHandle = selectHandle; 133 + 134 + async function doSearch(query) { 135 + const spinner = document.getElementById('searchSpinner'); 136 + 137 + if (query.length < 2) { 138 + autocompleteResults = []; 139 + hideResults(); 140 + return; 141 + } 142 + 143 + spinner.style.display = 'block'; 144 + 145 + try { 146 + autocompleteResults = await searchHandles(query); 147 + renderResults(); 148 + } catch (e) { 149 + console.error('search failed:', e); 150 + } finally { 151 + spinner.style.display = 'none'; 152 + } 153 + } 154 + 155 + function renderResults() { 156 + const resultsDiv = document.getElementById('autocompleteResults'); 157 + 158 + if (autocompleteResults.length === 0) { 159 + hideResults(); 160 + return; 161 + } 162 + 163 + resultsDiv.innerHTML = autocompleteResults.map(result => ` 164 + <button type="button" class="autocomplete-item" onclick="selectHandle('${result.handle}')"> 165 + ${result.avatarUrl 166 + ? `<img src="${result.avatarUrl}" alt="" class="autocomplete-avatar">` 167 + : `<div class="autocomplete-avatar-placeholder">${result.handle[0].toUpperCase()}</div>` 168 + } 169 + <div class="autocomplete-info"> 170 + <div class="autocomplete-name">${escapeHtml(result.displayName)}</div> 171 + <div class="autocomplete-handle">@${escapeHtml(result.handle)}</div> 172 + </div> 173 + </button> 174 + `).join(''); 175 + 176 + resultsDiv.classList.add('show'); 177 + } 178 + 179 + function initAutocomplete() { 180 + const handleInput = document.getElementById('handleInput'); 181 + const resultsDiv = document.getElementById('autocompleteResults'); 182 + 183 + handleInput.addEventListener('input', () => { 184 + if (searchTimeout) clearTimeout(searchTimeout); 185 + searchTimeout = setTimeout(() => doSearch(handleInput.value), 300); 186 + }); 187 + 188 + handleInput.addEventListener('keydown', (e) => { 189 + if (e.key === 'Escape') { 190 + hideResults(); 191 + } 192 + }); 193 + 194 + handleInput.addEventListener('focus', () => { 195 + if (autocompleteResults.length > 0) { 196 + resultsDiv.classList.add('show'); 197 + } 198 + }); 199 + 200 + document.addEventListener('click', (e) => { 201 + if (!e.target.closest('.input-wrapper')) { 202 + hideResults(); 203 + } 204 + }); 205 + } 206 + 207 + // ============================================================================ 208 + // ATMOSPHERE VISUALIZATION 209 + // ============================================================================ 210 + 211 + async function fetchAtmosphere() { 212 + const CACHE_KEY = 'atme_atmosphere'; 213 + const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours 214 + 215 + const cached = localStorage.getItem(CACHE_KEY); 216 + if (cached) { 217 + const { data, timestamp } = JSON.parse(cached); 218 + if (Date.now() - timestamp < CACHE_DURATION) { 219 + return data; 220 + } 221 + } 222 + 223 + try { 224 + const response = await fetch('https://ufos-api.microcosm.blue/collections?order=dids-estimate&limit=50'); 225 + const json = await response.json(); 226 + 227 + // Group by namespace (first two segments) 228 + const namespaces = {}; 229 + json.collections.forEach(col => { 230 + const parts = col.nsid.split('.'); 231 + if (parts.length >= 2) { 232 + const ns = `${parts[0]}.${parts[1]}`; 233 + if (!namespaces[ns]) { 234 + namespaces[ns] = { 235 + namespace: ns, 236 + dids_total: 0, 237 + records_total: 0, 238 + collections: [] 239 + }; 240 + } 241 + namespaces[ns].dids_total += col.dids_estimate; 242 + namespaces[ns].records_total += col.creates; 243 + namespaces[ns].collections.push(col.nsid); 244 + } 245 + }); 246 + 247 + const data = Object.values(namespaces).sort((a, b) => b.dids_total - a.dids_total).slice(0, 30); 248 + 249 + localStorage.setItem(CACHE_KEY, JSON.stringify({ 250 + data, 251 + timestamp: Date.now() 252 + })); 253 + 254 + return data; 255 + } catch (e) { 256 + console.error('Failed to fetch atmosphere data:', e); 257 + return []; 258 + } 259 + } 260 + 261 + async function renderAtmosphere() { 262 + const data = await fetchAtmosphere(); 263 + if (!data.length) return; 264 + 265 + const atmosphere = document.getElementById('atmosphere'); 266 + const maxSize = Math.max(...data.map(d => d.dids_total)); 267 + 268 + const namespaces = data.map(app => app.namespace); 269 + const avatarPromise = fetchAppAvatars(namespaces); 270 + const orbRegistry = []; 271 + 272 + data.forEach((app, i) => { 273 + const orb = document.createElement('div'); 274 + orb.className = 'app-orb'; 275 + 276 + // Size based on user count (20-80px) 277 + const size = 20 + (app.dids_total / maxSize) * 60; 278 + 279 + // Position in 3D space 280 + const angle = (i / data.length) * Math.PI * 2; 281 + const radius = 250 + (i % 3) * 100; 282 + const y = (i % 5) * 80 - 160; 283 + const x = Math.cos(angle) * radius; 284 + const z = Math.sin(angle) * radius; 285 + 286 + orb.style.width = `${size}px`; 287 + orb.style.height = `${size}px`; 288 + orb.style.left = `calc(50% + ${x}px)`; 289 + orb.style.top = `calc(50% + ${y}px)`; 290 + orb.style.transform = `translateZ(${z}px) translate(-50%, -50%)`; 291 + orb.style.background = `radial-gradient(circle, rgba(255,255,255,0.1), rgba(255,255,255,0.02))`; 292 + orb.style.border = '1px solid rgba(255,255,255,0.1)'; 293 + orb.style.boxShadow = '0 0 20px rgba(255,255,255,0.1)'; 294 + 295 + // Fallback letter 296 + const letter = app.namespace.split('.')[1]?.[0]?.toUpperCase() || app.namespace[0].toUpperCase(); 297 + orb.innerHTML = `<div class="fallback">${letter}</div>`; 298 + 299 + // Tooltip 300 + const tooltip = document.createElement('div'); 301 + tooltip.className = 'app-tooltip'; 302 + const users = app.dids_total >= 1000000 303 + ? `${(app.dids_total / 1000000).toFixed(1)}M users` 304 + : `${(app.dids_total / 1000).toFixed(0)}K users`; 305 + tooltip.textContent = `${app.namespace} - ${users}`; 306 + orb.appendChild(tooltip); 307 + 308 + atmosphere.appendChild(orb); 309 + 310 + orbRegistry.push({ orb, tooltip, namespace: app.namespace }); 311 + }); 312 + 313 + avatarPromise.then(avatarMap => { 314 + orbRegistry.forEach(({ orb, tooltip, namespace }) => { 315 + const avatarUrl = avatarMap[namespace]; 316 + if (avatarUrl) { 317 + orb.innerHTML = `<img src="${avatarUrl}" alt="${namespace}" />`; 318 + orb.appendChild(tooltip); 319 + } 320 + }); 321 + }); 322 + } 323 + 324 + // ============================================================================ 325 + // INITIALIZATION 326 + // ============================================================================ 327 + 328 + function init() { 329 + initAutocomplete(); 330 + renderAtmosphere(); 331 + } 332 + 333 + // Start when DOM is ready 334 + if (document.readyState === 'loading') { 335 + document.addEventListener('DOMContentLoaded', init); 336 + } else { 337 + init(); 338 + }
+420
src/landing/styles.css
···
··· 1 + /* Landing page styles */ 2 + 3 + * { margin: 0; padding: 0; box-sizing: border-box; } 4 + 5 + body { 6 + font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 7 + min-height: 100vh; 8 + background: radial-gradient(ellipse at center, #0a0a0f 0%, #000000 100%); 9 + color: #e5e5e5; 10 + overflow: hidden; 11 + perspective: 1000px; 12 + } 13 + 14 + @media (max-width: 768px) { 15 + body { 16 + overflow-y: auto; 17 + overflow-x: hidden; 18 + } 19 + } 20 + 21 + .atmosphere { 22 + position: fixed; 23 + inset: 0; 24 + transform-style: preserve-3d; 25 + animation: rotate 120s infinite linear; 26 + } 27 + 28 + @keyframes rotate { 29 + from { transform: rotateY(0deg); } 30 + to { transform: rotateY(360deg); } 31 + } 32 + 33 + .app-orb { 34 + position: absolute; 35 + border-radius: 50%; 36 + display: flex; 37 + align-items: center; 38 + justify-content: center; 39 + transition: all 0.3s ease; 40 + cursor: pointer; 41 + backdrop-filter: blur(4px); 42 + } 43 + 44 + .app-orb:hover { 45 + transform: scale(1.2) !important; 46 + z-index: 100; 47 + } 48 + 49 + .app-orb img { 50 + width: 100%; 51 + height: 100%; 52 + border-radius: 50%; 53 + object-fit: cover; 54 + } 55 + 56 + .app-orb .fallback { 57 + font-size: 1.5rem; 58 + font-weight: 600; 59 + color: rgba(255, 255, 255, 0.9); 60 + } 61 + 62 + .app-tooltip { 63 + position: absolute; 64 + background: rgba(10, 10, 15, 0.95); 65 + border: 1px solid rgba(255, 255, 255, 0.1); 66 + padding: 0.5rem 0.75rem; 67 + border-radius: 4px; 68 + font-size: 0.7rem; 69 + white-space: nowrap; 70 + pointer-events: none; 71 + opacity: 0; 72 + transition: opacity 0.2s; 73 + z-index: 1000; 74 + } 75 + 76 + .app-orb:hover .app-tooltip { 77 + opacity: 1; 78 + } 79 + 80 + .container { 81 + position: fixed; 82 + inset: 0; 83 + display: flex; 84 + align-items: center; 85 + justify-content: center; 86 + z-index: 10; 87 + } 88 + 89 + @media (max-width: 768px) { 90 + .container { 91 + position: relative; 92 + min-height: 100vh; 93 + padding: 2rem 0; 94 + } 95 + } 96 + 97 + .search-card { 98 + background: transparent; 99 + border: 1px solid rgba(255, 255, 255, 0.1); 100 + padding: 2.5rem 3rem; 101 + border-radius: 8px; 102 + backdrop-filter: blur(2px); 103 + text-align: center; 104 + max-width: min(500px, 90vw); 105 + } 106 + 107 + h1 { 108 + font-size: 2rem; 109 + margin-bottom: 0.5rem; 110 + font-weight: 300; 111 + letter-spacing: 0.05em; 112 + } 113 + 114 + .subtitle { 115 + font-size: 0.75rem; 116 + color: rgba(255, 255, 255, 0.5); 117 + margin-bottom: 2rem; 118 + } 119 + 120 + input { 121 + font-family: inherit; 122 + font-size: 0.9rem; 123 + padding: 0.75rem 1rem; 124 + margin-bottom: 1rem; 125 + background: rgba(10, 10, 15, 0.8); 126 + border: 1px solid rgba(255, 255, 255, 0.2); 127 + border-radius: 4px; 128 + color: #e5e5e5; 129 + width: 100%; 130 + transition: all 0.2s; 131 + } 132 + 133 + input:focus { 134 + outline: none; 135 + border-color: rgba(255, 255, 255, 0.4); 136 + background: rgba(10, 10, 15, 0.9); 137 + } 138 + 139 + input::placeholder { 140 + color: rgba(255, 255, 255, 0.3); 141 + } 142 + 143 + .input-wrapper { 144 + position: relative; 145 + width: 100%; 146 + } 147 + 148 + .autocomplete-results { 149 + position: absolute; 150 + z-index: 100; 151 + width: 100%; 152 + max-height: 240px; 153 + overflow-y: auto; 154 + background: rgba(10, 10, 15, 0.98); 155 + border: 1px solid rgba(255, 255, 255, 0.2); 156 + border-radius: 4px; 157 + margin-top: 0.25rem; 158 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); 159 + display: none; 160 + scrollbar-width: thin; 161 + scrollbar-color: rgba(255, 255, 255, 0.2) rgba(10, 10, 15, 0.5); 162 + } 163 + 164 + .autocomplete-results::-webkit-scrollbar { 165 + width: 8px; 166 + } 167 + 168 + .autocomplete-results::-webkit-scrollbar-track { 169 + background: rgba(10, 10, 15, 0.5); 170 + border-radius: 4px; 171 + } 172 + 173 + .autocomplete-results::-webkit-scrollbar-thumb { 174 + background: rgba(255, 255, 255, 0.2); 175 + border-radius: 4px; 176 + } 177 + 178 + .autocomplete-results::-webkit-scrollbar-thumb:hover { 179 + background: rgba(255, 255, 255, 0.3); 180 + } 181 + 182 + .autocomplete-results.show { 183 + display: block; 184 + } 185 + 186 + .autocomplete-item { 187 + width: 100%; 188 + display: flex; 189 + align-items: center; 190 + gap: 0.75rem; 191 + padding: 0.75rem; 192 + background: transparent; 193 + border: none; 194 + border-bottom: 1px solid rgba(255, 255, 255, 0.1); 195 + color: #e5e5e5; 196 + text-align: left; 197 + font-family: inherit; 198 + cursor: pointer; 199 + transition: background 0.15s; 200 + } 201 + 202 + .autocomplete-item:last-child { 203 + border-bottom: none; 204 + } 205 + 206 + .autocomplete-item:hover { 207 + background: rgba(255, 255, 255, 0.1); 208 + } 209 + 210 + .autocomplete-avatar { 211 + width: 36px; 212 + height: 36px; 213 + border-radius: 50%; 214 + object-fit: cover; 215 + border: 1px solid rgba(255, 255, 255, 0.2); 216 + flex-shrink: 0; 217 + } 218 + 219 + .autocomplete-avatar-placeholder { 220 + width: 36px; 221 + height: 36px; 222 + border-radius: 50%; 223 + background: rgba(255, 255, 255, 0.1); 224 + flex-shrink: 0; 225 + display: flex; 226 + align-items: center; 227 + justify-content: center; 228 + font-size: 0.9rem; 229 + color: rgba(255, 255, 255, 0.5); 230 + } 231 + 232 + .autocomplete-info { 233 + flex: 1; 234 + min-width: 0; 235 + overflow: hidden; 236 + } 237 + 238 + .autocomplete-name { 239 + font-weight: 500; 240 + color: rgba(255, 255, 255, 0.9); 241 + margin-bottom: 0.125rem; 242 + overflow: hidden; 243 + text-overflow: ellipsis; 244 + white-space: nowrap; 245 + font-size: 0.85rem; 246 + } 247 + 248 + .autocomplete-handle { 249 + font-size: 0.75rem; 250 + color: rgba(255, 255, 255, 0.5); 251 + overflow: hidden; 252 + text-overflow: ellipsis; 253 + white-space: nowrap; 254 + } 255 + 256 + .search-spinner { 257 + position: absolute; 258 + right: 0.75rem; 259 + top: 50%; 260 + transform: translateY(-50%); 261 + color: rgba(255, 255, 255, 0.4); 262 + font-size: 0.75rem; 263 + } 264 + 265 + button { 266 + font-family: inherit; 267 + font-size: 0.9rem; 268 + padding: 0.75rem 2rem; 269 + cursor: pointer; 270 + background: rgba(10, 10, 15, 0.8); 271 + border: 1px solid rgba(255, 255, 255, 0.2); 272 + border-radius: 4px; 273 + color: #e5e5e5; 274 + transition: all 0.2s; 275 + width: 100%; 276 + } 277 + 278 + button:hover { 279 + background: rgba(10, 10, 15, 0.9); 280 + border-color: rgba(255, 255, 255, 0.4); 281 + } 282 + 283 + .divider { 284 + display: flex; 285 + align-items: center; 286 + gap: 1rem; 287 + margin: 1.5rem 0 1rem; 288 + color: rgba(255, 255, 255, 0.3); 289 + font-size: 0.7rem; 290 + } 291 + 292 + .divider::before, 293 + .divider::after { 294 + content: ''; 295 + flex: 1; 296 + height: 1px; 297 + background: rgba(255, 255, 255, 0.1); 298 + } 299 + 300 + .suggestions { 301 + display: flex; 302 + gap: 0.75rem; 303 + flex-wrap: wrap; 304 + justify-content: center; 305 + } 306 + 307 + .suggestion-btn { 308 + font-family: inherit; 309 + font-size: 0.8rem; 310 + padding: 0.5rem 1rem; 311 + cursor: pointer; 312 + background: transparent; 313 + border: 1px solid rgba(255, 255, 255, 0.15); 314 + border-radius: 4px; 315 + color: rgba(255, 255, 255, 0.6); 316 + transition: all 0.2s; 317 + width: auto; 318 + } 319 + 320 + .suggestion-btn:hover { 321 + background: rgba(10, 10, 15, 0.5); 322 + border-color: rgba(255, 255, 255, 0.3); 323 + color: rgba(255, 255, 255, 0.8); 324 + } 325 + 326 + .info-toggle { 327 + margin-top: 1.5rem; 328 + color: rgba(255, 255, 255, 0.5); 329 + font-size: 0.75rem; 330 + cursor: pointer; 331 + border: none; 332 + background: none; 333 + padding: 0.5rem; 334 + transition: color 0.2s; 335 + text-decoration: underline; 336 + text-underline-offset: 2px; 337 + } 338 + 339 + .info-toggle:hover { 340 + color: rgba(255, 255, 255, 0.8); 341 + } 342 + 343 + .info-content { 344 + max-height: 0; 345 + overflow: hidden; 346 + transition: max-height 0.3s ease; 347 + margin-top: 1rem; 348 + } 349 + 350 + .info-content.expanded { 351 + max-height: 500px; 352 + overflow-y: auto; 353 + } 354 + 355 + @media (max-width: 768px) { 356 + .info-content.expanded { 357 + max-height: none; 358 + overflow-y: visible; 359 + } 360 + } 361 + 362 + .info-section { 363 + background: rgba(10, 10, 15, 0.6); 364 + border: 1px solid rgba(255, 255, 255, 0.1); 365 + border-radius: 4px; 366 + padding: 1.5rem; 367 + text-align: left; 368 + } 369 + 370 + .info-section h3 { 371 + font-size: 0.85rem; 372 + font-weight: 500; 373 + margin-bottom: 0.75rem; 374 + color: rgba(255, 255, 255, 0.9); 375 + } 376 + 377 + .info-section p { 378 + font-size: 0.7rem; 379 + line-height: 1.6; 380 + color: rgba(255, 255, 255, 0.6); 381 + margin-bottom: 1rem; 382 + } 383 + 384 + .info-section p:last-child { 385 + margin-bottom: 0; 386 + } 387 + 388 + .info-section strong { 389 + color: rgba(255, 255, 255, 0.85); 390 + } 391 + 392 + .info-section a { 393 + color: rgba(255, 255, 255, 0.8); 394 + text-decoration: underline; 395 + text-underline-offset: 2px; 396 + } 397 + 398 + .info-section a:hover { 399 + color: rgba(255, 255, 255, 1); 400 + } 401 + 402 + .footer { 403 + position: fixed; 404 + bottom: 1rem; 405 + left: 50%; 406 + transform: translateX(-50%); 407 + font-size: 0.7rem; 408 + color: rgba(255, 255, 255, 0.3); 409 + z-index: 20; 410 + } 411 + 412 + .footer a { 413 + color: rgba(255, 255, 255, 0.5); 414 + text-decoration: none; 415 + transition: color 0.2s; 416 + } 417 + 418 + .footer a:hover { 419 + color: rgba(255, 255, 255, 0.8); 420 + }
-45
src/main.rs
··· 1 - use actix_session::{SessionMiddleware, config::PersistentSession, storage::CookieSessionStore}; 2 - use actix_web::{App, HttpServer, cookie::{Key, time::Duration}, middleware, web}; 3 - use actix_files::Files; 4 - 5 - mod oauth; 6 - mod routes; 7 - mod templates; 8 - 9 - #[actix_web::main] 10 - async fn main() -> std::io::Result<()> { 11 - env_logger::init(); 12 - 13 - let client = oauth::create_oauth_client(); 14 - 15 - println!("starting server at http://localhost:8080"); 16 - 17 - HttpServer::new(move || { 18 - App::new() 19 - .wrap(middleware::Logger::default()) 20 - .wrap( 21 - SessionMiddleware::builder( 22 - CookieSessionStore::default(), 23 - Key::from(&[0; 64]), 24 - ) 25 - .cookie_secure(false) 26 - .session_lifecycle( 27 - PersistentSession::default() 28 - .session_ttl(Duration::days(30)) 29 - ) 30 - .build(), 31 - ) 32 - .app_data(web::Data::new(client.clone())) 33 - .service(routes::index) 34 - .service(routes::login) 35 - .service(routes::callback) 36 - .service(routes::client_metadata) 37 - .service(routes::logout) 38 - .service(routes::restore_session) 39 - .service(routes::favicon) 40 - .service(Files::new("/static", "./static")) 41 - }) 42 - .bind(("0.0.0.0", 8080))? 43 - .run() 44 - .await 45 - }
···
-100
src/oauth.rs
··· 1 - use atrium_identity::{ 2 - did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}, 3 - handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 4 - }; 5 - use atrium_oauth::{ 6 - AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, DefaultHttpClient, 7 - GrantType, KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope, 8 - store::{session::MemorySessionStore, state::MemoryStateStore}, 9 - }; 10 - use hickory_resolver::{TokioAsyncResolver, config::{ResolverConfig, ResolverOpts}}; 11 - use std::sync::Arc; 12 - 13 - #[derive(Clone)] 14 - pub struct HickoryDnsResolver(Arc<TokioAsyncResolver>); 15 - 16 - impl DnsTxtResolver for HickoryDnsResolver { 17 - async fn resolve( 18 - &self, 19 - domain: &str, 20 - ) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> { 21 - Ok(self 22 - .0 23 - .txt_lookup(domain) 24 - .await? 25 - .iter() 26 - .map(|txt| txt.to_string()) 27 - .collect()) 28 - } 29 - } 30 - 31 - pub type OAuthClientType = Arc< 32 - OAuthClient< 33 - MemoryStateStore, 34 - MemorySessionStore, 35 - CommonDidResolver<DefaultHttpClient>, 36 - AtprotoHandleResolver<HickoryDnsResolver, DefaultHttpClient>, 37 - >, 38 - >; 39 - 40 - pub fn create_oauth_client() -> OAuthClientType { 41 - let http_client = Arc::new(DefaultHttpClient::default()); 42 - let dns_resolver = HickoryDnsResolver(Arc::new( 43 - TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()), 44 - )); 45 - 46 - let redirect_uri = std::env::var("OAUTH_REDIRECT_URI") 47 - .unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string()); 48 - 49 - let is_production = redirect_uri.starts_with("https://"); 50 - 51 - let resolver = OAuthResolverConfig { 52 - did_resolver: CommonDidResolver::new(CommonDidResolverConfig { 53 - plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 54 - http_client: http_client.clone(), 55 - }), 56 - handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 57 - dns_txt_resolver: dns_resolver, 58 - http_client: http_client.clone(), 59 - }), 60 - authorization_server_metadata: Default::default(), 61 - protected_resource_metadata: Default::default(), 62 - }; 63 - 64 - if is_production { 65 - let base_url = redirect_uri.trim_end_matches("/oauth/callback"); 66 - Arc::new( 67 - OAuthClient::new(OAuthClientConfig { 68 - client_metadata: AtprotoClientMetadata { 69 - client_id: format!("{}/oauth-client-metadata.json", base_url), 70 - client_uri: Some(base_url.to_string()), 71 - redirect_uris: vec![redirect_uri], 72 - token_endpoint_auth_method: AuthMethod::None, 73 - grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], 74 - scopes: vec![Scope::Known(KnownScope::Atproto)], 75 - jwks_uri: None, 76 - token_endpoint_auth_signing_alg: None, 77 - }, 78 - keys: None, 79 - resolver, 80 - state_store: MemoryStateStore::default(), 81 - session_store: MemorySessionStore::default(), 82 - }) 83 - .expect("failed to create oauth client"), 84 - ) 85 - } else { 86 - Arc::new( 87 - OAuthClient::new(OAuthClientConfig { 88 - client_metadata: AtprotoLocalhostClientMetadata { 89 - redirect_uris: Some(vec![redirect_uri]), 90 - scopes: Some(vec![Scope::Known(KnownScope::Atproto)]), 91 - }, 92 - keys: None, 93 - resolver, 94 - state_store: MemoryStateStore::default(), 95 - session_store: MemorySessionStore::default(), 96 - }) 97 - .expect("failed to create oauth client"), 98 - ) 99 - } 100 - }
···
-153
src/routes.rs
··· 1 - use actix_session::Session; 2 - use actix_web::{get, post, web, HttpResponse, Responder}; 3 - use atrium_oauth::{AuthorizeOptions, CallbackParams, KnownScope, Scope}; 4 - use serde::Deserialize; 5 - 6 - use crate::oauth::OAuthClientType; 7 - use crate::templates; 8 - 9 - const FAVICON_SVG: &str = include_str!("../static/favicon.svg"); 10 - 11 - #[derive(Deserialize)] 12 - pub struct LoginForm { 13 - handle: String, 14 - } 15 - 16 - #[derive(Deserialize)] 17 - pub struct OAuthParams { 18 - state: Option<String>, 19 - iss: Option<String>, 20 - code: Option<String>, 21 - error: Option<String>, 22 - } 23 - 24 - #[get("/")] 25 - pub async fn index(session: Session) -> impl Responder { 26 - let did: Option<String> = session.get("did").unwrap_or(None); 27 - 28 - match did { 29 - Some(did) => HttpResponse::Ok() 30 - .content_type("text/html") 31 - .body(templates::app_page(&did)), 32 - None => HttpResponse::Ok() 33 - .content_type("text/html") 34 - .body(templates::login_page()), 35 - } 36 - } 37 - 38 - #[post("/login")] 39 - pub async fn login( 40 - form: web::Form<LoginForm>, 41 - client: web::Data<OAuthClientType>, 42 - ) -> HttpResponse { 43 - let handle = match atrium_api::types::string::Handle::new(form.handle.clone()) { 44 - Ok(h) => h, 45 - Err(_) => return HttpResponse::BadRequest().body("invalid handle"), 46 - }; 47 - 48 - match client 49 - .authorize( 50 - &handle, 51 - AuthorizeOptions { 52 - scopes: vec![Scope::Known(KnownScope::Atproto)], 53 - ..Default::default() 54 - }, 55 - ) 56 - .await 57 - { 58 - Ok(url) => HttpResponse::SeeOther() 59 - .append_header(("Location", url)) 60 - .finish(), 61 - Err(_) => HttpResponse::InternalServerError().body("oauth error"), 62 - } 63 - } 64 - 65 - #[get("/oauth/callback")] 66 - pub async fn callback( 67 - params: web::Query<OAuthParams>, 68 - client: web::Data<OAuthClientType>, 69 - session: Session, 70 - ) -> HttpResponse { 71 - if let Some(error) = &params.error { 72 - return HttpResponse::BadRequest().body(format!("oauth error: {}", error)); 73 - } 74 - 75 - let code = match &params.code { 76 - Some(c) => c.clone(), 77 - None => return HttpResponse::BadRequest().body("missing code"), 78 - }; 79 - 80 - let callback_params = CallbackParams { 81 - code, 82 - state: params.state.clone(), 83 - iss: params.iss.clone(), 84 - }; 85 - 86 - match client.callback(callback_params).await { 87 - Ok((bsky_session, _)) => { 88 - let agent = atrium_api::agent::Agent::new(bsky_session); 89 - if let Some(did) = agent.did().await { 90 - session.insert("did", did.to_string()).unwrap(); 91 - HttpResponse::SeeOther() 92 - .append_header(("Location", "/")) 93 - .finish() 94 - } else { 95 - HttpResponse::InternalServerError().body("no did") 96 - } 97 - } 98 - Err(e) => HttpResponse::InternalServerError().body(format!("callback error: {}", e)), 99 - } 100 - } 101 - 102 - #[get("/oauth-client-metadata.json")] 103 - pub async fn client_metadata() -> HttpResponse { 104 - let base_url = std::env::var("OAUTH_REDIRECT_URI") 105 - .unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string()) 106 - .trim_end_matches("/oauth/callback") 107 - .to_string(); 108 - 109 - let metadata = serde_json::json!({ 110 - "client_id": format!("{}/oauth-client-metadata.json", base_url), 111 - "client_name": "@me", 112 - "client_uri": base_url.clone(), 113 - "redirect_uris": [format!("{}/oauth/callback", base_url)], 114 - "scope": "atproto", 115 - "grant_types": ["authorization_code", "refresh_token"], 116 - "response_types": ["code"], 117 - "token_endpoint_auth_method": "none", 118 - "dpop_bound_access_tokens": true 119 - }); 120 - 121 - HttpResponse::Ok() 122 - .content_type("application/json") 123 - .body(metadata.to_string()) 124 - } 125 - 126 - #[get("/logout")] 127 - pub async fn logout(session: Session) -> HttpResponse { 128 - session.purge(); 129 - HttpResponse::SeeOther() 130 - .append_header(("Location", "/")) 131 - .finish() 132 - } 133 - 134 - #[derive(Deserialize)] 135 - pub struct RestoreSession { 136 - did: String, 137 - } 138 - 139 - #[post("/api/restore-session")] 140 - pub async fn restore_session( 141 - data: web::Json<RestoreSession>, 142 - session: Session, 143 - ) -> HttpResponse { 144 - session.insert("did", &data.did).unwrap(); 145 - HttpResponse::Ok().finish() 146 - } 147 - 148 - #[get("/favicon.svg")] 149 - pub async fn favicon() -> HttpResponse { 150 - HttpResponse::Ok() 151 - .content_type("image/svg+xml") 152 - .body(FAVICON_SVG) 153 - }
···
-991
src/templates.rs
··· 1 - pub fn login_page() -> &'static str { 2 - r#" 3 - <!DOCTYPE html> 4 - <html> 5 - <head> 6 - <meta charset="UTF-8"> 7 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 8 - <title>@me - explore your atproto identity</title> 9 - <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 10 - 11 - <!-- Open Graph / Facebook --> 12 - <meta property="og:type" content="website"> 13 - <meta property="og:url" content="https://at-me.fly.dev/"> 14 - <meta property="og:title" content="@me - explore your atproto identity"> 15 - <meta property="og:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 16 - <meta property="og:image" content="https://at-me.fly.dev/static/og-image.png"> 17 - 18 - <!-- Twitter --> 19 - <meta property="twitter:card" content="summary_large_image"> 20 - <meta property="twitter:url" content="https://at-me.fly.dev/"> 21 - <meta property="twitter:title" content="@me - explore your atproto identity"> 22 - <meta property="twitter:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 23 - <meta property="twitter:image" content="https://at-me.fly.dev/static/og-image.png"> 24 - 25 - <style> 26 - * { margin: 0; padding: 0; box-sizing: border-box; } 27 - 28 - body { 29 - font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 30 - height: 100vh; 31 - background: radial-gradient(ellipse at center, #0a0a0f 0%, #000000 100%); 32 - color: #e5e5e5; 33 - overflow: hidden; 34 - perspective: 1000px; 35 - } 36 - 37 - .atmosphere { 38 - position: fixed; 39 - inset: 0; 40 - transform-style: preserve-3d; 41 - animation: rotate 120s infinite linear; 42 - } 43 - 44 - @keyframes rotate { 45 - from { transform: rotateY(0deg); } 46 - to { transform: rotateY(360deg); } 47 - } 48 - 49 - .app-orb { 50 - position: absolute; 51 - border-radius: 50%; 52 - display: flex; 53 - align-items: center; 54 - justify-content: center; 55 - transition: all 0.3s ease; 56 - cursor: pointer; 57 - backdrop-filter: blur(4px); 58 - } 59 - 60 - .app-orb:hover { 61 - transform: scale(1.2) !important; 62 - z-index: 100; 63 - } 64 - 65 - .app-orb img { 66 - width: 100%; 67 - height: 100%; 68 - border-radius: 50%; 69 - object-fit: cover; 70 - } 71 - 72 - .app-orb .fallback { 73 - font-size: 1.5rem; 74 - font-weight: 600; 75 - color: rgba(255, 255, 255, 0.9); 76 - } 77 - 78 - .app-tooltip { 79 - position: absolute; 80 - background: rgba(10, 10, 15, 0.95); 81 - border: 1px solid rgba(255, 255, 255, 0.1); 82 - padding: 0.5rem 0.75rem; 83 - border-radius: 4px; 84 - font-size: 0.7rem; 85 - white-space: nowrap; 86 - pointer-events: none; 87 - opacity: 0; 88 - transition: opacity 0.2s; 89 - z-index: 1000; 90 - } 91 - 92 - .app-orb:hover .app-tooltip { 93 - opacity: 1; 94 - } 95 - 96 - .container { 97 - position: fixed; 98 - inset: 0; 99 - display: flex; 100 - align-items: center; 101 - justify-content: center; 102 - z-index: 10; 103 - } 104 - 105 - .login-card { 106 - background: transparent; 107 - border: 1px solid rgba(255, 255, 255, 0.1); 108 - padding: 2.5rem 3rem; 109 - border-radius: 8px; 110 - backdrop-filter: blur(2px); 111 - text-align: center; 112 - box-shadow: none; 113 - } 114 - 115 - h1 { 116 - font-size: 2rem; 117 - margin-bottom: 0.5rem; 118 - font-weight: 300; 119 - letter-spacing: 0.05em; 120 - } 121 - 122 - .subtitle { 123 - font-size: 0.75rem; 124 - color: rgba(255, 255, 255, 0.5); 125 - margin-bottom: 2rem; 126 - } 127 - 128 - input { 129 - font-family: inherit; 130 - font-size: 0.9rem; 131 - padding: 0.75rem 1rem; 132 - margin-bottom: 1rem; 133 - background: rgba(10, 10, 15, 0.8); 134 - border: 1px solid rgba(255, 255, 255, 0.2); 135 - border-radius: 4px; 136 - color: #e5e5e5; 137 - width: 100%; 138 - min-width: 300px; 139 - transition: all 0.2s; 140 - } 141 - 142 - input:focus { 143 - outline: none; 144 - border-color: rgba(255, 255, 255, 0.4); 145 - background: rgba(10, 10, 15, 0.9); 146 - } 147 - 148 - input::placeholder { 149 - color: rgba(255, 255, 255, 0.3); 150 - } 151 - 152 - button { 153 - font-family: inherit; 154 - font-size: 0.9rem; 155 - padding: 0.75rem 2rem; 156 - cursor: pointer; 157 - background: rgba(10, 10, 15, 0.8); 158 - border: 1px solid rgba(255, 255, 255, 0.2); 159 - border-radius: 4px; 160 - color: #e5e5e5; 161 - transition: all 0.2s; 162 - width: 100%; 163 - } 164 - 165 - button:hover { 166 - background: rgba(10, 10, 15, 0.9); 167 - border-color: rgba(255, 255, 255, 0.4); 168 - } 169 - 170 - .hidden { display: none; } 171 - .loading { color: rgba(255, 255, 255, 0.5); font-size: 0.9rem; } 172 - 173 - .footer { 174 - position: fixed; 175 - bottom: 1rem; 176 - left: 50%; 177 - transform: translateX(-50%); 178 - font-size: 0.7rem; 179 - color: rgba(255, 255, 255, 0.3); 180 - z-index: 20; 181 - } 182 - 183 - .footer a { 184 - color: rgba(255, 255, 255, 0.5); 185 - text-decoration: none; 186 - transition: color 0.2s; 187 - } 188 - 189 - .footer a:hover { 190 - color: rgba(255, 255, 255, 0.8); 191 - } 192 - </style> 193 - </head> 194 - <body> 195 - <div class="atmosphere" id="atmosphere"></div> 196 - 197 - <div class="container"> 198 - <div class="login-card"> 199 - <div id="restoring" class="loading hidden">restoring session...</div> 200 - <form id="loginForm" method="post" action="/login"> 201 - <h1>@me</h1> 202 - <div class="subtitle">explore the atmosphere</div> 203 - <input type="text" name="handle" placeholder="handle.bsky.social" required autofocus> 204 - <button type="submit">enter</button> 205 - </form> 206 - </div> 207 - </div> 208 - 209 - <div class="footer"> 210 - by <a href="https://bsky.app/profile/zzstoatzz.io" target="_blank" rel="noopener noreferrer">@zzstoatzz.io</a> 211 - </div> 212 - 213 - <script src="/static/login.js"></script> 214 - </body> 215 - </html> 216 - "# 217 - } 218 - 219 - pub fn app_page(did: &str) -> String { 220 - format!(r#" 221 - <!DOCTYPE html> 222 - <html> 223 - <head> 224 - <meta charset="UTF-8"> 225 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 226 - <title>@me - explore your atproto identity</title> 227 - <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 228 - 229 - <!-- Open Graph / Facebook --> 230 - <meta property="og:type" content="website"> 231 - <meta property="og:url" content="https://at-me.fly.dev/"> 232 - <meta property="og:title" content="@me - explore your atproto identity"> 233 - <meta property="og:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 234 - <meta property="og:image" content="https://at-me.fly.dev/static/og-image.png"> 235 - 236 - <!-- Twitter --> 237 - <meta property="twitter:card" content="summary_large_image"> 238 - <meta property="twitter:url" content="https://at-me.fly.dev/"> 239 - <meta property="twitter:title" content="@me - explore your atproto identity"> 240 - <meta property="twitter:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 241 - <meta property="twitter:image" content="https://at-me.fly.dev/static/og-image.png"> 242 - 243 - <style> 244 - * {{ margin: 0; padding: 0; box-sizing: border-box; }} 245 - 246 - :root {{ 247 - --bg: #f5f1e8; 248 - --text: #4a4238; 249 - --text-light: #8a7a6a; 250 - --text-lighter: #6b5d4f; 251 - --border: #c9bfa8; 252 - --surface: #e5dbc8; 253 - --surface-hover: #d9cdb5; 254 - }} 255 - 256 - @media (prefers-color-scheme: dark) {{ 257 - :root {{ 258 - --bg: #1a1a1a; 259 - --text: #e5e5e5; 260 - --text-light: #a0a0a0; 261 - --text-lighter: #c0c0c0; 262 - --border: #404040; 263 - --surface: #2a2a2a; 264 - --surface-hover: #353535; 265 - }} 266 - }} 267 - 268 - body {{ 269 - font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 270 - height: 100vh; 271 - background: var(--bg); 272 - color: var(--text); 273 - overflow: hidden; 274 - position: relative; 275 - -webkit-font-smoothing: antialiased; 276 - -moz-osx-font-smoothing: grayscale; 277 - }} 278 - 279 - .canvas {{ 280 - width: 100%; 281 - height: 100%; 282 - position: relative; 283 - display: flex; 284 - align-items: center; 285 - justify-content: center; 286 - }} 287 - 288 - .logout {{ 289 - position: fixed; 290 - top: clamp(1rem, 2vmin, 1.5rem); 291 - right: clamp(1rem, 2vmin, 1.5rem); 292 - font-size: clamp(0.65rem, 1.4vmin, 0.75rem); 293 - color: var(--text-light); 294 - text-decoration: none; 295 - border: 1px solid var(--border); 296 - padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem); 297 - transition: all 0.2s ease; 298 - z-index: 100; 299 - -webkit-tap-highlight-color: transparent; 300 - cursor: pointer; 301 - border-radius: 2px; 302 - }} 303 - 304 - .logout:hover, .logout:active {{ 305 - background: var(--surface); 306 - color: var(--text); 307 - border-color: var(--text-light); 308 - }} 309 - 310 - .info {{ 311 - position: fixed; 312 - top: clamp(1rem, 2vmin, 1.5rem); 313 - left: clamp(1rem, 2vmin, 1.5rem); 314 - width: clamp(32px, 6vmin, 40px); 315 - height: clamp(32px, 6vmin, 40px); 316 - border-radius: 50%; 317 - border: 1px solid var(--border); 318 - display: flex; 319 - align-items: center; 320 - justify-content: center; 321 - font-size: clamp(0.7rem, 1.5vmin, 0.85rem); 322 - color: var(--text-light); 323 - cursor: pointer; 324 - transition: all 0.2s ease; 325 - z-index: 100; 326 - -webkit-tap-highlight-color: transparent; 327 - }} 328 - 329 - .info:hover, .info:active {{ 330 - background: var(--surface); 331 - color: var(--text); 332 - border-color: var(--text-light); 333 - }} 334 - 335 - .info-modal {{ 336 - position: fixed; 337 - top: 50%; 338 - left: 50%; 339 - transform: translate(-50%, -50%); 340 - background: var(--surface); 341 - border: 2px solid var(--border); 342 - padding: 2rem; 343 - max-width: 500px; 344 - width: 90%; 345 - z-index: 2000; 346 - display: none; 347 - border-radius: 4px; 348 - }} 349 - 350 - @media (max-width: 768px) {{ 351 - .info-modal {{ 352 - padding: 1.5rem; 353 - width: 95%; 354 - }} 355 - 356 - .info-modal h2 {{ 357 - font-size: 0.9rem; 358 - }} 359 - 360 - .info-modal p {{ 361 - font-size: 0.7rem; 362 - }} 363 - }} 364 - 365 - .info-modal.visible {{ 366 - display: block; 367 - }} 368 - 369 - .info-modal h2 {{ 370 - margin-bottom: 1rem; 371 - font-size: 1rem; 372 - color: var(--text); 373 - }} 374 - 375 - .info-modal p {{ 376 - margin-bottom: 0.75rem; 377 - font-size: 0.75rem; 378 - line-height: 1.5; 379 - color: var(--text-lighter); 380 - }} 381 - 382 - .info-modal button {{ 383 - margin-top: 1rem; 384 - padding: 0.5rem 1rem; 385 - background: var(--bg); 386 - border: 1px solid var(--border); 387 - color: var(--text); 388 - font-family: inherit; 389 - font-size: 0.7rem; 390 - cursor: pointer; 391 - transition: all 0.2s ease; 392 - -webkit-tap-highlight-color: transparent; 393 - border-radius: 2px; 394 - }} 395 - 396 - .info-modal button:hover, .info-modal button:active {{ 397 - background: var(--surface-hover); 398 - border-color: var(--text-light); 399 - }} 400 - 401 - @media (max-width: 768px) {{ 402 - .info-modal button {{ 403 - padding: 0.65rem 1.2rem; 404 - font-size: 0.75rem; 405 - }} 406 - }} 407 - 408 - .overlay {{ 409 - position: fixed; 410 - top: 0; 411 - left: 0; 412 - right: 0; 413 - bottom: 0; 414 - background: rgba(0, 0, 0, 0.5); 415 - z-index: 1999; 416 - display: none; 417 - }} 418 - 419 - .overlay.visible {{ 420 - display: block; 421 - }} 422 - 423 - .identity {{ 424 - position: absolute; 425 - background: var(--surface); 426 - border: 2px solid var(--text-light); 427 - border-radius: 50%; 428 - width: clamp(100px, 20vmin, 140px); 429 - height: clamp(100px, 20vmin, 140px); 430 - display: flex; 431 - flex-direction: column; 432 - align-items: center; 433 - justify-content: center; 434 - gap: clamp(0.2rem, 1vmin, 0.3rem); 435 - padding: clamp(0.4rem, 1vmin, 0.6rem); 436 - z-index: 10; 437 - cursor: pointer; 438 - transition: all 0.2s ease; 439 - -webkit-tap-highlight-color: transparent; 440 - }} 441 - 442 - .identity:hover, .identity:active {{ 443 - transform: scale(1.05); 444 - border-color: var(--text); 445 - box-shadow: 0 0 20px rgba(255, 255, 255, 0.1); 446 - }} 447 - 448 - .identity-label {{ 449 - font-size: clamp(1rem, 2vmin, 1.2rem); 450 - color: var(--text); 451 - font-weight: 600; 452 - line-height: 1; 453 - }} 454 - 455 - .identity-value {{ 456 - font-size: clamp(0.6rem, 1.2vmin, 0.7rem); 457 - color: var(--text-lighter); 458 - text-align: center; 459 - word-break: break-word; 460 - max-width: 90%; 461 - font-weight: 400; 462 - line-height: 1.2; 463 - }} 464 - 465 - .identity-hint {{ 466 - font-size: clamp(0.35rem, 0.8vmin, 0.45rem); 467 - color: var(--text-lighter); 468 - margin-top: 0.2rem; 469 - letter-spacing: 0.05em; 470 - }} 471 - 472 - .identity-avatar {{ 473 - width: clamp(30px, 6vmin, 45px); 474 - height: clamp(30px, 6vmin, 45px); 475 - border-radius: 50%; 476 - object-fit: cover; 477 - border: 2px solid var(--text-light); 478 - margin-bottom: clamp(0.2rem, 1vmin, 0.3rem); 479 - }} 480 - 481 - .app-view {{ 482 - position: absolute; 483 - display: flex; 484 - flex-direction: column; 485 - align-items: center; 486 - gap: clamp(0.3rem, 1vmin, 0.5rem); 487 - cursor: pointer; 488 - transition: all 0.2s ease; 489 - opacity: 0.7; 490 - }} 491 - 492 - .app-view:hover {{ 493 - opacity: 1; 494 - transform: scale(1.1); 495 - z-index: 100; 496 - }} 497 - 498 - .app-circle {{ 499 - background: var(--surface-hover); 500 - border: 1px solid var(--border); 501 - border-radius: 50%; 502 - width: clamp(45px, 8vmin, 60px); 503 - height: clamp(45px, 8vmin, 60px); 504 - display: flex; 505 - align-items: center; 506 - justify-content: center; 507 - transition: all 0.2s ease; 508 - overflow: hidden; 509 - font-size: clamp(1rem, 2vmin, 1.5rem); 510 - }} 511 - 512 - .app-logo {{ 513 - width: 100%; 514 - height: 100%; 515 - object-fit: cover; 516 - }} 517 - 518 - .app-view:hover .app-circle {{ 519 - background: var(--surface); 520 - border-color: var(--text-light); 521 - }} 522 - 523 - .app-name {{ 524 - font-size: clamp(0.6rem, 1.2vmin, 0.7rem); 525 - color: var(--text); 526 - text-align: center; 527 - max-width: clamp(80px, 15vmin, 120px); 528 - }} 529 - 530 - .detail-panel {{ 531 - position: fixed; 532 - top: 0; 533 - left: 0; 534 - bottom: 0; 535 - width: 320px; 536 - background: var(--surface); 537 - border-right: 2px solid var(--border); 538 - padding: 2.5rem 2rem; 539 - overflow-y: auto; 540 - opacity: 0; 541 - transform: translateX(-100%); 542 - transition: all 0.25s ease; 543 - z-index: 1000; 544 - }} 545 - 546 - .detail-panel.visible {{ 547 - opacity: 1; 548 - transform: translateX(0); 549 - }} 550 - 551 - @media (max-width: 768px) {{ 552 - .detail-panel {{ 553 - width: 100%; 554 - padding: 4rem 1.5rem 2rem; 555 - border-right: none; 556 - border-bottom: 2px solid var(--border); 557 - }} 558 - }} 559 - 560 - .detail-panel h3 {{ 561 - margin-bottom: 0.75rem; 562 - font-size: 0.85rem; 563 - color: var(--text); 564 - }} 565 - 566 - .detail-panel .subtitle {{ 567 - font-size: 0.7rem; 568 - color: var(--text-light); 569 - margin-bottom: 1.5rem; 570 - line-height: 1.4; 571 - }} 572 - 573 - .detail-close {{ 574 - position: absolute; 575 - top: 1.5rem; 576 - right: 1.5rem; 577 - width: 32px; 578 - height: 32px; 579 - border: 1px solid var(--border); 580 - background: var(--bg); 581 - color: var(--text-light); 582 - cursor: pointer; 583 - display: flex; 584 - align-items: center; 585 - justify-content: center; 586 - font-size: 1.2rem; 587 - line-height: 1; 588 - transition: all 0.2s ease; 589 - border-radius: 2px; 590 - -webkit-tap-highlight-color: transparent; 591 - }} 592 - 593 - .detail-close:hover, .detail-close:active {{ 594 - background: var(--surface-hover); 595 - border-color: var(--text-light); 596 - color: var(--text); 597 - }} 598 - 599 - @media (max-width: 768px) {{ 600 - .detail-close {{ 601 - top: 1rem; 602 - right: 1rem; 603 - width: 40px; 604 - height: 40px; 605 - font-size: 1.4rem; 606 - }} 607 - }} 608 - 609 - .tree-item {{ 610 - padding: 0.65rem 0.75rem; 611 - font-size: 0.75rem; 612 - color: var(--text-lighter); 613 - background: var(--bg); 614 - border: 1px solid var(--border); 615 - border-radius: 2px; 616 - margin-bottom: 0.5rem; 617 - transition: all 0.15s ease; 618 - cursor: pointer; 619 - -webkit-tap-highlight-color: transparent; 620 - }} 621 - 622 - .tree-item:hover, .tree-item:active {{ 623 - background: var(--surface-hover); 624 - border-color: var(--text-light); 625 - }} 626 - 627 - @media (max-width: 768px) {{ 628 - .tree-item {{ 629 - padding: 0.8rem 0.9rem; 630 - font-size: 0.8rem; 631 - }} 632 - }} 633 - 634 - .tree-item:last-child {{ 635 - margin-bottom: 0; 636 - }} 637 - 638 - .tree-item-header {{ 639 - display: flex; 640 - justify-content: space-between; 641 - align-items: center; 642 - }} 643 - 644 - .tree-item-count {{ 645 - font-size: 0.65rem; 646 - color: var(--text-light); 647 - }} 648 - 649 - .record-list {{ 650 - margin-top: 0.5rem; 651 - padding-top: 0.5rem; 652 - border-top: 1px solid var(--border); 653 - }} 654 - 655 - .record {{ 656 - margin-bottom: 0.5rem; 657 - background: var(--bg); 658 - border: 1px solid var(--border); 659 - border-radius: 4px; 660 - font-size: 0.65rem; 661 - color: var(--text-light); 662 - transition: all 0.15s ease; 663 - overflow: hidden; 664 - }} 665 - 666 - .record:hover {{ 667 - border-color: var(--text-light); 668 - background: var(--surface); 669 - }} 670 - 671 - .record:last-child {{ 672 - margin-bottom: 0; 673 - }} 674 - 675 - .record-header {{ 676 - display: flex; 677 - justify-content: space-between; 678 - align-items: center; 679 - padding: 0.5rem 0.6rem; 680 - background: var(--surface); 681 - border-bottom: 1px solid var(--border); 682 - }} 683 - 684 - .record-label {{ 685 - font-size: 0.6rem; 686 - color: var(--text-lighter); 687 - font-weight: 500; 688 - }} 689 - 690 - .copy-btn {{ 691 - background: var(--bg); 692 - border: 1px solid var(--border); 693 - color: var(--text-light); 694 - font-family: inherit; 695 - font-size: 0.55rem; 696 - padding: 0.2rem 0.5rem; 697 - cursor: pointer; 698 - transition: all 0.15s ease; 699 - border-radius: 2px; 700 - -webkit-tap-highlight-color: transparent; 701 - }} 702 - 703 - .copy-btn:hover, .copy-btn:active {{ 704 - background: var(--surface-hover); 705 - border-color: var(--text-light); 706 - color: var(--text); 707 - }} 708 - 709 - .copy-btn.copied {{ 710 - color: var(--text); 711 - border-color: var(--text); 712 - }} 713 - 714 - .record-content {{ 715 - padding: 0.6rem; 716 - }} 717 - 718 - .record-content pre {{ 719 - margin: 0; 720 - white-space: pre-wrap; 721 - word-break: break-word; 722 - line-height: 1.5; 723 - font-size: 0.625rem; 724 - }} 725 - 726 - .load-more {{ 727 - margin-top: 0.5rem; 728 - padding: 0.4rem 0.6rem; 729 - background: var(--bg); 730 - border: 1px solid var(--border); 731 - color: var(--text); 732 - font-family: inherit; 733 - font-size: 0.65rem; 734 - cursor: pointer; 735 - width: 100%; 736 - transition: all 0.15s ease; 737 - -webkit-tap-highlight-color: transparent; 738 - border-radius: 2px; 739 - }} 740 - 741 - .load-more:hover, .load-more:active {{ 742 - background: var(--surface-hover); 743 - border-color: var(--text-light); 744 - }} 745 - 746 - @media (max-width: 768px) {{ 747 - .load-more {{ 748 - padding: 0.6rem 0.8rem; 749 - font-size: 0.7rem; 750 - }} 751 - }} 752 - 753 - .footer {{ 754 - position: fixed; 755 - bottom: clamp(0.75rem, 2vmin, 1rem); 756 - left: 50%; 757 - transform: translateX(-50%); 758 - font-size: clamp(0.6rem, 1.2vmin, 0.7rem); 759 - color: var(--text); 760 - z-index: 100; 761 - background: var(--surface); 762 - padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.75rem, 2vmin, 1rem); 763 - border-radius: 4px; 764 - border: 1px solid var(--border); 765 - }} 766 - 767 - .footer a {{ 768 - color: var(--text); 769 - text-decoration: none; 770 - border-bottom: 1px solid transparent; 771 - transition: all 0.2s ease; 772 - }} 773 - 774 - .footer a:hover {{ 775 - border-bottom-color: var(--text); 776 - }} 777 - 778 - .loading {{ color: var(--text-light); font-size: 0.75rem; }} 779 - 780 - .onboarding-overlay {{ 781 - position: fixed; 782 - inset: 0; 783 - background: transparent; 784 - z-index: 3000; 785 - display: none; 786 - opacity: 0; 787 - transition: opacity 0.3s ease; 788 - pointer-events: none; 789 - }} 790 - 791 - .onboarding-overlay.active {{ 792 - display: block; 793 - opacity: 1; 794 - }} 795 - 796 - .onboarding-spotlight {{ 797 - position: absolute; 798 - border: 2px solid rgba(255, 255, 255, 0.9); 799 - border-radius: 50%; 800 - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.75), 0 0 40px rgba(255, 255, 255, 0.5); 801 - pointer-events: none; 802 - transition: all 0.5s ease; 803 - }} 804 - 805 - .onboarding-content {{ 806 - position: fixed; 807 - background: var(--surface); 808 - border: 2px solid var(--border); 809 - padding: clamp(1rem, 3vmin, 2rem); 810 - max-width: min(400px, 90vw); 811 - z-index: 3001; 812 - border-radius: 4px; 813 - transition: all 0.3s ease; 814 - pointer-events: auto; 815 - }} 816 - 817 - .onboarding-content h3 {{ 818 - font-size: clamp(0.9rem, 2vmin, 1.1rem); 819 - margin-bottom: clamp(0.5rem, 1.5vmin, 0.75rem); 820 - color: var(--text); 821 - font-weight: 500; 822 - }} 823 - 824 - .onboarding-content p {{ 825 - font-size: clamp(0.7rem, 1.5vmin, 0.85rem); 826 - color: var(--text-light); 827 - line-height: 1.5; 828 - margin-bottom: clamp(1rem, 2vmin, 1.25rem); 829 - }} 830 - 831 - .onboarding-actions {{ 832 - display: flex; 833 - gap: clamp(0.5rem, 1.5vmin, 0.75rem); 834 - justify-content: flex-end; 835 - }} 836 - 837 - .onboarding-actions button {{ 838 - font-family: inherit; 839 - font-size: clamp(0.7rem, 1.5vmin, 0.8rem); 840 - padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem); 841 - background: transparent; 842 - border: 1px solid var(--border); 843 - color: var(--text); 844 - cursor: pointer; 845 - transition: all 0.2s ease; 846 - border-radius: 2px; 847 - }} 848 - 849 - .onboarding-actions button:hover {{ 850 - background: var(--surface-hover); 851 - border-color: var(--text-light); 852 - }} 853 - 854 - .onboarding-actions button.primary {{ 855 - background: var(--surface-hover); 856 - border-color: var(--text-light); 857 - }} 858 - 859 - .onboarding-progress {{ 860 - display: flex; 861 - gap: clamp(0.4rem, 1vmin, 0.5rem); 862 - justify-content: center; 863 - margin-top: clamp(0.75rem, 2vmin, 1rem); 864 - }} 865 - 866 - .onboarding-progress span {{ 867 - width: clamp(6px, 1.5vmin, 8px); 868 - height: clamp(6px, 1.5vmin, 8px); 869 - border-radius: 50%; 870 - background: var(--border); 871 - transition: background 0.3s ease; 872 - }} 873 - 874 - .onboarding-progress span.active {{ 875 - background: var(--text); 876 - }} 877 - 878 - .onboarding-progress span.done {{ 879 - background: var(--text-light); 880 - }} 881 - 882 - .stats-box {{ 883 - display: flex; 884 - gap: 1.5rem; 885 - margin: 1.5rem 0; 886 - padding: 1rem; 887 - background: var(--bg); 888 - border-radius: 4px; 889 - border: 1px solid var(--border); 890 - }} 891 - 892 - .stat {{ 893 - flex: 1; 894 - text-align: center; 895 - }} 896 - 897 - .stat-value {{ 898 - font-size: 1.8rem; 899 - font-weight: 600; 900 - color: var(--text); 901 - margin-bottom: 0.25rem; 902 - }} 903 - 904 - .stat-label {{ 905 - font-size: 0.65rem; 906 - color: var(--text-light); 907 - text-transform: uppercase; 908 - letter-spacing: 0.05em; 909 - }} 910 - 911 - .ownership-box {{ 912 - margin: 1rem 0; 913 - padding: 1rem; 914 - background: var(--bg); 915 - border-radius: 4px; 916 - border: 1px solid var(--border); 917 - }} 918 - 919 - .ownership-box.yours {{ 920 - background: rgba(76, 175, 80, 0.05); 921 - border-color: rgba(76, 175, 80, 0.3); 922 - }} 923 - 924 - @media (prefers-color-scheme: dark) {{ 925 - .ownership-box.yours {{ 926 - background: rgba(76, 175, 80, 0.08); 927 - border-color: rgba(76, 175, 80, 0.4); 928 - }} 929 - }} 930 - 931 - .ownership-header {{ 932 - font-size: 0.7rem; 933 - font-weight: 600; 934 - color: var(--text); 935 - margin-bottom: 0.5rem; 936 - text-transform: uppercase; 937 - letter-spacing: 0.05em; 938 - }} 939 - 940 - .ownership-text {{ 941 - font-size: 0.7rem; 942 - color: var(--text-lighter); 943 - line-height: 1.5; 944 - }} 945 - 946 - .ownership-text strong {{ 947 - color: var(--text); 948 - }} 949 - </style> 950 - </head> 951 - <body> 952 - <div class="info" id="infoBtn">?</div> 953 - <a href="javascript:void(0)" id="logoutBtn" class="logout">logout</a> 954 - 955 - <div class="overlay" id="overlay"></div> 956 - <div class="info-modal" id="infoModal"> 957 - <h2>@me - your repository</h2> 958 - <p>on traditional social platforms, your content is locked in. want to switch? you start from zero. you build their network, they control the distribution.</p> 959 - <p>on atproto, you own everything. your data lives in your <a href="https://atproto.com/guides/overview" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data Server (PDS)</a>. apps like bluesky, whitewind, and frontpage just write to YOUR space. switch apps anytime, take it all with you.</p> 960 - <p>click your @ in the center to see what you've built. click any app to see what it's stored in your repository.</p> 961 - <button id="closeInfo">got it</button> 962 - <button id="restartTour" onclick="window.restartOnboarding()" style="margin-left: 0.5rem; background: var(--surface-hover);">restart tour</button> 963 - </div> 964 - 965 - <div class="onboarding-overlay" id="onboardingOverlay"> 966 - <div class="onboarding-spotlight" id="onboardingSpotlight"></div> 967 - <div class="onboarding-content" id="onboardingContent"></div> 968 - </div> 969 - 970 - <div class="canvas"> 971 - <div class="identity"> 972 - <div class="identity-label">@</div> 973 - <div class="identity-value" id="handle">loading...</div> 974 - <div class="identity-hint">tap for details</div> 975 - </div> 976 - <div id="field" class="loading">loading...</div> 977 - </div> 978 - <div id="detail" class="detail-panel"></div> 979 - 980 - <div class="footer"> 981 - <a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer">view source</a> 982 - </div> 983 - <script> 984 - window.DID = '{}'; 985 - </script> 986 - <script src="/static/app.js"></script> 987 - <script src="/static/onboarding.js"></script> 988 - </body> 989 - </html> 990 - "#, did) 991 - }
···
+201
src/view/atproto.js
···
··· 1 + // ============================================================================ 2 + // ATPROTO UTILITIES - Client-side DID/PDS resolution 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + 7 + const DOMAIN_REDIRECTS = { 8 + 'tangled.sh': 'tangled.org', 9 + }; 10 + 11 + export function applyDomainRedirect(domain) { 12 + return DOMAIN_REDIRECTS[domain] || domain; 13 + } 14 + 15 + export function escapeHtml(text) { 16 + const div = document.createElement('div'); 17 + div.textContent = text; 18 + return div.innerHTML; 19 + } 20 + 21 + // Resolve a handle to a DID 22 + export async function resolveHandle(handle) { 23 + try { 24 + const response = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); 25 + if (response.ok) { 26 + const data = await response.json(); 27 + return data.did; 28 + } 29 + } catch (e) { 30 + console.error('Failed to resolve handle:', e); 31 + } 32 + return null; 33 + } 34 + 35 + // Resolve a DID to its DID document (for PDS endpoint) 36 + export async function resolveDid(did) { 37 + try { 38 + // For did:plc, use the plc.directory 39 + if (did.startsWith('did:plc:')) { 40 + const response = await fetch(`https://plc.directory/${did}`); 41 + if (response.ok) { 42 + return await response.json(); 43 + } 44 + } 45 + // For did:web, resolve via .well-known 46 + if (did.startsWith('did:web:')) { 47 + const domain = did.replace('did:web:', ''); 48 + const response = await fetch(`https://${domain}/.well-known/did.json`); 49 + if (response.ok) { 50 + return await response.json(); 51 + } 52 + } 53 + } catch (e) { 54 + console.error('Failed to resolve DID:', e); 55 + } 56 + return null; 57 + } 58 + 59 + // Get PDS endpoint from DID document 60 + export function getPdsFromDidDoc(didDoc) { 61 + if (!didDoc || !didDoc.service) return null; 62 + const atprotoService = didDoc.service.find(s => 63 + s.type === 'AtprotoPersonalDataServer' || 64 + s.id === '#atproto_pds' 65 + ); 66 + return atprotoService?.serviceEndpoint || null; 67 + } 68 + 69 + // Get handle from DID document 70 + export function getHandleFromDidDoc(didDoc) { 71 + if (!didDoc || !didDoc.alsoKnownAs) return null; 72 + const atUri = didDoc.alsoKnownAs.find(u => u.startsWith('at://')); 73 + return atUri ? atUri.replace('at://', '') : null; 74 + } 75 + 76 + // Get profile from Bluesky AppView 77 + export async function getProfile(handleOrDid) { 78 + try { 79 + const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`); 80 + if (response.ok) { 81 + return await response.json(); 82 + } 83 + } catch (e) { 84 + console.error('Failed to get profile:', e); 85 + } 86 + return null; 87 + } 88 + 89 + // Describe the repo to get all collections 90 + export async function describeRepo(pds, did) { 91 + try { 92 + const response = await fetch(`${pds}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(did)}`); 93 + if (response.ok) { 94 + return await response.json(); 95 + } 96 + } catch (e) { 97 + console.error('Failed to describe repo:', e); 98 + } 99 + return null; 100 + } 101 + 102 + // List records in a collection 103 + export async function listRecords(pds, did, collection, limit = 10, cursor = null) { 104 + try { 105 + let url = `${pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&limit=${limit}`; 106 + if (cursor) url += `&cursor=${cursor}`; 107 + const response = await fetch(url); 108 + if (response.ok) { 109 + return await response.json(); 110 + } 111 + } catch (e) { 112 + console.error('Failed to list records:', e); 113 + } 114 + return null; 115 + } 116 + 117 + // Get app avatar from the namespace's well-known profile 118 + export async function getAppAvatar(namespace) { 119 + const domain = namespace.split('.').reverse().join('.'); 120 + const redirectedDomain = applyDomainRedirect(domain); 121 + try { 122 + const profile = await getProfile(redirectedDomain); 123 + if (profile && profile.avatar) { 124 + return profile.avatar; 125 + } 126 + } catch (e) { 127 + // Silently fail 128 + } 129 + return null; 130 + } 131 + 132 + // Batch fetch app avatars 133 + export async function fetchAppAvatars(namespaces) { 134 + const avatars = {}; 135 + const promises = namespaces.map(async (ns) => { 136 + const avatar = await getAppAvatar(ns); 137 + if (avatar) { 138 + avatars[ns] = avatar; 139 + } 140 + }); 141 + await Promise.all(promises); 142 + return avatars; 143 + } 144 + 145 + // Validate app URLs by checking if the domain is reachable 146 + export async function validateAppUrls(appDivs) { 147 + // Clear previous invalid apps 148 + state.invalidApps.clear(); 149 + 150 + const validationPromises = appDivs.map(async ({ div, namespace }) => { 151 + const link = div.querySelector('.app-name'); 152 + const url = link?.dataset.url; 153 + if (!url || !link) return; 154 + 155 + try { 156 + new URL(url); // Check syntax first 157 + } catch (e) { 158 + markInvalid(link, namespace, 'malformed URL'); 159 + return; 160 + } 161 + 162 + // Try HEAD request with short timeout to check if domain is reachable 163 + try { 164 + const controller = new AbortController(); 165 + const timeout = setTimeout(() => controller.abort(), 3000); 166 + 167 + await fetch(url, { 168 + method: 'HEAD', 169 + mode: 'no-cors', 170 + signal: controller.signal, 171 + }); 172 + 173 + clearTimeout(timeout); 174 + // If we get here, domain is reachable (even if response is opaque due to CORS) 175 + } catch (e) { 176 + // Only mark as invalid for actual DNS/connection failures 177 + // CORS blocks mean the server IS reachable, just not allowing our request 178 + const errorMsg = e.message || ''; 179 + if (errorMsg.includes('ERR_NAME_NOT_RESOLVED') || 180 + errorMsg.includes('ERR_CONNECTION_REFUSED') || 181 + errorMsg.includes('ERR_CONNECTION_TIMED_OUT') || 182 + e.name === 'AbortError') { 183 + markInvalid(link, namespace, 'domain not reachable'); 184 + } 185 + // For CORS blocks (ERR_FAILED) and other errors, server exists so don't mark invalid 186 + } 187 + }); 188 + 189 + await Promise.all(validationPromises); 190 + } 191 + 192 + function markInvalid(link, namespace, reason) { 193 + const displayName = link.textContent.replace(' โ†—', '').replace(' \u2197', ''); 194 + link.classList.add('invalid-link'); 195 + link.setAttribute('title', reason); 196 + link.style.pointerEvents = 'none'; 197 + link.textContent = displayName; 198 + if (namespace) { 199 + state.invalidApps.add(namespace); 200 + } 201 + }
+155
src/view/base.css
···
··· 1 + * { 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 + } 6 + 7 + :root { 8 + --bg: #f5f1e8; 9 + --text: #4a4238; 10 + --text-light: #8a7a6a; 11 + --text-lighter: #6b5d4f; 12 + --border: #c9bfa8; 13 + --surface: #e5dbc8; 14 + --surface-hover: #d9cdb5; 15 + } 16 + 17 + @media (prefers-color-scheme: dark) { 18 + :root { 19 + --bg: #1a1a1a; 20 + --text: #e5e5e5; 21 + --text-light: #a0a0a0; 22 + --text-lighter: #c0c0c0; 23 + --border: #404040; 24 + --surface: #2a2a2a; 25 + --surface-hover: #353535; 26 + } 27 + } 28 + 29 + body { 30 + font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 31 + height: 100vh; 32 + background: var(--bg); 33 + color: var(--text); 34 + overflow: hidden; 35 + position: relative; 36 + -webkit-font-smoothing: antialiased; 37 + -moz-osx-font-smoothing: grayscale; 38 + } 39 + 40 + .canvas { 41 + position: fixed; 42 + inset: 0; 43 + } 44 + 45 + .info { 46 + position: fixed; 47 + bottom: clamp(0.75rem, 2vmin, 1rem); 48 + left: clamp(0.75rem, 2vmin, 1rem); 49 + width: clamp(32px, 7vmin, 40px); 50 + height: clamp(32px, 7vmin, 40px); 51 + display: flex; 52 + align-items: center; 53 + justify-content: center; 54 + font-size: clamp(0.85rem, 1.8vmin, 1rem); 55 + color: var(--text-light); 56 + cursor: pointer; 57 + transition: all 0.2s ease; 58 + z-index: 100; 59 + -webkit-tap-highlight-color: transparent; 60 + } 61 + 62 + .info:hover, 63 + .info:active { 64 + color: var(--text); 65 + } 66 + 67 + .info-modal { 68 + position: fixed; 69 + top: 50%; 70 + left: 50%; 71 + transform: translate(-50%, -50%); 72 + background: var(--surface); 73 + border: 2px solid var(--border); 74 + padding: 2rem; 75 + max-width: 500px; 76 + width: 90%; 77 + z-index: 2000; 78 + display: none; 79 + border-radius: 4px; 80 + } 81 + 82 + @media (max-width: 768px) { 83 + .info-modal { 84 + padding: 1.5rem; 85 + width: 95%; 86 + } 87 + 88 + .info-modal h2 { 89 + font-size: 0.9rem; 90 + } 91 + 92 + .info-modal p { 93 + font-size: 0.7rem; 94 + } 95 + } 96 + 97 + .info-modal.visible { 98 + display: block; 99 + } 100 + 101 + .info-modal h2 { 102 + margin-bottom: 1rem; 103 + font-size: 1rem; 104 + color: var(--text); 105 + } 106 + 107 + .info-modal p { 108 + margin-bottom: 0.75rem; 109 + font-size: 0.75rem; 110 + line-height: 1.5; 111 + color: var(--text-lighter); 112 + } 113 + 114 + .info-modal button { 115 + margin-top: 1rem; 116 + padding: 0.5rem 1rem; 117 + background: var(--bg); 118 + border: 1px solid var(--border); 119 + color: var(--text); 120 + font-family: inherit; 121 + font-size: 0.7rem; 122 + cursor: pointer; 123 + transition: all 0.2s ease; 124 + -webkit-tap-highlight-color: transparent; 125 + border-radius: 2px; 126 + } 127 + 128 + .info-modal button:hover, 129 + .info-modal button:active { 130 + background: var(--surface-hover); 131 + border-color: var(--text-light); 132 + } 133 + 134 + @media (max-width: 768px) { 135 + .info-modal button { 136 + padding: 0.65rem 1.2rem; 137 + font-size: 0.75rem; 138 + } 139 + } 140 + 141 + .overlay { 142 + position: fixed; 143 + top: 0; 144 + left: 0; 145 + right: 0; 146 + bottom: 0; 147 + background: rgba(0, 0, 0, 0.5); 148 + z-index: 1999; 149 + display: none; 150 + } 151 + 152 + .overlay.visible { 153 + display: block; 154 + } 155 +
+262
src/view/controls.css
···
··· 1 + .home-btn { 2 + position: fixed; 3 + top: clamp(1rem, 2vmin, 1.5rem); 4 + left: clamp(1rem, 2vmin, 1.5rem); 5 + font-family: inherit; 6 + font-size: clamp(0.85rem, 1.8vmin, 1rem); 7 + color: var(--text-light); 8 + border: 1px solid var(--border); 9 + background: var(--bg); 10 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem); 11 + transition: all 0.2s ease; 12 + z-index: 100; 13 + cursor: pointer; 14 + border-radius: 2px; 15 + text-decoration: none; 16 + display: inline-flex; 17 + align-items: center; 18 + justify-content: center; 19 + width: clamp(32px, 7vmin, 40px); 20 + height: clamp(32px, 7vmin, 40px); 21 + } 22 + 23 + .home-btn:hover, 24 + .home-btn:active { 25 + background: var(--surface); 26 + color: var(--text); 27 + border-color: var(--text-light); 28 + } 29 + 30 + .watch-live-btn { 31 + font-family: inherit; 32 + font-size: clamp(0.65rem, 1.4vmin, 0.75rem); 33 + color: var(--text-light); 34 + border: 1px solid var(--border); 35 + background: var(--bg); 36 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem); 37 + transition: all 0.2s ease; 38 + cursor: pointer; 39 + border-radius: 2px; 40 + display: flex; 41 + align-items: center; 42 + gap: clamp(0.3rem, 0.8vmin, 0.5rem); 43 + } 44 + 45 + .watch-live-btn:hover, 46 + .watch-live-btn:active { 47 + background: var(--surface); 48 + color: var(--text); 49 + border-color: var(--text-light); 50 + } 51 + 52 + .watch-live-btn.active { 53 + background: var(--surface-hover); 54 + color: var(--text); 55 + border-color: var(--text); 56 + } 57 + 58 + .watch-indicator { 59 + width: clamp(8px, 2vmin, 10px); 60 + height: clamp(8px, 2vmin, 10px); 61 + border-radius: 50%; 62 + background: var(--text-light); 63 + display: none; 64 + } 65 + 66 + .watch-live-btn.active .watch-indicator { 67 + display: block; 68 + animation: pulse 2s ease-in-out infinite; 69 + } 70 + 71 + @keyframes pulse { 72 + 0%, 100% { opacity: 0.5; } 73 + 50% { opacity: 1; } 74 + } 75 + 76 + .top-right-buttons { 77 + position: fixed; 78 + top: clamp(1rem, 2vmin, 1.5rem); 79 + right: clamp(1rem, 2vmin, 1.5rem); 80 + display: flex; 81 + flex-direction: row; 82 + align-items: center; 83 + gap: clamp(0.5rem, 1vmin, 0.75rem); 84 + z-index: 100; 85 + } 86 + 87 + @media (max-width: 768px) { 88 + .top-right-buttons { 89 + flex-direction: column; 90 + align-items: flex-end; 91 + } 92 + } 93 + 94 + .filter-btn { 95 + font-family: inherit; 96 + font-size: clamp(0.65rem, 1.4vmin, 0.75rem); 97 + color: var(--text-light); 98 + border: 1px solid var(--border); 99 + background: var(--bg); 100 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem); 101 + transition: all 0.2s ease; 102 + cursor: pointer; 103 + border-radius: 2px; 104 + display: flex; 105 + align-items: center; 106 + gap: clamp(0.3rem, 0.8vmin, 0.5rem); 107 + } 108 + 109 + .filter-btn:hover, 110 + .filter-btn:active { 111 + background: var(--surface); 112 + color: var(--text); 113 + border-color: var(--text-light); 114 + } 115 + 116 + .filter-btn.active { 117 + background: var(--surface-hover); 118 + color: var(--text); 119 + border-color: var(--text); 120 + } 121 + 122 + .filter-btn.has-filters { 123 + border-color: var(--text-light); 124 + } 125 + 126 + .filter-count { 127 + font-size: 0.6rem; 128 + background: var(--text-light); 129 + color: var(--bg); 130 + padding: 0.1rem 0.35rem; 131 + border-radius: 2px; 132 + font-weight: 500; 133 + } 134 + 135 + .info { 136 + position: fixed; 137 + bottom: clamp(0.75rem, 2vmin, 1rem); 138 + left: clamp(0.75rem, 2vmin, 1rem); 139 + width: clamp(32px, 7vmin, 40px); 140 + height: clamp(32px, 7vmin, 40px); 141 + display: flex; 142 + align-items: center; 143 + justify-content: center; 144 + font-size: clamp(0.85rem, 1.8vmin, 1rem); 145 + color: var(--text-light); 146 + cursor: pointer; 147 + transition: all 0.2s ease; 148 + z-index: 100; 149 + -webkit-tap-highlight-color: transparent; 150 + } 151 + 152 + .info:hover, 153 + .info:active { 154 + color: var(--text); 155 + } 156 + 157 + .info-modal { 158 + position: fixed; 159 + top: 50%; 160 + left: 50%; 161 + transform: translate(-50%, -50%); 162 + background: var(--surface); 163 + border: 2px solid var(--border); 164 + padding: 2rem; 165 + max-width: 500px; 166 + width: 90%; 167 + z-index: 2000; 168 + display: none; 169 + border-radius: 4px; 170 + } 171 + 172 + @media (max-width: 768px) { 173 + .info-modal { 174 + padding: 1.5rem; 175 + width: 95%; 176 + } 177 + 178 + .info-modal h2 { 179 + font-size: 0.9rem; 180 + } 181 + 182 + .info-modal p { 183 + font-size: 0.7rem; 184 + } 185 + } 186 + 187 + .info-modal.visible { 188 + display: block; 189 + } 190 + 191 + .info-modal h2 { 192 + margin-bottom: 1rem; 193 + font-size: 1rem; 194 + color: var(--text); 195 + } 196 + 197 + .info-modal p { 198 + margin-bottom: 0.75rem; 199 + font-size: 0.75rem; 200 + line-height: 1.5; 201 + color: var(--text-lighter); 202 + } 203 + 204 + .info-modal button { 205 + margin-top: 1rem; 206 + padding: 0.5rem 1rem; 207 + background: var(--bg); 208 + border: 1px solid var(--border); 209 + color: var(--text); 210 + font-family: inherit; 211 + font-size: 0.7rem; 212 + cursor: pointer; 213 + transition: all 0.2s ease; 214 + -webkit-tap-highlight-color: transparent; 215 + border-radius: 2px; 216 + } 217 + 218 + .info-modal button:hover, 219 + .info-modal button:active { 220 + background: var(--surface-hover); 221 + border-color: var(--text-light); 222 + } 223 + 224 + @media (max-width: 768px) { 225 + .info-modal button { 226 + padding: 0.65rem 1.2rem; 227 + font-size: 0.75rem; 228 + } 229 + } 230 + 231 + .overlay { 232 + position: fixed; 233 + top: 0; 234 + left: 0; 235 + right: 0; 236 + bottom: 0; 237 + background: rgba(0, 0, 0, 0.5); 238 + z-index: 1999; 239 + display: none; 240 + } 241 + 242 + .overlay.visible { 243 + display: block; 244 + } 245 + 246 + .pov-indicator { 247 + position: fixed; 248 + bottom: clamp(0.75rem, 2vmin, 1rem); 249 + right: clamp(0.75rem, 2vmin, 1rem); 250 + font-size: clamp(0.55rem, 1.2vmin, 0.65rem); 251 + color: var(--text-light); 252 + z-index: 100; 253 + } 254 + 255 + .pov-handle { 256 + color: var(--text); 257 + text-decoration: none; 258 + } 259 + 260 + .pov-handle:hover { 261 + text-decoration: underline; 262 + }
+359
src/view/detail.css
···
··· 1 + .detail-panel { 2 + position: fixed; 3 + top: 0; 4 + left: 0; 5 + bottom: 0; 6 + width: 500px; 7 + background: var(--surface); 8 + border-right: 2px solid var(--border); 9 + padding: 2.5rem 2rem; 10 + overflow-y: auto; 11 + opacity: 0; 12 + transform: translateX(-100%); 13 + transition: all 0.25s ease; 14 + z-index: 1000; 15 + scrollbar-width: none; 16 + -ms-overflow-style: none; 17 + } 18 + 19 + .detail-panel::-webkit-scrollbar { 20 + display: none; 21 + } 22 + 23 + .detail-panel.visible { 24 + opacity: 1; 25 + transform: translateX(0); 26 + } 27 + 28 + @media (max-width: 768px) { 29 + .detail-panel { 30 + width: 100%; 31 + padding: 4rem 1.5rem 2rem; 32 + border-right: none; 33 + border-bottom: 2px solid var(--border); 34 + } 35 + } 36 + 37 + .detail-panel h3 { 38 + margin-bottom: 0.75rem; 39 + font-size: 0.85rem; 40 + color: var(--text); 41 + } 42 + 43 + .detail-panel .subtitle { 44 + font-size: 0.7rem; 45 + color: var(--text-light); 46 + margin-bottom: 1.5rem; 47 + line-height: 1.4; 48 + } 49 + 50 + .detail-close { 51 + position: absolute; 52 + top: 1.5rem; 53 + right: 1.5rem; 54 + width: 32px; 55 + height: 32px; 56 + border: 1px solid var(--border); 57 + background: var(--bg); 58 + color: var(--text-light); 59 + cursor: pointer; 60 + display: flex; 61 + align-items: center; 62 + justify-content: center; 63 + font-size: 1.2rem; 64 + line-height: 1; 65 + transition: all 0.2s ease; 66 + border-radius: 2px; 67 + -webkit-tap-highlight-color: transparent; 68 + } 69 + 70 + .detail-close:hover, 71 + .detail-close:active { 72 + background: var(--surface-hover); 73 + border-color: var(--text-light); 74 + color: var(--text); 75 + } 76 + 77 + @media (max-width: 768px) { 78 + .detail-close { 79 + top: 1rem; 80 + right: 1rem; 81 + width: 40px; 82 + height: 40px; 83 + font-size: 1.4rem; 84 + } 85 + } 86 + 87 + .tree-item { 88 + padding: 0.65rem 0.75rem; 89 + font-size: 0.75rem; 90 + color: var(--text-lighter); 91 + background: var(--bg); 92 + border: 1px solid var(--border); 93 + border-radius: 2px; 94 + margin-bottom: 0.5rem; 95 + transition: all 0.15s ease; 96 + cursor: pointer; 97 + -webkit-tap-highlight-color: transparent; 98 + } 99 + 100 + .tree-item:hover, 101 + .tree-item:active { 102 + background: var(--surface-hover); 103 + border-color: var(--text-light); 104 + } 105 + 106 + @media (max-width: 768px) { 107 + .tree-item { 108 + padding: 0.8rem 0.9rem; 109 + font-size: 0.8rem; 110 + } 111 + } 112 + 113 + .tree-item:last-child { 114 + margin-bottom: 0; 115 + } 116 + 117 + .tree-item-header { 118 + display: flex; 119 + justify-content: space-between; 120 + align-items: center; 121 + } 122 + 123 + .tree-item-count { 124 + font-size: 0.65rem; 125 + color: var(--text-light); 126 + } 127 + 128 + .collection-content { 129 + margin-top: 0.5rem; 130 + padding-top: 0.5rem; 131 + border-top: 1px solid var(--border); 132 + } 133 + 134 + .collection-tabs { 135 + display: flex; 136 + gap: 0; 137 + margin-bottom: 0.75rem; 138 + border: 1px solid var(--border); 139 + border-radius: 2px; 140 + overflow: hidden; 141 + } 142 + 143 + .collection-tab { 144 + flex: 1; 145 + padding: 0.5rem 0.75rem; 146 + background: var(--bg); 147 + border: none; 148 + border-right: 1px solid var(--border); 149 + color: var(--text-light); 150 + font-family: inherit; 151 + font-size: 0.65rem; 152 + cursor: pointer; 153 + transition: all 0.15s ease; 154 + -webkit-tap-highlight-color: transparent; 155 + } 156 + 157 + .collection-tab:last-child { 158 + border-right: none; 159 + } 160 + 161 + .collection-tab:hover { 162 + background: var(--surface); 163 + color: var(--text); 164 + } 165 + 166 + .collection-tab.active { 167 + background: var(--surface-hover); 168 + color: var(--text); 169 + font-weight: 500; 170 + } 171 + 172 + .collection-view-content { 173 + position: relative; 174 + } 175 + 176 + .collection-view { 177 + display: none; 178 + } 179 + 180 + .collection-view.active { 181 + display: block; 182 + } 183 + 184 + .structure-view { 185 + min-height: 600px; 186 + } 187 + 188 + .mst-canvas { 189 + width: 100%; 190 + height: 600px; 191 + border: 1px solid var(--border); 192 + border-radius: 4px; 193 + background: var(--bg); 194 + margin-top: 0.5rem; 195 + } 196 + 197 + .mst-info { 198 + background: var(--bg); 199 + border: 1px solid var(--border); 200 + padding: 0.75rem; 201 + border-radius: 4px; 202 + margin-bottom: 0.75rem; 203 + } 204 + 205 + .mst-info p { 206 + font-size: 0.65rem; 207 + color: var(--text-lighter); 208 + line-height: 1.5; 209 + margin: 0; 210 + } 211 + 212 + .record-list { 213 + margin-top: 0.5rem; 214 + padding-top: 0.5rem; 215 + border-top: 1px solid var(--border); 216 + } 217 + 218 + .record { 219 + margin-bottom: 0.5rem; 220 + background: var(--bg); 221 + border: 1px solid var(--border); 222 + border-radius: 4px; 223 + font-size: 0.65rem; 224 + color: var(--text-light); 225 + transition: all 0.15s ease; 226 + overflow: hidden; 227 + } 228 + 229 + .record:hover { 230 + border-color: var(--text-light); 231 + background: var(--surface); 232 + } 233 + 234 + .record:last-child { 235 + margin-bottom: 0; 236 + } 237 + 238 + .record-header { 239 + display: flex; 240 + justify-content: space-between; 241 + align-items: center; 242 + padding: 0.5rem 0.6rem; 243 + background: var(--surface); 244 + border-bottom: 1px solid var(--border); 245 + } 246 + 247 + .record-label { 248 + font-size: 0.6rem; 249 + color: var(--text-lighter); 250 + font-weight: 500; 251 + } 252 + 253 + .copy-btn { 254 + background: var(--bg); 255 + border: 1px solid var(--border); 256 + color: var(--text-light); 257 + font-family: inherit; 258 + font-size: 0.55rem; 259 + padding: 0.2rem 0.5rem; 260 + cursor: pointer; 261 + transition: all 0.15s ease; 262 + border-radius: 2px; 263 + -webkit-tap-highlight-color: transparent; 264 + } 265 + 266 + .copy-btn:hover, 267 + .copy-btn:active { 268 + background: var(--surface-hover); 269 + border-color: var(--text-light); 270 + color: var(--text); 271 + } 272 + 273 + .copy-btn.copied { 274 + color: var(--text); 275 + border-color: var(--text); 276 + } 277 + 278 + .record-content { 279 + padding: 0.6rem; 280 + } 281 + 282 + .record-content pre { 283 + margin: 0; 284 + white-space: pre-wrap; 285 + word-break: break-word; 286 + line-height: 1.5; 287 + font-size: 0.625rem; 288 + } 289 + 290 + .load-more { 291 + margin-top: 0.5rem; 292 + padding: 0.4rem 0.6rem; 293 + background: var(--bg); 294 + border: 1px solid var(--border); 295 + color: var(--text); 296 + font-family: inherit; 297 + font-size: 0.65rem; 298 + cursor: pointer; 299 + width: 100%; 300 + transition: all 0.15s ease; 301 + -webkit-tap-highlight-color: transparent; 302 + border-radius: 2px; 303 + } 304 + 305 + .load-more:hover, 306 + .load-more:active { 307 + background: var(--surface-hover); 308 + border-color: var(--text-light); 309 + } 310 + 311 + @media (max-width: 768px) { 312 + .load-more { 313 + padding: 0.6rem 0.8rem; 314 + font-size: 0.7rem; 315 + } 316 + } 317 + 318 + #field.loading { 319 + position: fixed; 320 + inset: 0; 321 + display: flex; 322 + flex-direction: column; 323 + align-items: center; 324 + justify-content: center; 325 + gap: 1.5rem; 326 + z-index: 1000; 327 + background: var(--bg); 328 + } 329 + 330 + #field.loading~.identity { 331 + display: none; 332 + } 333 + 334 + .loading-spinner { 335 + width: 48px; 336 + height: 48px; 337 + border: 3px solid var(--border); 338 + border-top-color: var(--text); 339 + border-radius: 50%; 340 + animation: spin 0.8s linear infinite; 341 + } 342 + 343 + @keyframes spin { 344 + to { 345 + transform: rotate(360deg); 346 + } 347 + } 348 + 349 + .loading-text { 350 + color: var(--text); 351 + font-size: 0.85rem; 352 + font-weight: 500; 353 + letter-spacing: 0.05em; 354 + } 355 + 356 + .loading-progress { 357 + color: var(--text-light); 358 + font-size: 0.7rem; 359 + }
+158
src/view/filter.css
···
··· 1 + .filter-panel { 2 + position: fixed; 3 + top: clamp(3.5rem, 7vmin, 4.5rem); 4 + right: clamp(1rem, 2vmin, 1.5rem); 5 + background: var(--surface); 6 + border: 1px solid var(--border); 7 + border-radius: 4px; 8 + padding: 1rem; 9 + z-index: 250; 10 + max-height: 60vh; 11 + overflow-y: auto; 12 + min-width: 200px; 13 + max-width: 280px; 14 + display: none; 15 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 16 + } 17 + 18 + @media (max-width: 768px) { 19 + .filter-panel { 20 + top: clamp(6rem, 12vmin, 8rem); 21 + } 22 + } 23 + 24 + @media (prefers-color-scheme: dark) { 25 + .filter-panel { 26 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 27 + } 28 + } 29 + 30 + .filter-panel.visible { 31 + display: block; 32 + } 33 + 34 + .filter-panel-header { 35 + display: flex; 36 + justify-content: space-between; 37 + align-items: center; 38 + margin-bottom: 0.75rem; 39 + padding-bottom: 0.5rem; 40 + border-bottom: 1px solid var(--border); 41 + } 42 + 43 + .filter-panel-title { 44 + font-size: 0.7rem; 45 + font-weight: 500; 46 + color: var(--text); 47 + text-transform: lowercase; 48 + } 49 + 50 + .filter-panel-actions { 51 + display: flex; 52 + gap: 0.5rem; 53 + } 54 + 55 + .filter-action-btn { 56 + font-family: inherit; 57 + font-size: 0.6rem; 58 + color: var(--text-light); 59 + background: transparent; 60 + border: none; 61 + cursor: pointer; 62 + padding: 0.2rem 0; 63 + transition: color 0.2s ease; 64 + } 65 + 66 + .filter-action-btn:hover { 67 + color: var(--text); 68 + } 69 + 70 + .filter-list { 71 + display: flex; 72 + flex-direction: column; 73 + gap: 0.25rem; 74 + } 75 + 76 + .filter-item { 77 + display: flex; 78 + align-items: center; 79 + gap: 0.5rem; 80 + padding: 0.4rem 0.5rem; 81 + border-radius: 2px; 82 + cursor: pointer; 83 + transition: background 0.15s ease; 84 + } 85 + 86 + .filter-item:hover { 87 + background: var(--surface-hover); 88 + } 89 + 90 + .filter-checkbox { 91 + width: 14px; 92 + height: 14px; 93 + border: 1px solid var(--border); 94 + border-radius: 2px; 95 + background: var(--bg); 96 + display: flex; 97 + align-items: center; 98 + justify-content: center; 99 + flex-shrink: 0; 100 + transition: all 0.15s ease; 101 + } 102 + 103 + .filter-item.checked .filter-checkbox { 104 + background: var(--text); 105 + border-color: var(--text); 106 + } 107 + 108 + .filter-checkbox-icon { 109 + width: 10px; 110 + height: 10px; 111 + stroke: var(--bg); 112 + stroke-width: 2; 113 + opacity: 0; 114 + transition: opacity 0.15s ease; 115 + } 116 + 117 + .filter-item.checked .filter-checkbox-icon { 118 + opacity: 1; 119 + } 120 + 121 + .filter-label { 122 + font-size: 0.7rem; 123 + color: var(--text-lighter); 124 + overflow: hidden; 125 + text-overflow: ellipsis; 126 + white-space: nowrap; 127 + } 128 + 129 + .filter-item.checked .filter-label { 130 + color: var(--text); 131 + } 132 + 133 + .app-view.filtered { 134 + display: none !important; 135 + } 136 + 137 + @keyframes pulse { 138 + 0%, 139 + 100% { 140 + opacity: 1; 141 + } 142 + 50% { 143 + opacity: 0.3; 144 + } 145 + } 146 + 147 + @keyframes gentle-pulse { 148 + 0%, 149 + 100% { 150 + transform: scale(1); 151 + box-shadow: 0 0 0 0 var(--text-light); 152 + } 153 + 50% { 154 + transform: scale(1.02); 155 + box-shadow: 0 0 0 3px rgba(160, 160, 160, 0.2); 156 + } 157 + } 158 +
+245
src/view/filters.js
···
··· 1 + // ============================================================================ 2 + // FILTER PANEL 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + import { applyDomainRedirect } from './atproto.js'; 7 + 8 + function loadHiddenApps() { 9 + const params = new URLSearchParams(window.location.search); 10 + const hideParam = params.get('hide'); 11 + if (hideParam) { 12 + state.hiddenApps = new Set(hideParam.split(',').filter(Boolean)); 13 + return; 14 + } 15 + 16 + try { 17 + const stored = localStorage.getItem(`atme_hidden_apps_${state.did}`); 18 + if (stored) { 19 + state.hiddenApps = new Set(JSON.parse(stored)); 20 + } 21 + } catch (e) { 22 + state.hiddenApps = new Set(); 23 + } 24 + } 25 + 26 + function saveHiddenApps() { 27 + try { 28 + localStorage.setItem(`atme_hidden_apps_${state.did}`, JSON.stringify([...state.hiddenApps])); 29 + } catch (e) {} 30 + 31 + const params = new URLSearchParams(window.location.search); 32 + if (state.hiddenApps.size > 0) { 33 + params.set('hide', [...state.hiddenApps].join(',')); 34 + } else { 35 + params.delete('hide'); 36 + } 37 + const newUrl = params.toString() 38 + ? `${window.location.pathname}?${params.toString()}` 39 + : window.location.pathname; 40 + history.replaceState(null, '', newUrl); 41 + } 42 + 43 + function updateFilterButton() { 44 + const filterBtn = document.getElementById('filterBtn'); 45 + const filterCount = document.getElementById('filterCount'); 46 + 47 + if (state.hiddenApps.size > 0) { 48 + filterBtn.classList.add('has-filters'); 49 + filterCount.textContent = state.hiddenApps.size; 50 + filterCount.style.display = 'inline-block'; 51 + } else { 52 + filterBtn.classList.remove('has-filters'); 53 + filterCount.style.display = 'none'; 54 + } 55 + } 56 + 57 + export function applyFilters() { 58 + const appViews = document.querySelectorAll('.app-view'); 59 + appViews.forEach(view => { 60 + const circle = view.querySelector('.app-circle'); 61 + if (circle) { 62 + const namespace = circle.dataset.namespace; 63 + if (state.hiddenApps.has(namespace)) { 64 + view.classList.add('filtered'); 65 + } else { 66 + view.classList.remove('filtered'); 67 + } 68 + } 69 + }); 70 + updateFilterButton(); 71 + saveHiddenApps(); 72 + // Reposition visible apps in a circle 73 + repositionAppCircles(); 74 + } 75 + 76 + function populateFilterList() { 77 + if (!state.globalApps) return; 78 + 79 + const filterList = document.getElementById('filterList'); 80 + const appNames = Object.keys(state.globalApps).filter(k => k !== '_circleSize').sort(); 81 + 82 + filterList.innerHTML = appNames.map(namespace => { 83 + const rawDisplayName = namespace.split('.').reverse().join('.'); 84 + const displayName = applyDomainRedirect(rawDisplayName); 85 + const isChecked = !state.hiddenApps.has(namespace); 86 + const isInvalid = state.invalidApps.has(namespace); 87 + return ` 88 + <div class="filter-item ${isChecked ? 'checked' : ''}" data-namespace="${namespace}"> 89 + <div class="filter-checkbox"> 90 + <svg class="filter-checkbox-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> 91 + <polyline points="20 6 9 17 4 12"></polyline> 92 + </svg> 93 + </div> 94 + <span class="filter-label">${displayName}${isInvalid ? ' (unresolved)' : ''}</span> 95 + </div> 96 + `; 97 + }).join(''); 98 + 99 + filterList.querySelectorAll('.filter-item').forEach(item => { 100 + item.addEventListener('click', () => { 101 + const namespace = item.dataset.namespace; 102 + if (state.hiddenApps.has(namespace)) { 103 + state.hiddenApps.delete(namespace); 104 + item.classList.add('checked'); 105 + } else { 106 + state.hiddenApps.add(namespace); 107 + item.classList.remove('checked'); 108 + } 109 + applyFilters(); 110 + }); 111 + }); 112 + } 113 + 114 + export function initFilterPanel() { 115 + loadHiddenApps(); 116 + 117 + // Clean up stale hidden apps that no longer exist 118 + if (state.globalApps) { 119 + const validNamespaces = new Set(Object.keys(state.globalApps).filter(k => k !== '_circleSize')); 120 + state.hiddenApps = new Set([...state.hiddenApps].filter(ns => validNamespaces.has(ns))); 121 + } 122 + 123 + const filterBtn = document.getElementById('filterBtn'); 124 + const filterPanel = document.getElementById('filterPanel'); 125 + const filterShowAll = document.getElementById('filterShowAll'); 126 + const filterHideAll = document.getElementById('filterHideAll'); 127 + const filterHideUnresolved = document.getElementById('filterHideUnresolved'); 128 + 129 + filterBtn.addEventListener('click', (e) => { 130 + e.stopPropagation(); 131 + filterPanel.classList.toggle('visible'); 132 + filterBtn.classList.toggle('active'); 133 + if (filterPanel.classList.contains('visible')) { 134 + populateFilterList(); 135 + } 136 + }); 137 + 138 + document.addEventListener('click', (e) => { 139 + if (!filterPanel.contains(e.target) && e.target !== filterBtn && !filterBtn.contains(e.target)) { 140 + filterPanel.classList.remove('visible'); 141 + filterBtn.classList.remove('active'); 142 + } 143 + }); 144 + 145 + filterShowAll.addEventListener('click', (e) => { 146 + e.preventDefault(); 147 + e.stopPropagation(); 148 + state.hiddenApps.clear(); 149 + populateFilterList(); 150 + applyFilters(); 151 + }); 152 + 153 + filterHideAll.addEventListener('click', (e) => { 154 + e.preventDefault(); 155 + e.stopPropagation(); 156 + if (!state.globalApps) return; 157 + const appNames = Object.keys(state.globalApps).filter(k => k !== '_circleSize'); 158 + state.hiddenApps = new Set(appNames); 159 + populateFilterList(); 160 + applyFilters(); 161 + }); 162 + 163 + // "valid" button - hide unresolved/invalid apps, show valid ones 164 + filterHideUnresolved.addEventListener('click', (e) => { 165 + e.preventDefault(); 166 + e.stopPropagation(); 167 + if (!state.globalApps) return; 168 + // Hide only invalid apps, show all valid ones 169 + state.hiddenApps = new Set(state.invalidApps); 170 + populateFilterList(); 171 + applyFilters(); 172 + }); 173 + 174 + applyFilters(); 175 + } 176 + 177 + export function repositionAppCircles() { 178 + if (!state.globalApps) return; 179 + 180 + const allAppViews = document.querySelectorAll('.app-view'); 181 + // Get only visible (non-filtered) app views 182 + const visibleAppViews = Array.from(allAppViews).filter(view => !view.classList.contains('filtered')); 183 + const visibleCount = visibleAppViews.length; 184 + 185 + if (visibleCount === 0) return; 186 + 187 + const vmin = Math.min(window.innerWidth, window.innerHeight); 188 + const isMobile = window.innerWidth < 768; 189 + 190 + let circleSize, radius; 191 + if (isMobile) { 192 + if (visibleCount <= 5) { 193 + circleSize = Math.min(60, vmin * 0.08); 194 + radius = vmin * 0.38; 195 + } else if (visibleCount <= 10) { 196 + circleSize = Math.min(50, vmin * 0.07); 197 + radius = vmin * 0.4; 198 + } else if (visibleCount <= 20) { 199 + circleSize = Math.min(40, vmin * 0.055); 200 + radius = vmin * 0.42; 201 + } else { 202 + circleSize = Math.min(32, vmin * 0.045); 203 + radius = vmin * 0.44; 204 + } 205 + circleSize = Math.max(circleSize, 28); 206 + radius = Math.max(radius, 120); 207 + } else { 208 + if (visibleCount <= 5) { 209 + circleSize = Math.min(70, vmin * 0.1); 210 + } else if (visibleCount <= 10) { 211 + circleSize = Math.min(60, vmin * 0.09); 212 + } else if (visibleCount <= 20) { 213 + circleSize = Math.min(50, vmin * 0.07); 214 + } else { 215 + circleSize = Math.min(40, vmin * 0.06); 216 + } 217 + circleSize = Math.max(circleSize, 35); 218 + // Calculate radius to ensure minimum spacing between apps 219 + // Arc length between apps should be at least circleSize + gap 220 + const minGap = 30; 221 + const minRadiusForSpacing = (visibleCount * (circleSize + minGap)) / (2 * Math.PI); 222 + radius = Math.max(vmin * 0.35, minRadiusForSpacing, 150); 223 + } 224 + 225 + const centerX = window.innerWidth / 2; 226 + const centerY = window.innerHeight / 2; 227 + 228 + // Position only visible apps evenly around the circle 229 + visibleAppViews.forEach((div, i) => { 230 + const angle = (i / visibleCount) * 2 * Math.PI - Math.PI / 2; 231 + const circleOffset = circleSize / 2; 232 + const x = centerX + radius * Math.cos(angle) - circleOffset; 233 + const y = centerY + radius * Math.sin(angle) - circleOffset; 234 + 235 + div.style.left = `${x}px`; 236 + div.style.top = `${y}px`; 237 + 238 + const circle = div.querySelector('.app-circle'); 239 + if (circle) { 240 + circle.style.width = `${circleSize}px`; 241 + circle.style.height = `${circleSize}px`; 242 + circle.style.fontSize = `${circleSize * 0.4}px`; 243 + } 244 + }); 245 + }
+58
src/view/firehose.css
···
··· 1 + .firehose-toast { 2 + position: fixed; 3 + top: clamp(4rem, 8vmin, 5rem); 4 + right: clamp(1rem, 2vmin, 1.5rem); 5 + background: var(--surface); 6 + border: 1px solid var(--border); 7 + padding: 0.75rem 1rem; 8 + border-radius: 4px; 9 + font-size: 0.7rem; 10 + color: var(--text); 11 + z-index: 200; 12 + opacity: 0; 13 + transform: translateY(-10px); 14 + transition: all 0.3s ease; 15 + pointer-events: none; 16 + max-width: min(300px, calc(100vw - 2rem)); 17 + width: max-content; 18 + } 19 + 20 + @media (max-width: 768px) { 21 + .firehose-toast { 22 + top: clamp(7rem, 14vmin, 9rem); 23 + } 24 + } 25 + 26 + .firehose-toast.visible { 27 + opacity: 1; 28 + transform: translateY(0); 29 + pointer-events: auto; 30 + } 31 + 32 + .firehose-toast-action { 33 + font-weight: 600; 34 + color: var(--text); 35 + } 36 + 37 + .firehose-toast-collection { 38 + color: var(--text-light); 39 + font-size: 0.65rem; 40 + margin-top: 0.25rem; 41 + } 42 + 43 + .firehose-toast-link { 44 + display: inline-block; 45 + color: var(--text-light); 46 + font-size: 0.6rem; 47 + margin-top: 0.5rem; 48 + text-decoration: none; 49 + border-bottom: 1px solid transparent; 50 + transition: all 0.2s ease; 51 + pointer-events: auto; 52 + } 53 + 54 + .firehose-toast-link:hover { 55 + color: var(--text); 56 + border-bottom-color: var(--text); 57 + } 58 +
+131
src/view/firehose.js
···
··· 1 + // ============================================================================ 2 + // FIREHOSE (Jetstream WebSocket) 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + import { 7 + createFirehoseParticle, 8 + initFirehoseCanvas, 9 + animateFirehoseParticles, 10 + cleanupFirehoseCanvas 11 + } from './particles.js'; 12 + 13 + function connectFirehose() { 14 + if (!state.did || state.jetstreamWs) return; 15 + 16 + const watchBtn = document.getElementById('watchLiveBtn'); 17 + const watchLabel = watchBtn.querySelector('.watch-label'); 18 + 19 + // Connect to Jetstream filtering by this DID 20 + const wsUrl = `wss://jetstream2.us-east.bsky.network/subscribe?wantedDids=${encodeURIComponent(state.did)}`; 21 + state.jetstreamWs = new WebSocket(wsUrl); 22 + 23 + state.jetstreamWs.onopen = () => { 24 + watchLabel.textContent = 'watching...'; 25 + watchBtn.classList.add('active'); 26 + }; 27 + 28 + state.jetstreamWs.onmessage = (event) => { 29 + try { 30 + const data = JSON.parse(event.data); 31 + if (data.kind === 'commit' && data.commit) { 32 + const commit = data.commit; 33 + const collection = commit.collection; 34 + const operation = commit.operation; 35 + 36 + // Get namespace from collection 37 + const parts = collection.split('.'); 38 + const namespace = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : collection; 39 + 40 + const eventData = { 41 + action: operation, 42 + collection: collection, 43 + namespace: namespace, 44 + rkey: commit.rkey 45 + }; 46 + 47 + // Create particle animation 48 + createFirehoseParticle(eventData); 49 + 50 + // Show toast notification 51 + showFirehoseToast(eventData); 52 + } 53 + } catch (e) { 54 + console.error('Error processing Jetstream message:', e); 55 + } 56 + }; 57 + 58 + state.jetstreamWs.onerror = (error) => { 59 + console.error('Jetstream error:', error); 60 + watchLabel.textContent = 'connection error'; 61 + }; 62 + 63 + state.jetstreamWs.onclose = () => { 64 + if (state.isWatchingLive) { 65 + watchLabel.textContent = 'reconnecting...'; 66 + setTimeout(() => { 67 + state.jetstreamWs = null; 68 + if (state.isWatchingLive) connectFirehose(); 69 + }, 3000); 70 + } 71 + }; 72 + } 73 + 74 + function disconnectFirehose() { 75 + if (state.jetstreamWs) { 76 + state.jetstreamWs.close(); 77 + state.jetstreamWs = null; 78 + } 79 + } 80 + 81 + function showFirehoseToast(event) { 82 + const toast = document.getElementById('firehoseToast'); 83 + const actionEl = toast.querySelector('.firehose-toast-action'); 84 + const collectionEl = toast.querySelector('.firehose-toast-collection'); 85 + const linkEl = document.getElementById('firehoseToastLink'); 86 + 87 + const actionText = { 88 + 'create': 'created', 89 + 'update': 'updated', 90 + 'delete': 'deleted' 91 + }[event.action] || event.action; 92 + 93 + actionEl.textContent = `${actionText} record`; 94 + collectionEl.innerHTML = `<code style="background: var(--bg); padding: 0.1rem 0.3rem; border-radius: 2px; font-size: 0.6rem;">${event.collection}</code>`; 95 + 96 + if (event.action === 'delete') { 97 + linkEl.style.display = 'none'; 98 + } else { 99 + linkEl.style.display = 'inline-block'; 100 + if (state.globalPds && event.rkey) { 101 + linkEl.href = `${state.globalPds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(state.did)}&collection=${encodeURIComponent(event.collection)}&rkey=${encodeURIComponent(event.rkey)}`; 102 + } 103 + } 104 + 105 + toast.classList.add('visible'); 106 + setTimeout(() => { 107 + toast.classList.remove('visible'); 108 + }, 4000); 109 + } 110 + 111 + export function initFirehoseUI() { 112 + // Watch live button handler 113 + document.getElementById('watchLiveBtn').addEventListener('click', () => { 114 + const watchBtn = document.getElementById('watchLiveBtn'); 115 + const watchLabel = watchBtn.querySelector('.watch-label'); 116 + 117 + if (state.isWatchingLive) { 118 + state.isWatchingLive = false; 119 + watchLabel.textContent = 'watch live'; 120 + watchBtn.classList.remove('active'); 121 + disconnectFirehose(); 122 + cleanupFirehoseCanvas(); 123 + } else { 124 + state.isWatchingLive = true; 125 + watchLabel.textContent = 'connecting...'; 126 + initFirehoseCanvas(); 127 + animateFirehoseParticles(); 128 + connectFirehose(); 129 + } 130 + }); 131 + }
+71
src/view/guestbook-state.js
···
··· 1 + // ============================================================================ 2 + // GUESTBOOK STATE (via Microcosm UFOs API) 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + 7 + const UFOS_API = 'https://ufos-api.microcosm.blue'; 8 + export let allGuestbookSignatures = []; 9 + 10 + export async function fetchAllGuestbookSignatures() { 11 + try { 12 + const response = await fetch(`${UFOS_API}/records?collection=app.at-me.visit`); 13 + if (response.ok) { 14 + allGuestbookSignatures = await response.json(); 15 + } 16 + } catch (e) { 17 + console.error('Error fetching guestbook signatures from Microcosm:', e); 18 + } 19 + } 20 + 21 + export async function checkGuestbookState() { 22 + if (!state.did) return; 23 + 24 + // Fetch all signatures if we haven't yet 25 + if (allGuestbookSignatures.length === 0) { 26 + await fetchAllGuestbookSignatures(); 27 + } 28 + 29 + // Check if the viewed user has signed the guestbook 30 + state.pageOwnerHasSigned = allGuestbookSignatures.some(sig => sig.did === state.did); 31 + 32 + updateGuestbookUI(); 33 + } 34 + 35 + export function updateGuestbookUI() { 36 + const signEl = document.querySelector('.guestbook-sign'); 37 + const signBtn = document.getElementById('signGuestbookBtn'); 38 + const avatarImg = document.getElementById('guestbookAvatar'); 39 + const iconSpan = signBtn?.querySelector('.guestbook-icon'); 40 + const textSpan = signBtn?.querySelector('.guestbook-text'); 41 + 42 + if (signEl) { 43 + signEl.textContent = state.pageOwnerHasSigned ? 'you already signed' : 'sign the guest list'; 44 + } 45 + 46 + if (!signBtn || !iconSpan || !textSpan) return; 47 + 48 + signBtn.classList.remove('signed', 'pulse'); 49 + 50 + if (state.pageOwnerHasSigned) { 51 + if (avatarImg) avatarImg.style.display = 'none'; 52 + iconSpan.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; 53 + iconSpan.style.display = 'flex'; 54 + textSpan.textContent = 'signed'; 55 + signBtn.classList.add('signed'); 56 + signBtn.setAttribute('title', 'this user has signed the guestbook'); 57 + } else { 58 + // Show the viewed user's avatar 59 + if (state.viewedAvatar && avatarImg) { 60 + avatarImg.src = state.viewedAvatar; 61 + avatarImg.style.display = 'block'; 62 + iconSpan.style.display = 'none'; 63 + } else { 64 + if (avatarImg) avatarImg.style.display = 'none'; 65 + iconSpan.style.display = 'none'; 66 + } 67 + textSpan.textContent = 'sign as'; 68 + signBtn.classList.add('pulse'); 69 + signBtn.setAttribute('title', `sign in as @${state.globalHandle || 'user'}`); 70 + } 71 + }
+78
src/view/guestbook-ui.js
···
··· 1 + // ============================================================================ 2 + // GUESTBOOK (simplified - just viewing, no OAuth write for now) 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + import { escapeHtml } from './atproto.js'; 7 + import { allGuestbookSignatures, fetchAllGuestbookSignatures } from './guestbook-state.js'; 8 + 9 + export function initGuestbookUI() { 10 + document.getElementById('viewGuestbookBtn').addEventListener('click', async () => { 11 + const modal = document.getElementById('guestbookModal'); 12 + const content = document.getElementById('guestbookContent'); 13 + 14 + modal.classList.add('visible'); 15 + content.innerHTML = ` 16 + <div class="guestbook-loading"> 17 + <div class="guestbook-loading-spinner"></div> 18 + <div class="guestbook-loading-text">loading signatures...</div> 19 + </div> 20 + `; 21 + 22 + // Fetch all guestbook signatures from Microcosm UFOs API 23 + if (allGuestbookSignatures.length === 0) { 24 + await fetchAllGuestbookSignatures(); 25 + } 26 + 27 + if (allGuestbookSignatures.length > 0) { 28 + // Sort by time_us descending (most recent first) 29 + const sorted = [...allGuestbookSignatures].sort((a, b) => b.time_us - a.time_us); 30 + 31 + content.innerHTML = ` 32 + <div class="guestbook-paper"> 33 + <div class="guestbook-paper-title">guestbook</div> 34 + <div class="guestbook-paper-subtitle">visitors who have signed</div> 35 + <div class="guestbook-tally">${sorted.length} signature${sorted.length !== 1 ? 's' : ''}</div> 36 + <div class="guestbook-signatures-list"> 37 + ${sorted.map(sig => ` 38 + <div class="guestbook-paper-signature"> 39 + <a class="guestbook-did" href="?did=${encodeURIComponent(sig.did)}" title="view ${sig.did}">${sig.did}</a> 40 + ${sig.record?.text ? `<div class="guestbook-message">${escapeHtml(sig.record.text)}</div>` : ''} 41 + </div> 42 + `).join('')} 43 + </div> 44 + </div> 45 + `; 46 + } else { 47 + content.innerHTML = ` 48 + <div class="guestbook-paper"> 49 + <div class="guestbook-paper-title">guestbook</div> 50 + <div class="guestbook-paper-subtitle">no signatures yet</div> 51 + <div class="guestbook-tally">be the first to sign!</div> 52 + </div> 53 + `; 54 + } 55 + }); 56 + 57 + document.getElementById('guestbookClose').addEventListener('click', () => { 58 + document.getElementById('guestbookModal').classList.remove('visible'); 59 + }); 60 + 61 + // Escape key to close guestbook modal 62 + document.addEventListener('keydown', (e) => { 63 + if (e.key === 'Escape') { 64 + document.getElementById('guestbookModal').classList.remove('visible'); 65 + } 66 + }); 67 + 68 + // Sign guestbook button - for now just show a message about OAuth 69 + document.getElementById('signGuestbookBtn').addEventListener('click', () => { 70 + if (state.pageOwnerHasSigned) { 71 + // Already signed - show the guestbook 72 + document.getElementById('viewGuestbookBtn').click(); 73 + } else { 74 + // Not signed - would need OAuth 75 + alert(`OAuth signing coming soon! For now, visit your own PDS to create an app.at-me.visit record.`); 76 + } 77 + }); 78 + }
+405
src/view/guestbook.css
···
··· 1 + .guestbook-buttons-container { 2 + position: fixed; 3 + bottom: clamp(0.75rem, 2vmin, 1rem); 4 + right: clamp(0.75rem, 2vmin, 1rem); 5 + display: flex; 6 + flex-direction: row; 7 + align-items: center; 8 + gap: clamp(0.5rem, 1.5vmin, 0.75rem); 9 + z-index: 100; 10 + } 11 + 12 + .view-guestbook-btn { 13 + font-family: inherit; 14 + font-size: clamp(0.85rem, 1.8vmin, 1rem); 15 + color: var(--text-light); 16 + border: 1px solid var(--border); 17 + background: var(--bg); 18 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem); 19 + transition: all 0.2s ease; 20 + cursor: pointer; 21 + border-radius: 2px; 22 + width: clamp(32px, 7vmin, 40px); 23 + height: clamp(32px, 7vmin, 40px); 24 + display: flex; 25 + align-items: center; 26 + justify-content: center; 27 + } 28 + 29 + .view-guestbook-btn:hover, 30 + .view-guestbook-btn:active { 31 + background: var(--surface); 32 + color: var(--text); 33 + border-color: var(--text-light); 34 + } 35 + 36 + .sign-guestbook-btn { 37 + font-family: inherit; 38 + font-size: clamp(0.65rem, 1.4vmin, 0.75rem); 39 + color: var(--text-light); 40 + border: 1px solid var(--border); 41 + background: var(--bg); 42 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem); 43 + transition: all 0.2s ease; 44 + cursor: pointer; 45 + border-radius: 2px; 46 + display: flex; 47 + align-items: center; 48 + gap: clamp(0.3rem, 0.8vmin, 0.5rem); 49 + height: clamp(32px, 7vmin, 40px); 50 + white-space: nowrap; 51 + } 52 + 53 + .sign-guestbook-btn:hover, 54 + .sign-guestbook-btn:active { 55 + background: var(--surface); 56 + color: var(--text); 57 + border-color: var(--text-light); 58 + } 59 + 60 + .sign-guestbook-btn:disabled { 61 + opacity: 0.5; 62 + cursor: not-allowed; 63 + } 64 + 65 + .sign-guestbook-btn.signed { 66 + border-color: var(--text-light); 67 + background: var(--surface); 68 + } 69 + 70 + .sign-guestbook-btn.pulse { 71 + animation: gentle-pulse 2s ease-in-out infinite; 72 + } 73 + 74 + .guestbook-icon { 75 + display: flex; 76 + align-items: center; 77 + line-height: 1; 78 + } 79 + 80 + .guestbook-avatar { 81 + width: clamp(20px, 4.5vmin, 24px); 82 + height: clamp(20px, 4.5vmin, 24px); 83 + border-radius: 50%; 84 + object-fit: cover; 85 + border: 1px solid var(--border); 86 + flex-shrink: 0; 87 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.1); 88 + } 89 + 90 + @media (prefers-color-scheme: dark) { 91 + .guestbook-avatar { 92 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); 93 + } 94 + } 95 + 96 + .guestbook-modal { 97 + position: fixed; 98 + inset: 0; 99 + background: var(--bg); 100 + z-index: 2000; 101 + display: none; 102 + overflow-y: auto; 103 + padding: clamp(4rem, 8vmin, 6rem) clamp(1rem, 3vmin, 2rem) clamp(2rem, 4vmin, 3rem); 104 + } 105 + 106 + .guestbook-modal.visible { 107 + display: block; 108 + } 109 + 110 + .guestbook-paper { 111 + max-width: 700px; 112 + margin: 0 auto; 113 + background: 114 + repeating-linear-gradient(0deg, 115 + transparent, 116 + transparent 31px, 117 + rgba(212, 197, 168, 0.15) 31px, 118 + rgba(212, 197, 168, 0.15) 32px), 119 + linear-gradient(to bottom, #fdfcf8 0%, #f9f7f1 100%); 120 + border: 1px solid #d4c5a8; 121 + box-shadow: 122 + 0 4px 6px rgba(0, 0, 0, 0.1), 123 + 0 2px 4px rgba(0, 0, 0, 0.06), 124 + inset 0 0 80px rgba(255, 248, 240, 0.6); 125 + padding: clamp(2.5rem, 6vmin, 4rem) clamp(2rem, 5vmin, 3rem); 126 + position: relative; 127 + } 128 + 129 + @media (prefers-color-scheme: dark) { 130 + .guestbook-paper { 131 + background: 132 + repeating-linear-gradient(0deg, 133 + transparent, 134 + transparent 31px, 135 + rgba(90, 80, 70, 0.2) 31px, 136 + rgba(90, 80, 70, 0.2) 32px), 137 + linear-gradient(to bottom, #2a2520 0%, #1f1b17 100%); 138 + border-color: #3a3530; 139 + box-shadow: 140 + 0 4px 6px rgba(0, 0, 0, 0.5), 141 + 0 2px 4px rgba(0, 0, 0, 0.3), 142 + inset 0 0 80px rgba(60, 50, 40, 0.4); 143 + } 144 + } 145 + 146 + .guestbook-paper::before { 147 + content: ''; 148 + position: absolute; 149 + top: 0; 150 + left: clamp(2rem, 5vmin, 3rem); 151 + width: 2px; 152 + height: 100%; 153 + background: linear-gradient(to bottom, 154 + transparent 0%, 155 + rgba(212, 100, 100, 0.2) 5%, 156 + rgba(212, 100, 100, 0.2) 95%, 157 + transparent 100%); 158 + } 159 + 160 + @media (prefers-color-scheme: dark) { 161 + .guestbook-paper::before { 162 + background: linear-gradient(to bottom, 163 + transparent 0%, 164 + rgba(180, 80, 80, 0.15) 5%, 165 + rgba(180, 80, 80, 0.15) 95%, 166 + transparent 100%); 167 + } 168 + } 169 + 170 + .guestbook-paper-title { 171 + font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 172 + font-size: clamp(1.8rem, 4.5vmin, 2.5rem); 173 + color: #3a2f25; 174 + text-align: center; 175 + margin-bottom: clamp(0.5rem, 1.5vmin, 1rem); 176 + font-weight: 500; 177 + letter-spacing: 0.02em; 178 + } 179 + 180 + @media (prefers-color-scheme: dark) { 181 + .guestbook-paper-title { 182 + color: #d4c5a8; 183 + } 184 + } 185 + 186 + .guestbook-paper-subtitle { 187 + font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 188 + font-size: clamp(0.75rem, 1.6vmin, 0.9rem); 189 + color: #6b5d4f; 190 + text-align: center; 191 + margin-bottom: clamp(2rem, 5vmin, 3rem); 192 + font-style: normal; 193 + } 194 + 195 + @media (prefers-color-scheme: dark) { 196 + .guestbook-paper-subtitle { 197 + color: #8a7a6a; 198 + } 199 + } 200 + 201 + .guestbook-tally { 202 + font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace; 203 + text-align: center; 204 + font-size: clamp(0.7rem, 1.8vmin, 0.85rem); 205 + color: #6b5d4f; 206 + margin: clamp(1rem, 2.5vmin, 1.5rem) 0 0; 207 + font-weight: 500; 208 + letter-spacing: 0.03em; 209 + text-transform: lowercase; 210 + } 211 + 212 + @media (prefers-color-scheme: dark) { 213 + .guestbook-tally { 214 + color: #8a7a6a; 215 + } 216 + } 217 + 218 + .guestbook-signatures-list { 219 + margin-top: clamp(1.5rem, 4vmin, 2.5rem); 220 + } 221 + 222 + .guestbook-paper-signature { 223 + padding: clamp(1rem, 2.5vmin, 1.5rem) 0; 224 + border-bottom: 1px solid rgba(212, 197, 168, 0.3); 225 + position: relative; 226 + cursor: pointer; 227 + transition: all 0.3s ease; 228 + } 229 + 230 + .guestbook-paper-signature:last-child { 231 + border-bottom: none; 232 + } 233 + 234 + .guestbook-paper-signature:hover { 235 + background: rgba(255, 248, 240, 0.3); 236 + padding-left: 0.5rem; 237 + padding-right: 0.5rem; 238 + margin-left: -0.5rem; 239 + margin-right: -0.5rem; 240 + } 241 + 242 + @media (prefers-color-scheme: dark) { 243 + .guestbook-paper-signature { 244 + border-bottom-color: rgba(90, 80, 70, 0.3); 245 + } 246 + 247 + .guestbook-paper-signature:hover { 248 + background: rgba(60, 50, 40, 0.3); 249 + } 250 + } 251 + 252 + .guestbook-did { 253 + font-family: 'Brush Script MT', cursive, 'Georgia', serif; 254 + font-size: clamp(1.1rem, 2.5vmin, 1.4rem); 255 + color: #2a2520; 256 + margin-bottom: clamp(0.3rem, 0.8vmin, 0.5rem); 257 + font-weight: 400; 258 + letter-spacing: 0.02em; 259 + word-break: break-all; 260 + cursor: pointer; 261 + transition: all 0.2s ease; 262 + position: relative; 263 + } 264 + 265 + .guestbook-did:hover { 266 + color: #4a4238; 267 + transform: translateX(2px); 268 + } 269 + 270 + @media (prefers-color-scheme: dark) { 271 + .guestbook-did { 272 + color: #c9bfa8; 273 + } 274 + 275 + .guestbook-did:hover { 276 + color: #d4c5a8; 277 + } 278 + } 279 + 280 + .guestbook-message { 281 + font-size: 0.65rem; 282 + color: #6b6052; 283 + font-style: italic; 284 + margin-top: 0.2rem; 285 + padding-left: 0.5rem; 286 + border-left: 2px solid #d4c5a866; 287 + } 288 + 289 + @media (prefers-color-scheme: dark) { 290 + .guestbook-message { 291 + color: #a89f8c; 292 + border-left-color: #c9bfa866; 293 + } 294 + } 295 + 296 + .guestbook-close { 297 + position: fixed; 298 + top: clamp(1rem, 2vmin, 1.5rem); 299 + right: clamp(1rem, 2vmin, 1.5rem); 300 + width: clamp(40px, 8vmin, 48px); 301 + height: clamp(40px, 8vmin, 48px); 302 + border: 2px solid var(--border); 303 + background: var(--surface); 304 + color: var(--text-light); 305 + cursor: pointer; 306 + display: flex; 307 + align-items: center; 308 + justify-content: center; 309 + font-size: clamp(1.2rem, 3vmin, 1.5rem); 310 + line-height: 1; 311 + transition: all 0.2s ease; 312 + border-radius: 4px; 313 + z-index: 2001; 314 + } 315 + 316 + .guestbook-close:hover, 317 + .guestbook-close:active { 318 + background: var(--surface-hover); 319 + border-color: var(--text-light); 320 + color: var(--text); 321 + } 322 + 323 + .guestbook-loading { 324 + max-width: 800px; 325 + margin: 0 auto; 326 + text-align: center; 327 + padding: clamp(3rem, 8vmin, 5rem) clamp(1rem, 3vmin, 2rem); 328 + } 329 + 330 + .guestbook-loading-spinner { 331 + width: 40px; 332 + height: 40px; 333 + border: 3px solid var(--border); 334 + border-top-color: var(--text); 335 + border-radius: 50%; 336 + animation: spin 0.8s linear infinite; 337 + margin: 0 auto clamp(1rem, 3vmin, 1.5rem); 338 + } 339 + 340 + .guestbook-loading-text { 341 + font-size: clamp(0.75rem, 1.6vmin, 0.9rem); 342 + color: var(--text-light); 343 + } 344 + 345 + .guestbook-sign { 346 + position: fixed; 347 + bottom: clamp(3.5rem, 8.5vmin, 5.5rem); 348 + right: clamp(0.75rem, 2vmin, 1rem); 349 + font-family: ui-monospace, 'SF Mono', Monaco, monospace; 350 + font-size: clamp(0.6rem, 1.3vmin, 0.7rem); 351 + color: var(--text-light); 352 + text-transform: lowercase; 353 + letter-spacing: 0.1em; 354 + z-index: 50; 355 + opacity: 0.6; 356 + text-shadow: 0 0 4px currentColor; 357 + animation: neon-flicker 8s infinite; 358 + pointer-events: none; 359 + user-select: none; 360 + white-space: nowrap; 361 + } 362 + 363 + @media (prefers-color-scheme: dark) { 364 + .guestbook-sign { 365 + color: #ff6b9d; 366 + opacity: 0.5; 367 + text-shadow: 0 0 6px currentColor, 0 0 12px rgba(255, 107, 157, 0.3); 368 + } 369 + } 370 + 371 + .pov-indicator { 372 + position: fixed; 373 + left: 50%; 374 + top: clamp(1rem, 2vmin, 1.5rem); 375 + transform: translateX(-50%); 376 + font-family: ui-monospace, 'SF Mono', Monaco, monospace; 377 + font-size: clamp(0.65rem, 1.4vmin, 0.75rem); 378 + color: var(--text-light); 379 + text-transform: lowercase; 380 + letter-spacing: 0.12em; 381 + z-index: 50; 382 + opacity: 0.4; 383 + text-shadow: 0 0 3px currentColor; 384 + animation: pov-subtle-flicker 37s infinite; 385 + pointer-events: none; 386 + user-select: none; 387 + text-align: center; 388 + line-height: 1.4; 389 + } 390 + 391 + .pov-handle { 392 + display: inline; 393 + margin-left: 0.3rem; 394 + font-size: inherit; 395 + opacity: 0.9; 396 + pointer-events: auto; 397 + text-decoration: none; 398 + color: inherit; 399 + transition: opacity 0.2s ease; 400 + } 401 + 402 + .pov-handle:hover { 403 + opacity: 1; 404 + text-decoration: underline; 405 + }
+173
src/view/layout.css
···
··· 1 + .identity { 2 + position: absolute; 3 + left: 50%; 4 + top: 50%; 5 + transform: translate(-50%, -50%); 6 + background: var(--surface); 7 + border: 2px solid var(--text-light); 8 + border-radius: 50%; 9 + width: clamp(100px, 20vmin, 140px); 10 + height: clamp(100px, 20vmin, 140px); 11 + display: flex; 12 + flex-direction: column; 13 + align-items: center; 14 + justify-content: center; 15 + z-index: 10; 16 + cursor: pointer; 17 + transition: all 0.2s ease; 18 + -webkit-tap-highlight-color: transparent; 19 + } 20 + 21 + .identity:hover, 22 + .identity:active { 23 + transform: translate(-50%, -50%) scale(1.05); 24 + border-color: var(--text); 25 + box-shadow: 0 0 20px rgba(255, 255, 255, 0.1); 26 + } 27 + 28 + .identity.pulse { 29 + animation: identityPulse 0.3s ease-out; 30 + } 31 + 32 + @keyframes identityPulse { 33 + 0% { box-shadow: 0 0 0 0 rgba(139, 164, 184, 0.4); } 34 + 70% { box-shadow: 0 0 0 15px rgba(139, 164, 184, 0); } 35 + 100% { box-shadow: 0 0 0 0 rgba(139, 164, 184, 0); } 36 + } 37 + 38 + .identity-label { 39 + font-size: clamp(1rem, 2vmin, 1.2rem); 40 + color: var(--text); 41 + font-weight: 600; 42 + line-height: 1; 43 + } 44 + 45 + .identity-value { 46 + font-size: 0.7rem; 47 + color: var(--text-lighter); 48 + text-align: center; 49 + white-space: nowrap; 50 + font-weight: 400; 51 + line-height: 1.2; 52 + } 53 + 54 + .identity-value:hover { 55 + opacity: 0.7; 56 + } 57 + 58 + .identity-pds-label { 59 + position: absolute; 60 + bottom: clamp(-1.5rem, -3vmin, -2rem); 61 + font-size: clamp(0.55rem, 1.1vmin, 0.65rem); 62 + color: var(--text-light); 63 + letter-spacing: 0.05em; 64 + font-weight: 500; 65 + text-decoration: none; 66 + white-space: nowrap; 67 + transition: opacity 0.2s ease; 68 + } 69 + 70 + .identity-pds-label:hover { 71 + opacity: 0.7; 72 + } 73 + 74 + .identity-avatar { 75 + position: absolute; 76 + top: 0; 77 + left: 0; 78 + width: 100%; 79 + height: 100%; 80 + border-radius: 50%; 81 + object-fit: cover; 82 + } 83 + 84 + .identity-handle { 85 + position: absolute; 86 + bottom: -1.8rem; 87 + left: 50%; 88 + transform: translateX(-50%); 89 + font-size: clamp(0.7rem, 1.5vmin, 0.85rem); 90 + color: var(--text-light); 91 + white-space: nowrap; 92 + } 93 + 94 + .app-view { 95 + position: absolute; 96 + display: flex; 97 + flex-direction: column; 98 + align-items: center; 99 + gap: clamp(0.3rem, 1vmin, 0.5rem); 100 + cursor: pointer; 101 + transition: all 0.2s ease; 102 + opacity: 0.7; 103 + } 104 + 105 + .app-view:hover { 106 + opacity: 1; 107 + transform: scale(1.1); 108 + z-index: 100; 109 + } 110 + 111 + .app-circle { 112 + background: var(--surface-hover); 113 + border: 1px solid var(--border); 114 + border-radius: 50%; 115 + width: clamp(55px, 10vmin, 70px); 116 + height: clamp(55px, 10vmin, 70px); 117 + display: flex; 118 + align-items: center; 119 + justify-content: center; 120 + transition: all 0.2s ease; 121 + overflow: hidden; 122 + font-size: clamp(1rem, 2vmin, 1.5rem); 123 + } 124 + 125 + .app-logo { 126 + width: 100%; 127 + height: 100%; 128 + object-fit: cover; 129 + } 130 + 131 + .app-view:hover .app-circle { 132 + background: var(--surface); 133 + border-color: var(--text-light); 134 + } 135 + 136 + .app-name { 137 + font-size: clamp(0.55rem, 1.2vmin, 0.7rem); 138 + color: var(--text); 139 + text-align: center; 140 + max-width: clamp(70px, 15vmin, 120px); 141 + text-decoration: none; 142 + display: block; 143 + overflow: hidden; 144 + text-overflow: ellipsis; 145 + white-space: nowrap; 146 + } 147 + 148 + @media (max-width: 768px) { 149 + .app-name { 150 + font-size: clamp(0.5rem, 1vmin, 0.6rem); 151 + max-width: clamp(60px, 12vmin, 100px); 152 + } 153 + 154 + #field.many-apps .app-name { 155 + display: none; 156 + } 157 + } 158 + 159 + .app-name:hover { 160 + text-decoration: underline; 161 + color: var(--text); 162 + } 163 + 164 + .app-name.invalid-link { 165 + color: var(--text-light); 166 + opacity: 0.5; 167 + cursor: not-allowed; 168 + } 169 + 170 + .app-name.invalid-link:hover { 171 + text-decoration: none; 172 + color: var(--text-light); 173 + }
+133
src/view/main.js
···
··· 1 + // ============================================================================ 2 + // MAIN ENTRY POINT - View Page 3 + // ============================================================================ 4 + 5 + import './styles.css'; 6 + import { state, urlParams, paramDid, paramHandle } from './state.js'; 7 + import { 8 + resolveHandle, 9 + resolveDid, 10 + getPdsFromDidDoc, 11 + getHandleFromDidDoc, 12 + getProfile, 13 + describeRepo 14 + } from './atproto.js'; 15 + import { renderVisualization } from './visualization.js'; 16 + import { checkGuestbookState } from './guestbook-state.js'; 17 + import { initGuestbookUI } from './guestbook-ui.js'; 18 + import { initFirehoseUI } from './firehose.js'; 19 + 20 + // ============================================================================ 21 + // INITIALIZATION 22 + // ============================================================================ 23 + 24 + async function init() { 25 + const statusEl = document.getElementById('status'); 26 + 27 + try { 28 + // Get DID from URL params 29 + let did = paramDid; 30 + 31 + // If handle provided, resolve to DID 32 + if (!did && paramHandle) { 33 + statusEl.textContent = 'resolving handle...'; 34 + did = await resolveHandle(paramHandle); 35 + if (!did) { 36 + statusEl.textContent = 'could not resolve handle'; 37 + return; 38 + } 39 + } 40 + 41 + if (!did) { 42 + statusEl.textContent = 'no identity specified'; 43 + return; 44 + } 45 + 46 + // Store DID in state 47 + state.did = did; 48 + 49 + statusEl.textContent = 'resolving identity...'; 50 + 51 + // Resolve DID document 52 + const didDoc = await resolveDid(did); 53 + if (!didDoc) { 54 + statusEl.textContent = 'could not resolve DID'; 55 + return; 56 + } 57 + 58 + // Get PDS endpoint 59 + const pds = getPdsFromDidDoc(didDoc); 60 + if (!pds) { 61 + statusEl.textContent = 'could not find PDS'; 62 + return; 63 + } 64 + 65 + // Get handle from DID doc 66 + const handle = getHandleFromDidDoc(didDoc); 67 + state.globalPds = pds; 68 + state.globalHandle = handle || did; 69 + 70 + // Update identity display 71 + const handleEl = document.getElementById('handleDisplay'); 72 + if (handleEl) { 73 + handleEl.textContent = `@${state.globalHandle}`; 74 + } 75 + 76 + statusEl.textContent = 'loading profile...'; 77 + 78 + // Get profile for avatar 79 + const profile = await getProfile(did); 80 + if (profile?.avatar) { 81 + state.viewedAvatar = profile.avatar; 82 + const avatarEl = document.getElementById('identityAvatar'); 83 + if (avatarEl) { 84 + avatarEl.src = profile.avatar; 85 + avatarEl.style.display = 'block'; 86 + } 87 + } 88 + 89 + statusEl.textContent = 'discovering apps...'; 90 + 91 + // Describe repo to get collections 92 + const repoInfo = await describeRepo(pds, did); 93 + if (!repoInfo?.collections) { 94 + statusEl.textContent = 'could not load repository'; 95 + return; 96 + } 97 + 98 + // Group collections by namespace (first two parts) 99 + const apps = {}; 100 + repoInfo.collections.forEach(collection => { 101 + const parts = collection.split('.'); 102 + const namespace = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : collection; 103 + if (!apps[namespace]) apps[namespace] = []; 104 + apps[namespace].push(collection); 105 + }); 106 + 107 + state.globalApps = apps; 108 + 109 + // Hide status 110 + statusEl.style.display = 'none'; 111 + 112 + // Render visualization 113 + renderVisualization(apps, profile); 114 + 115 + // Initialize UI components 116 + initGuestbookUI(); 117 + initFirehoseUI(); 118 + 119 + // Check guestbook state 120 + await checkGuestbookState(); 121 + 122 + } catch (error) { 123 + console.error('Initialization error:', error); 124 + statusEl.textContent = 'an error occurred'; 125 + } 126 + } 127 + 128 + // Start the app when DOM is ready 129 + if (document.readyState === 'loading') { 130 + document.addEventListener('DOMContentLoaded', init); 131 + } else { 132 + init(); 133 + }
+150
src/view/mst.css
···
··· 1 + .mst-canvas { 2 + width: 100%; 3 + height: 600px; 4 + border: 1px solid var(--border); 5 + border-radius: 4px; 6 + background: var(--bg); 7 + margin-top: 0.5rem; 8 + } 9 + 10 + .mst-info { 11 + background: var(--bg); 12 + border: 1px solid var(--border); 13 + padding: 0.75rem; 14 + border-radius: 4px; 15 + margin-bottom: 0.75rem; 16 + } 17 + 18 + .mst-info p { 19 + font-size: 0.65rem; 20 + color: var(--text-lighter); 21 + line-height: 1.5; 22 + margin: 0; 23 + } 24 + 25 + .mst-node-modal { 26 + position: fixed; 27 + inset: 0; 28 + background: rgba(0, 0, 0, 0.75); 29 + display: flex; 30 + align-items: center; 31 + justify-content: center; 32 + z-index: 3000; 33 + padding: 1rem; 34 + } 35 + 36 + .mst-node-modal-content { 37 + background: var(--surface); 38 + border: 2px solid var(--border); 39 + padding: 2rem; 40 + border-radius: 4px; 41 + max-width: 600px; 42 + width: 100%; 43 + max-height: 80vh; 44 + overflow-y: auto; 45 + position: relative; 46 + } 47 + 48 + .mst-node-close { 49 + position: absolute; 50 + top: 1rem; 51 + right: 1rem; 52 + width: 32px; 53 + height: 32px; 54 + border: 1px solid var(--border); 55 + background: var(--bg); 56 + color: var(--text-light); 57 + cursor: pointer; 58 + display: flex; 59 + align-items: center; 60 + justify-content: center; 61 + font-size: 1.2rem; 62 + line-height: 1; 63 + transition: all 0.2s ease; 64 + border-radius: 2px; 65 + } 66 + 67 + .mst-node-close:hover { 68 + background: var(--surface-hover); 69 + border-color: var(--text-light); 70 + color: var(--text); 71 + } 72 + 73 + .mst-node-modal-content h3 { 74 + margin-bottom: 1rem; 75 + font-size: 0.9rem; 76 + color: var(--text); 77 + } 78 + 79 + .mst-node-info { 80 + background: var(--bg); 81 + border: 1px solid var(--border); 82 + padding: 0.75rem; 83 + border-radius: 4px; 84 + margin-bottom: 1rem; 85 + } 86 + 87 + .mst-node-field { 88 + display: flex; 89 + gap: 0.5rem; 90 + margin-bottom: 0.5rem; 91 + font-size: 0.65rem; 92 + } 93 + 94 + .mst-node-field:last-child { 95 + margin-bottom: 0; 96 + } 97 + 98 + .mst-node-label { 99 + color: var(--text-light); 100 + font-weight: 500; 101 + min-width: 40px; 102 + } 103 + 104 + .mst-node-value { 105 + color: var(--text); 106 + word-break: break-all; 107 + font-family: monospace; 108 + } 109 + 110 + .mst-node-explanation { 111 + background: var(--bg); 112 + border: 1px solid var(--border); 113 + padding: 0.75rem; 114 + border-radius: 4px; 115 + margin-bottom: 1rem; 116 + } 117 + 118 + .mst-node-explanation p { 119 + font-size: 0.65rem; 120 + color: var(--text-lighter); 121 + line-height: 1.5; 122 + margin: 0; 123 + } 124 + 125 + .mst-node-data { 126 + background: var(--bg); 127 + border: 1px solid var(--border); 128 + border-radius: 4px; 129 + overflow: hidden; 130 + } 131 + 132 + .mst-node-data-header { 133 + font-size: 0.65rem; 134 + color: var(--text-light); 135 + padding: 0.5rem 0.75rem; 136 + border-bottom: 1px solid var(--border); 137 + font-weight: 500; 138 + } 139 + 140 + .mst-node-data pre { 141 + margin: 0; 142 + padding: 0.75rem; 143 + font-size: 0.625rem; 144 + color: var(--text); 145 + white-space: pre-wrap; 146 + word-break: break-word; 147 + line-height: 1.5; 148 + max-height: 300px; 149 + overflow-y: auto; 150 + }
+353
src/view/mst.js
···
··· 1 + // ============================================================================ 2 + // MST (Merkle Search Tree) VISUALIZATION 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + import { listRecords, escapeHtml } from './atproto.js'; 7 + 8 + const MST_MAX_DEPTH = 5; 9 + const MST_FETCH_LIMIT = 100; 10 + 11 + function calculateKeyDepth(key) { 12 + // Hash the key using a simple hash function 13 + let hash = 0; 14 + for (let i = 0; i < key.length; i++) { 15 + hash = (hash << 5) - hash + key.charCodeAt(i); 16 + hash = hash & hash; // Convert to 32bit integer 17 + } 18 + 19 + // Count leading zero pairs in binary representation 20 + const absHash = Math.abs(hash) >>> 0; 21 + const binary = absHash.toString(2).padStart(32, '0'); 22 + 23 + let depth = 0; 24 + for (let i = 0; i < binary.length - 1; i += 2) { 25 + if (binary[i] === '0' && binary[i + 1] === '0') { 26 + depth++; 27 + } else { 28 + break; 29 + } 30 + } 31 + 32 + return Math.min(depth, MST_MAX_DEPTH); 33 + } 34 + 35 + function buildMST(records) { 36 + const recordCount = records.length; 37 + 38 + // Extract and sort by key (rkey from URI) 39 + let nodes = records.map(r => { 40 + const key = r.uri.split('/').pop() || ''; 41 + return { 42 + key: key, 43 + cid: r.cid, 44 + uri: r.uri, 45 + value: r.value, 46 + depth: calculateKeyDepth(key), 47 + children: [] 48 + }; 49 + }); 50 + 51 + nodes.sort((a, b) => a.key.localeCompare(b.key)); 52 + 53 + // Build tree structure 54 + const root = buildTree(nodes); 55 + 56 + return { root, recordCount }; 57 + } 58 + 59 + function buildTree(nodes) { 60 + if (nodes.length === 0) { 61 + return { 62 + key: 'root', 63 + cid: null, 64 + uri: null, 65 + value: null, 66 + depth: -1, 67 + children: [] 68 + }; 69 + } 70 + 71 + // Group by depth 72 + const byDepth = {}; 73 + for (const node of nodes) { 74 + if (!byDepth[node.depth]) byDepth[node.depth] = []; 75 + byDepth[node.depth].push(node); 76 + } 77 + 78 + const depths = Object.keys(byDepth).map(Number).sort((a, b) => a - b); 79 + 80 + // Build tree bottom-up 81 + let currentLevel = byDepth[depths[depths.length - 1]] || []; 82 + 83 + // Work backwards through depths 84 + for (let i = depths.length - 2; i >= 0; i--) { 85 + const depth = depths[i]; 86 + const parentNodes = byDepth[depth] || []; 87 + 88 + if (parentNodes.length === 0) continue; 89 + 90 + // Distribute children to parents 91 + const childrenPerParent = Math.ceil(currentLevel.length / parentNodes.length); 92 + 93 + for (let j = 0; j < parentNodes.length; j++) { 94 + const start = j * childrenPerParent; 95 + const end = Math.min((j + 1) * childrenPerParent, currentLevel.length); 96 + if (start < currentLevel.length) { 97 + parentNodes[j].children = currentLevel.slice(start, end); 98 + } 99 + } 100 + 101 + currentLevel = parentNodes; 102 + } 103 + 104 + // Create root and attach top-level nodes 105 + return { 106 + key: 'root', 107 + cid: null, 108 + uri: null, 109 + value: null, 110 + depth: -1, 111 + children: currentLevel 112 + }; 113 + } 114 + 115 + export async function loadMSTStructure(lexicon, containerView) { 116 + try { 117 + // Fetch records for MST building 118 + const data = await listRecords(state.globalPds, state.did, lexicon, MST_FETCH_LIMIT); 119 + 120 + if (!data?.records?.length) { 121 + containerView.innerHTML = '<div class="mst-info"><p>no records to visualize</p></div>'; 122 + return; 123 + } 124 + 125 + const { root, recordCount } = buildMST(data.records); 126 + 127 + containerView.innerHTML = ` 128 + <div class="mst-info"> 129 + <p>this shows the <a href="https://atproto.com/specs/repository#mst-structure" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Merkle Search Tree (MST)</a> structure used to store your ${recordCount} record${recordCount !== 1 ? 's' : ''} in your repository. records are organized by their <a href="https://atproto.com/specs/record-key#record-key-type-tid" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">TIDs</a> (timestamp identifiers), which determines how they're arranged in the tree.</p> 130 + </div> 131 + <canvas class="mst-canvas" id="mstCanvas-${Date.now()}"></canvas> 132 + `; 133 + 134 + setTimeout(() => { 135 + const canvas = containerView.querySelector('.mst-canvas'); 136 + if (canvas) { 137 + renderMSTTree(canvas, root); 138 + } 139 + }, 50); 140 + 141 + } catch (e) { 142 + console.error('Error loading MST structure:', e); 143 + containerView.innerHTML = '<div class="mst-info"><p>error loading structure</p></div>'; 144 + } 145 + } 146 + 147 + function renderMSTTree(canvas, tree) { 148 + const ctx = canvas.getContext('2d'); 149 + const width = canvas.width = canvas.offsetWidth; 150 + const height = canvas.height = canvas.offsetHeight; 151 + 152 + const layout = layoutTree(tree, width, height); 153 + 154 + const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border').trim(); 155 + const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text').trim(); 156 + const surfaceColor = getComputedStyle(document.documentElement).getPropertyValue('--surface').trim(); 157 + const surfaceHoverColor = getComputedStyle(document.documentElement).getPropertyValue('--surface-hover').trim(); 158 + const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim(); 159 + 160 + let hoveredNode = null; 161 + 162 + function draw() { 163 + ctx.clearRect(0, 0, width, height); 164 + 165 + // Draw connections first 166 + layout.forEach(node => { 167 + if (node.children) { 168 + node.children.forEach(child => { 169 + ctx.beginPath(); 170 + ctx.moveTo(node.x, node.y); 171 + ctx.lineTo(child.x, child.y); 172 + ctx.strokeStyle = borderColor; 173 + ctx.lineWidth = 1; 174 + ctx.stroke(); 175 + }); 176 + } 177 + }); 178 + 179 + // Draw nodes 180 + layout.forEach(node => { 181 + const isRoot = node.depth === -1; 182 + const isLeaf = !node.children || node.children.length === 0; 183 + const isHovered = hoveredNode === node; 184 + 185 + ctx.beginPath(); 186 + ctx.arc(node.x, node.y, isRoot ? 12 : 8, 0, Math.PI * 2); 187 + 188 + ctx.fillStyle = isRoot ? textColor : isLeaf ? surfaceHoverColor : surfaceColor; 189 + ctx.fill(); 190 + 191 + ctx.strokeStyle = isHovered ? textColor : borderColor; 192 + ctx.lineWidth = isRoot ? 2 : isHovered ? 2 : 1; 193 + ctx.stroke(); 194 + }); 195 + 196 + // Draw label for hovered node 197 + if (hoveredNode && hoveredNode.key && hoveredNode.key !== 'root') { 198 + const padding = 6; 199 + const fontSize = 10; 200 + ctx.font = `${fontSize}px monospace`; 201 + const textWidth = ctx.measureText(hoveredNode.key).width; 202 + 203 + const tooltipX = hoveredNode.x; 204 + const tooltipY = hoveredNode.y - 20; 205 + const boxWidth = textWidth + padding * 2; 206 + const boxHeight = fontSize + padding * 2; 207 + 208 + ctx.fillStyle = bgColor; 209 + ctx.fillRect(tooltipX - boxWidth / 2, tooltipY - boxHeight / 2, boxWidth, boxHeight); 210 + 211 + ctx.strokeStyle = borderColor; 212 + ctx.lineWidth = 1; 213 + ctx.strokeRect(tooltipX - boxWidth / 2, tooltipY - boxHeight / 2, boxWidth, boxHeight); 214 + 215 + ctx.fillStyle = textColor; 216 + ctx.textAlign = 'center'; 217 + ctx.textBaseline = 'middle'; 218 + ctx.fillText(hoveredNode.key, tooltipX, tooltipY); 219 + } 220 + } 221 + 222 + canvas.addEventListener('mousemove', (e) => { 223 + const rect = canvas.getBoundingClientRect(); 224 + const mouseX = e.clientX - rect.left; 225 + const mouseY = e.clientY - rect.top; 226 + 227 + let foundNode = null; 228 + for (const node of layout) { 229 + const isRoot = node.depth === -1; 230 + const radius = isRoot ? 12 : 8; 231 + const dist = Math.sqrt((mouseX - node.x) ** 2 + (mouseY - node.y) ** 2); 232 + if (dist <= radius) { 233 + foundNode = node; 234 + break; 235 + } 236 + } 237 + 238 + if (foundNode !== hoveredNode) { 239 + hoveredNode = foundNode; 240 + canvas.style.cursor = hoveredNode ? 'pointer' : 'default'; 241 + draw(); 242 + } 243 + }); 244 + 245 + canvas.addEventListener('mouseleave', () => { 246 + if (hoveredNode) { 247 + hoveredNode = null; 248 + canvas.style.cursor = 'default'; 249 + draw(); 250 + } 251 + }); 252 + 253 + canvas.addEventListener('click', (e) => { 254 + if (hoveredNode && hoveredNode.key && hoveredNode.key !== 'root') { 255 + showNodeModal(hoveredNode); 256 + } 257 + }); 258 + 259 + draw(); 260 + } 261 + 262 + function layoutTree(tree, width, height) { 263 + const nodes = []; 264 + const padding = 40; 265 + const availableHeight = height - padding * 2; 266 + 267 + // Calculate max depth 268 + const depthCounts = {}; 269 + function countDepths(node, depth) { 270 + if (!depthCounts[depth]) depthCounts[depth] = 0; 271 + depthCounts[depth]++; 272 + if (node.children) { 273 + node.children.forEach(child => countDepths(child, depth + 1)); 274 + } 275 + } 276 + countDepths(tree, 0); 277 + 278 + const maxDepth = Math.max(...Object.keys(depthCounts).map(Number)); 279 + const verticalSpacing = availableHeight / (maxDepth + 1); 280 + 281 + function traverse(node, depth, minX, maxX) { 282 + const x = (minX + maxX) / 2; 283 + const y = padding + verticalSpacing * depth; 284 + 285 + const layoutNode = { ...node, x, y }; 286 + nodes.push(layoutNode); 287 + 288 + if (node.children && node.children.length > 0) { 289 + layoutNode.children = []; 290 + const childWidth = (maxX - minX) / node.children.length; 291 + 292 + node.children.forEach((child, idx) => { 293 + const childMinX = minX + childWidth * idx; 294 + const childMaxX = minX + childWidth * (idx + 1); 295 + const childLayout = traverse(child, depth + 1, childMinX, childMaxX); 296 + layoutNode.children.push(childLayout); 297 + }); 298 + } 299 + 300 + return layoutNode; 301 + } 302 + 303 + traverse(tree, 0, padding, width - padding); 304 + return nodes; 305 + } 306 + 307 + function showNodeModal(node) { 308 + const modal = document.createElement('div'); 309 + modal.className = 'mst-node-modal'; 310 + modal.innerHTML = ` 311 + <div class="mst-node-modal-content"> 312 + <button class="mst-node-close">x</button> 313 + <h3>record in MST</h3> 314 + <div class="mst-node-info"> 315 + <div class="mst-node-field"> 316 + <span class="mst-node-label">TID:</span> 317 + <span class="mst-node-value">${node.key}</span> 318 + </div> 319 + <div class="mst-node-field"> 320 + <span class="mst-node-label">CID:</span> 321 + <span class="mst-node-value">${node.cid || 'N/A'}</span> 322 + </div> 323 + ${node.uri ? ` 324 + <div class="mst-node-field"> 325 + <span class="mst-node-label">URI:</span> 326 + <span class="mst-node-value">${node.uri}</span> 327 + </div> 328 + ` : ''} 329 + </div> 330 + <div class="mst-node-explanation"> 331 + <p>this is a leaf node in your Merkle Search Tree. the TID (timestamp identifier) determines its position in the tree. records are sorted by TID, making range queries efficient.</p> 332 + </div> 333 + ${node.value ? ` 334 + <div class="mst-node-data"> 335 + <div class="mst-node-data-header">record data</div> 336 + <pre>${escapeHtml(JSON.stringify(node.value, null, 2))}</pre> 337 + </div> 338 + ` : ''} 339 + </div> 340 + `; 341 + 342 + document.body.appendChild(modal); 343 + 344 + modal.querySelector('.mst-node-close').addEventListener('click', () => { 345 + modal.remove(); 346 + }); 347 + 348 + modal.addEventListener('click', (e) => { 349 + if (e.target === modal) { 350 + modal.remove(); 351 + } 352 + }); 353 + }
+156
src/view/particles.js
···
··· 1 + // ============================================================================ 2 + // FIREHOSE PARTICLE ANIMATION 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + 7 + export class FirehoseParticle { 8 + constructor(startX, startY, endX, endY, color, metadata) { 9 + this.x = startX; 10 + this.y = startY; 11 + this.startX = startX; 12 + this.startY = startY; 13 + this.endX = endX; 14 + this.endY = endY; 15 + this.color = color; 16 + this.metadata = metadata; 17 + this.progress = 0; 18 + this.speed = 0.008; 19 + this.size = 6; 20 + this.glowSize = 14; 21 + } 22 + 23 + update() { 24 + if (this.progress < 1) { 25 + this.progress += this.speed; 26 + const eased = 1 - Math.pow(1 - this.progress, 3); 27 + this.x = this.startX + (this.endX - this.startX) * eased; 28 + this.y = this.startY + (this.endY - this.startY) * eased; 29 + } 30 + return this.progress < 1; 31 + } 32 + 33 + draw(ctx) { 34 + const fadeIn = Math.min(this.progress * 4, 1); 35 + const fadeOut = this.progress > 0.8 ? 1 - ((this.progress - 0.8) / 0.2) : 1; 36 + const opacity = Math.min(fadeIn, fadeOut); 37 + 38 + ctx.beginPath(); 39 + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); 40 + const gradient = ctx.createRadialGradient( 41 + this.x, this.y, 0, 42 + this.x, this.y, this.glowSize 43 + ); 44 + gradient.addColorStop(0, this.color + Math.floor(opacity * 60).toString(16).padStart(2, '0')); 45 + gradient.addColorStop(0.5, this.color + Math.floor(opacity * 30).toString(16).padStart(2, '0')); 46 + gradient.addColorStop(1, this.color + '00'); 47 + ctx.fillStyle = gradient; 48 + ctx.fill(); 49 + 50 + ctx.beginPath(); 51 + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); 52 + ctx.fillStyle = this.color + Math.floor(opacity * 180).toString(16).padStart(2, '0'); 53 + ctx.fill(); 54 + } 55 + } 56 + 57 + export function initFirehoseCanvas() { 58 + if (state.firehoseCanvas) return; 59 + 60 + state.firehoseCanvas = document.createElement('canvas'); 61 + state.firehoseCanvas.id = 'firehoseCanvas'; 62 + state.firehoseCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:50;'; 63 + state.firehoseCanvas.width = window.innerWidth; 64 + state.firehoseCanvas.height = window.innerHeight; 65 + document.body.appendChild(state.firehoseCanvas); 66 + state.firehoseCtx = state.firehoseCanvas.getContext('2d'); 67 + 68 + window.addEventListener('resize', () => { 69 + if (state.firehoseCanvas) { 70 + state.firehoseCanvas.width = window.innerWidth; 71 + state.firehoseCanvas.height = window.innerHeight; 72 + } 73 + }); 74 + } 75 + 76 + function getParticleColor() { 77 + const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text').trim(); 78 + if (textColor.startsWith('rgb')) { 79 + const match = textColor.match(/(\d+),\s*(\d+),\s*(\d+)/); 80 + if (match) { 81 + const r = parseInt(match[1]); 82 + const g = parseInt(match[2]); 83 + const b = parseInt(match[3]); 84 + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 85 + } 86 + } 87 + return '#8ba4b8'; 88 + } 89 + 90 + export function createFirehoseParticle(event) { 91 + const identity = document.querySelector('.identity'); 92 + if (!identity) return; 93 + 94 + const identityRect = identity.getBoundingClientRect(); 95 + const endX = identityRect.left + identityRect.width / 2; 96 + const endY = identityRect.top + identityRect.height / 2; 97 + 98 + // Find the app circle for this event 99 + const appCircle = document.querySelector(`[data-namespace="${event.namespace}"]`); 100 + 101 + let startX, startY; 102 + if (appCircle) { 103 + const appRect = appCircle.getBoundingClientRect(); 104 + startX = appRect.left + appRect.width / 2; 105 + startY = appRect.top + appRect.height / 2; 106 + } else { 107 + startX = endX; 108 + startY = endY; 109 + } 110 + 111 + const particle = new FirehoseParticle( 112 + startX, startY, 113 + endX, endY, 114 + getParticleColor(), 115 + { action: event.action, collection: event.collection, namespace: event.namespace } 116 + ); 117 + state.firehoseParticles.push(particle); 118 + } 119 + 120 + function pulseIdentity() { 121 + const identity = document.querySelector('.identity'); 122 + if (!identity) return; 123 + identity.classList.add('pulse'); 124 + setTimeout(() => identity.classList.remove('pulse'), 300); 125 + } 126 + 127 + export function animateFirehoseParticles() { 128 + if (!state.firehoseCtx) return; 129 + 130 + state.firehoseCtx.clearRect(0, 0, state.firehoseCanvas.width, state.firehoseCanvas.height); 131 + 132 + state.firehoseParticles = state.firehoseParticles.filter(particle => { 133 + const alive = particle.update(); 134 + if (alive) { 135 + particle.draw(state.firehoseCtx); 136 + } else { 137 + pulseIdentity(); 138 + } 139 + return alive; 140 + }); 141 + 142 + state.firehoseAnimationId = requestAnimationFrame(animateFirehoseParticles); 143 + } 144 + 145 + export function cleanupFirehoseCanvas() { 146 + if (state.firehoseAnimationId) { 147 + cancelAnimationFrame(state.firehoseAnimationId); 148 + state.firehoseAnimationId = null; 149 + } 150 + state.firehoseParticles = []; 151 + if (state.firehoseCanvas) { 152 + state.firehoseCanvas.remove(); 153 + state.firehoseCanvas = null; 154 + state.firehoseCtx = null; 155 + } 156 + }
+27
src/view/state.js
···
··· 1 + // Shared state for the view page 2 + 3 + export const state = { 4 + did: null, 5 + globalPds: null, 6 + globalHandle: null, 7 + globalApps: null, 8 + hiddenApps: new Set(), 9 + invalidApps: new Set(), 10 + pageOwnerHasSigned: false, 11 + viewedAvatar: null, 12 + 13 + // Firehose animation state 14 + firehoseParticles: [], 15 + firehoseCanvas: null, 16 + firehoseCtx: null, 17 + firehoseAnimationId: null, 18 + 19 + // WebSocket state 20 + jetstreamWs: null, 21 + isWatchingLive: false 22 + }; 23 + 24 + // URL params 25 + export const urlParams = new URLSearchParams(window.location.search); 26 + export const paramDid = urlParams.get('did'); 27 + export const paramHandle = urlParams.get('handle');
+9
src/view/styles.css
···
··· 1 + /* View page styles */ 2 + @import './base.css'; 3 + @import './layout.css'; 4 + @import './detail.css'; 5 + @import './controls.css'; 6 + @import './filter.css'; 7 + @import './firehose.css'; 8 + @import './guestbook.css'; 9 + @import './mst.css';
+410
src/view/visualization.js
···
··· 1 + // ============================================================================ 2 + // VISUALIZATION 3 + // ============================================================================ 4 + 5 + import { state } from './state.js'; 6 + import { 7 + applyDomainRedirect, 8 + escapeHtml, 9 + fetchAppAvatars, 10 + validateAppUrls, 11 + listRecords 12 + } from './atproto.js'; 13 + import { initFilterPanel, repositionAppCircles } from './filters.js'; 14 + import { loadMSTStructure } from './mst.js'; 15 + 16 + export function renderVisualization(apps, profile) { 17 + const field = document.getElementById('field'); 18 + field.innerHTML = ''; 19 + field.classList.remove('loading'); 20 + 21 + const appNames = Object.keys(apps).sort(); 22 + const appCount = appNames.length; 23 + const allCollections = Object.values(apps).flat(); 24 + 25 + // Hide labels on mobile when there are too many apps 26 + const isMobileView = window.innerWidth < 768; 27 + if (isMobileView && appCount > 20) { 28 + field.classList.add('many-apps'); 29 + } 30 + 31 + // Calculate dimensions 32 + const vmin = Math.min(window.innerWidth, window.innerHeight); 33 + const isMobile = window.innerWidth < 768; 34 + 35 + let circleSize, radius; 36 + if (isMobile) { 37 + if (appCount <= 5) { 38 + circleSize = Math.min(60, vmin * 0.08); 39 + radius = vmin * 0.38; 40 + } else if (appCount <= 10) { 41 + circleSize = Math.min(50, vmin * 0.07); 42 + radius = vmin * 0.4; 43 + } else if (appCount <= 20) { 44 + circleSize = Math.min(40, vmin * 0.055); 45 + radius = vmin * 0.42; 46 + } else { 47 + circleSize = Math.min(32, vmin * 0.045); 48 + radius = vmin * 0.44; 49 + } 50 + circleSize = Math.max(circleSize, 28); 51 + radius = Math.max(radius, 120); 52 + } else { 53 + if (appCount <= 5) { 54 + circleSize = Math.min(70, vmin * 0.1); 55 + } else if (appCount <= 10) { 56 + circleSize = Math.min(60, vmin * 0.09); 57 + } else if (appCount <= 20) { 58 + circleSize = Math.min(50, vmin * 0.07); 59 + } else { 60 + circleSize = Math.min(40, vmin * 0.06); 61 + } 62 + circleSize = Math.max(circleSize, 35); 63 + radius = Math.max(vmin * 0.35, 150); 64 + } 65 + 66 + const centerX = window.innerWidth / 2; 67 + const centerY = window.innerHeight / 2; 68 + 69 + state.globalApps._circleSize = circleSize; 70 + 71 + // Create app circles 72 + const appDivs = appNames.map((namespace, i) => { 73 + const angle = (i / appNames.length) * 2 * Math.PI - Math.PI / 2; 74 + const circleOffset = circleSize / 2; 75 + const x = centerX + radius * Math.cos(angle) - circleOffset; 76 + const y = centerY + radius * Math.sin(angle) - circleOffset; 77 + 78 + const div = document.createElement('div'); 79 + div.className = 'app-view'; 80 + div.style.left = `${x}px`; 81 + div.style.top = `${y}px`; 82 + 83 + const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase(); 84 + const rawDisplayName = namespace.split('.').reverse().join('.'); 85 + const displayName = applyDomainRedirect(rawDisplayName); 86 + const url = `https://${displayName}`; 87 + 88 + div.innerHTML = ` 89 + <div class="app-circle" data-namespace="${namespace}" style="width: ${circleSize}px; height: ${circleSize}px; font-size: ${circleSize * 0.4}px;">${firstLetter}</div> 90 + <a href="${url}" target="_blank" rel="noopener noreferrer" class="app-name" data-url="${url}">${displayName} &#8599;</a> 91 + `; 92 + 93 + div.addEventListener('click', () => showAppDetail(namespace, apps[namespace], displayName, url)); 94 + 95 + return { div, namespace }; 96 + }); 97 + 98 + // Add all divs to field 99 + appDivs.forEach(({ div }) => field.appendChild(div)); 100 + 101 + // Fetch avatars asynchronously 102 + fetchAppAvatars(appNames).then(avatarMap => { 103 + appDivs.forEach(({ div, namespace }) => { 104 + const avatarUrl = avatarMap[namespace]; 105 + if (avatarUrl) { 106 + const circle = div.querySelector('.app-circle'); 107 + circle.innerHTML = `<img src="${avatarUrl}" class="app-logo" alt="${namespace}" />`; 108 + } 109 + }); 110 + }); 111 + 112 + // Validate app URLs (client-side check via image load) 113 + validateAppUrls(appDivs); 114 + 115 + // Set up identity click handler 116 + setupIdentityClickHandler(allCollections, appCount, profile); 117 + 118 + // Set up filter panel 119 + initFilterPanel(); 120 + 121 + // Handle window resize 122 + let resizeTimeout; 123 + window.addEventListener('resize', () => { 124 + clearTimeout(resizeTimeout); 125 + resizeTimeout = setTimeout(repositionAppCircles, 50); 126 + }); 127 + } 128 + 129 + function setupIdentityClickHandler(allCollections, appCount, profile) { 130 + const pdsHost = state.globalPds.replace('https://', '').replace('http://', ''); 131 + 132 + document.querySelector('.identity').addEventListener('click', () => { 133 + const detail = document.getElementById('detail'); 134 + 135 + detail.innerHTML = ` 136 + <button class="detail-close" id="detailClose">x</button> 137 + <h3>your personal data server</h3> 138 + <div class="subtitle">where your social data lives</div> 139 + 140 + <div class="stats-box"> 141 + <div class="stat"> 142 + <div class="stat-value">${allCollections.length}</div> 143 + <div class="stat-label">record types</div> 144 + </div> 145 + <div class="stat"> 146 + <div class="stat-value">${appCount}</div> 147 + <div class="stat-label">apps</div> 148 + </div> 149 + </div> 150 + 151 + <div class="ownership-box yours"> 152 + <div class="ownership-header">your pds location</div> 153 + <div class="ownership-text">your <a href="https://atproto.com/guides/data-repos" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data Server</a> is hosted at <a href="${state.globalPds}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;"><strong>${pdsHost}</strong></a>. all your posts, likes, and follows are stored here. apps like bluesky just connect to it.</div> 154 + </div> 155 + 156 + <div class="ownership-box"> 157 + <div class="ownership-header">explore your data</div> 158 + <div class="ownership-text">want to see everything stored on your PDS? check out <a href="https://pdsls.dev/${pdsHost}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">pdsls.dev/${pdsHost}</a> - a tool for browsing all the records in your repository.</div> 159 + </div> 160 + 161 + <a href="https://bsky.app/profile/${state.globalHandle}" target="_blank" rel="noopener noreferrer" class="tree-item" style="text-decoration: none; display: block; margin-top: 1rem;"> 162 + <div class="tree-item-header"> 163 + <div style="display: flex; align-items: center; gap: 0.5rem;"> 164 + <svg width="16" height="16" viewBox="0 0 600 530" fill="none" xmlns="http://www.w3.org/2000/svg"> 165 + <path d="M135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="var(--text)" /> 166 + </svg> 167 + <span style="color: var(--text-light);">view profile on bluesky</span> 168 + </div> 169 + <span style="font-size: 0.6rem; color: var(--text);">&#8599;</span> 170 + </div> 171 + </a> 172 + 173 + <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);"> 174 + <div style="font-size: 0.65rem; color: var(--text-light); margin-bottom: 0.5rem;">technical details</div> 175 + <div class="tree-item"> 176 + <div class="tree-item-header"> 177 + <span style="color: var(--text-light);">did</span> 178 + <span style="font-size: 0.55rem; color: var(--text);">${state.did}</span> 179 + </div> 180 + </div> 181 + <div class="tree-item"> 182 + <div class="tree-item-header"> 183 + <span style="color: var(--text-light);">handle</span> 184 + <span style="font-size: 0.6rem; color: var(--text);">@${state.globalHandle}</span> 185 + </div> 186 + </div> 187 + </div> 188 + `; 189 + detail.classList.add('visible'); 190 + 191 + document.getElementById('detailClose').addEventListener('click', (e) => { 192 + e.stopPropagation(); 193 + detail.classList.remove('visible'); 194 + }); 195 + }); 196 + } 197 + 198 + async function showAppDetail(namespace, collections, displayName, appUrl) { 199 + const detail = document.getElementById('detail'); 200 + 201 + let html = ` 202 + <button class="detail-close" id="detailClose">x</button> 203 + <h3><a href="${appUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);">${displayName} &#8599;</a></h3> 204 + <div class="subtitle">records stored in your <a href="https://atproto.com/guides/self-hosting" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">PDS</a>:</div> 205 + `; 206 + 207 + if (collections && collections.length > 0) { 208 + const grouped = {}; 209 + collections.forEach(lexicon => { 210 + const parts = lexicon.split('.'); 211 + const subNamespace = parts.slice(2).join('.'); 212 + const firstPart = parts[2] || lexicon; 213 + 214 + if (!grouped[firstPart]) grouped[firstPart] = []; 215 + grouped[firstPart].push({ lexicon, subNamespace }); 216 + }); 217 + 218 + Object.keys(grouped).sort().forEach(group => { 219 + const items = grouped[group]; 220 + 221 + if (items.length === 1 && items[0].subNamespace === group) { 222 + html += ` 223 + <div class="tree-item" data-lexicon="${items[0].lexicon}"> 224 + <div class="tree-item-header"> 225 + <span>${group}</span> 226 + <span class="tree-item-count">loading...</span> 227 + </div> 228 + </div> 229 + `; 230 + } else { 231 + html += `<div style="margin-bottom: 0.75rem;">`; 232 + html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${group}</div>`; 233 + 234 + items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => { 235 + const itemDisplayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace; 236 + html += ` 237 + <div class="tree-item" data-lexicon="${item.lexicon}" style="margin-left: 0.75rem;"> 238 + <div class="tree-item-header"> 239 + <span>${itemDisplayName}</span> 240 + <span class="tree-item-count">loading...</span> 241 + </div> 242 + </div> 243 + `; 244 + }); 245 + html += `</div>`; 246 + } 247 + }); 248 + } else { 249 + html += `<div class="tree-item">no collections found</div>`; 250 + } 251 + 252 + detail.innerHTML = html; 253 + detail.classList.add('visible'); 254 + 255 + document.getElementById('detailClose').addEventListener('click', (e) => { 256 + e.stopPropagation(); 257 + detail.classList.remove('visible'); 258 + }); 259 + 260 + // Fetch record counts 261 + if (collections) { 262 + for (const lexicon of collections) { 263 + const data = await listRecords(state.globalPds, state.did, lexicon, 1); 264 + const item = detail.querySelector(`[data-lexicon="${lexicon}"]`); 265 + if (item) { 266 + const countSpan = item.querySelector('.tree-item-count'); 267 + countSpan.textContent = data?.records?.length > 0 ? 'has records' : 'empty'; 268 + } 269 + } 270 + } 271 + 272 + // Add click handlers for expanding collections 273 + detail.querySelectorAll('.tree-item[data-lexicon]').forEach(item => { 274 + item.addEventListener('click', (e) => { 275 + e.stopPropagation(); 276 + expandCollection(item); 277 + }); 278 + }); 279 + } 280 + 281 + async function expandCollection(item) { 282 + const lexicon = item.dataset.lexicon; 283 + const existingContent = item.querySelector('.collection-content'); 284 + 285 + if (existingContent) { 286 + existingContent.remove(); 287 + return; 288 + } 289 + 290 + const contentDiv = document.createElement('div'); 291 + contentDiv.className = 'collection-content'; 292 + contentDiv.innerHTML = ` 293 + <div class="collection-view-content"> 294 + <div class="collection-view records-view active"> 295 + <div class="loading">loading records...</div> 296 + </div> 297 + <div class="collection-view structure-view"> 298 + <div class="loading">loading structure...</div> 299 + </div> 300 + </div> 301 + `; 302 + item.appendChild(contentDiv); 303 + 304 + const recordsView = contentDiv.querySelector('.records-view'); 305 + const structureView = contentDiv.querySelector('.structure-view'); 306 + const data = await listRecords(state.globalPds, state.did, lexicon, 10); 307 + 308 + // Add tabs if there are enough records for MST view 309 + const hasEnoughRecords = data?.records?.length >= 5; 310 + if (hasEnoughRecords) { 311 + const tabsHtml = ` 312 + <div class="collection-tabs"> 313 + <button class="collection-tab active" data-tab="records">records</button> 314 + <button class="collection-tab" data-tab="structure">mst</button> 315 + </div> 316 + `; 317 + contentDiv.insertAdjacentHTML('afterbegin', tabsHtml); 318 + 319 + // Tab switching logic 320 + contentDiv.querySelectorAll('.collection-tab').forEach(tab => { 321 + tab.addEventListener('click', (e) => { 322 + e.stopPropagation(); 323 + const tabName = tab.dataset.tab; 324 + 325 + contentDiv.querySelectorAll('.collection-tab').forEach(t => t.classList.remove('active')); 326 + tab.classList.add('active'); 327 + 328 + contentDiv.querySelectorAll('.collection-view').forEach(v => v.classList.remove('active')); 329 + if (tabName === 'records') { 330 + recordsView.classList.add('active'); 331 + } else if (tabName === 'structure') { 332 + structureView.classList.add('active'); 333 + if (structureView.querySelector('.loading')) { 334 + loadMSTStructure(lexicon, structureView); 335 + } 336 + } 337 + }); 338 + }); 339 + } 340 + 341 + if (data?.records?.length > 0) { 342 + let recordsHtml = data.records.map((record, idx) => { 343 + const json = JSON.stringify(record.value, null, 2); 344 + return ` 345 + <div class="record"> 346 + <div class="record-header"> 347 + <span class="record-label">record</span> 348 + <button class="copy-btn" data-content="${encodeURIComponent(json)}">copy</button> 349 + </div> 350 + <div class="record-content"> 351 + <pre>${escapeHtml(json)}</pre> 352 + </div> 353 + </div> 354 + `; 355 + }).join(''); 356 + 357 + if (data.cursor) { 358 + recordsHtml += `<button class="load-more" data-cursor="${data.cursor}" data-lexicon="${lexicon}">load more</button>`; 359 + } 360 + 361 + recordsView.innerHTML = recordsHtml; 362 + 363 + // Event delegation for copy and load more 364 + recordsView.addEventListener('click', async (e) => { 365 + if (e.target.classList.contains('copy-btn')) { 366 + e.stopPropagation(); 367 + const content = decodeURIComponent(e.target.dataset.content); 368 + await navigator.clipboard.writeText(content); 369 + e.target.textContent = 'copied!'; 370 + setTimeout(() => { e.target.textContent = 'copy'; }, 1500); 371 + } 372 + 373 + if (e.target.classList.contains('load-more')) { 374 + e.stopPropagation(); 375 + const cursor = e.target.dataset.cursor; 376 + const lex = e.target.dataset.lexicon; 377 + e.target.textContent = 'loading...'; 378 + 379 + const moreData = await listRecords(state.globalPds, state.did, lex, 10, cursor); 380 + if (moreData?.records) { 381 + let moreHtml = moreData.records.map(record => { 382 + const json = JSON.stringify(record.value, null, 2); 383 + return ` 384 + <div class="record"> 385 + <div class="record-header"> 386 + <span class="record-label">record</span> 387 + <button class="copy-btn" data-content="${encodeURIComponent(json)}">copy</button> 388 + </div> 389 + <div class="record-content"> 390 + <pre>${escapeHtml(json)}</pre> 391 + </div> 392 + </div> 393 + `; 394 + }).join(''); 395 + 396 + e.target.remove(); 397 + recordsView.insertAdjacentHTML('beforeend', moreHtml); 398 + 399 + if (moreData.cursor) { 400 + recordsView.insertAdjacentHTML('beforeend', 401 + `<button class="load-more" data-cursor="${moreData.cursor}" data-lexicon="${lex}">load more</button>` 402 + ); 403 + } 404 + } 405 + } 406 + }); 407 + } else { 408 + recordsView.innerHTML = '<div class="record">no records found</div>'; 409 + } 410 + }
-440
static/app.js
··· 1 - // DID is set as window.DID by the template 2 - const did = window.DID; 3 - localStorage.setItem('atme_did', did); 4 - 5 - let globalPds = null; 6 - let globalHandle = null; 7 - 8 - // Try to fetch app avatar from their bsky profile 9 - async function fetchAppAvatar(namespace) { 10 - try { 11 - // Reverse namespace to get domain (e.g., io.zzstoatzz -> zzstoatzz.io) 12 - const reversed = namespace.split('.').reverse().join('.'); 13 - // Try reversed domain, then reversed.bsky.social 14 - const handles = [reversed, `${reversed}.bsky.social`]; 15 - 16 - for (const handle of handles) { 17 - try { 18 - const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`); 19 - if (!didRes.ok) continue; 20 - 21 - const { did } = await didRes.json(); 22 - const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`); 23 - if (!profileRes.ok) continue; 24 - 25 - const profile = await profileRes.json(); 26 - if (profile.avatar) { 27 - return profile.avatar; 28 - } 29 - } catch (e) { 30 - // Silently continue to next handle 31 - continue; 32 - } 33 - } 34 - } catch (e) { 35 - // Expected for namespaces without Bluesky accounts 36 - } 37 - return null; 38 - } 39 - 40 - // Logout handler 41 - document.getElementById('logoutBtn').addEventListener('click', (e) => { 42 - e.preventDefault(); 43 - localStorage.removeItem('atme_did'); 44 - window.location.href = '/logout'; 45 - }); 46 - 47 - // Info modal handlers 48 - document.getElementById('infoBtn').addEventListener('click', () => { 49 - document.getElementById('infoModal').classList.add('visible'); 50 - document.getElementById('overlay').classList.add('visible'); 51 - }); 52 - 53 - document.getElementById('closeInfo').addEventListener('click', () => { 54 - document.getElementById('infoModal').classList.remove('visible'); 55 - document.getElementById('overlay').classList.remove('visible'); 56 - }); 57 - 58 - document.getElementById('overlay').addEventListener('click', () => { 59 - document.getElementById('infoModal').classList.remove('visible'); 60 - document.getElementById('overlay').classList.remove('visible'); 61 - const detail = document.getElementById('detail'); 62 - detail.classList.remove('visible'); 63 - }); 64 - 65 - // First resolve DID to get PDS endpoint and handle 66 - fetch('https://plc.directory/' + did) 67 - .then(r => r.json()) 68 - .then(didDoc => { 69 - const pds = didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint; 70 - const handle = didDoc.alsoKnownAs?.[0]?.replace('at://', '') || did; 71 - 72 - globalPds = pds; 73 - globalHandle = handle; 74 - 75 - // Update identity display with handle 76 - document.getElementById('handle').textContent = handle; 77 - 78 - // Try to fetch and display user's avatar 79 - fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`) 80 - .then(r => r.json()) 81 - .then(profile => { 82 - if (profile.avatar) { 83 - const identity = document.querySelector('.identity'); 84 - const avatarImg = document.createElement('img'); 85 - avatarImg.src = profile.avatar; 86 - avatarImg.className = 'identity-avatar'; 87 - avatarImg.alt = handle; 88 - // Insert avatar before the @ label 89 - identity.insertBefore(avatarImg, identity.firstChild); 90 - } 91 - }) 92 - .catch(() => { 93 - // User may not have an avatar set 94 - }); 95 - 96 - // Store collections and apps for later use 97 - let allCollections = []; 98 - let apps = {}; 99 - 100 - // Get all collections from PDS 101 - return fetch(`${pds}/xrpc/com.atproto.repo.describeRepo?repo=${did}`); 102 - }) 103 - .then(r => r.json()) 104 - .then(repo => { 105 - const collections = repo.collections || []; 106 - allCollections = collections; 107 - 108 - // Group by app namespace (first two parts of lexicon) 109 - apps = {}; 110 - collections.forEach(collection => { 111 - const parts = collection.split('.'); 112 - if (parts.length >= 2) { 113 - const namespace = `${parts[0]}.${parts[1]}`; 114 - if (!apps[namespace]) apps[namespace] = []; 115 - apps[namespace].push(collection); 116 - } 117 - }); 118 - 119 - // Add identity click handler now that we have the data 120 - const pdsHost = globalPds.replace('https://', '').replace('http://', ''); 121 - document.querySelector('.identity').addEventListener('click', () => { 122 - const detail = document.getElementById('detail'); 123 - const appCount = Object.keys(apps).length; 124 - 125 - detail.innerHTML = ` 126 - <button class="detail-close" id="detailClose">ร—</button> 127 - <h3>your repository</h3> 128 - <div class="subtitle">what you've built</div> 129 - 130 - <div class="stats-box"> 131 - <div class="stat"> 132 - <div class="stat-value">${allCollections.length}</div> 133 - <div class="stat-label">record types</div> 134 - </div> 135 - <div class="stat"> 136 - <div class="stat-value">${appCount}</div> 137 - <div class="stat-label">apps</div> 138 - </div> 139 - </div> 140 - 141 - <div class="ownership-box"> 142 - <div class="ownership-header">on traditional platforms</div> 143 - <div class="ownership-text">your content is locked in. switching platforms means starting over. you build their network, they own the distribution.</div> 144 - </div> 145 - 146 - <div class="ownership-box yours"> 147 - <div class="ownership-header">on atproto</div> 148 - <div class="ownership-text">your content, your server. apps just read and write to <strong>${pdsHost}</strong>. switch apps anytime, take your data anywhere.</div> 149 - </div> 150 - 151 - <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);"> 152 - <div style="font-size: 0.65rem; color: var(--text-light); margin-bottom: 0.5rem;">technical details</div> 153 - <div class="tree-item"> 154 - <div class="tree-item-header"> 155 - <span style="color: var(--text-light);">did</span> 156 - <span style="font-size: 0.55rem; color: var(--text);">${did}</span> 157 - </div> 158 - </div> 159 - <div class="tree-item"> 160 - <div class="tree-item-header"> 161 - <span style="color: var(--text-light);">handle</span> 162 - <span style="font-size: 0.6rem; color: var(--text);">@${globalHandle}</span> 163 - </div> 164 - </div> 165 - </div> 166 - `; 167 - detail.classList.add('visible'); 168 - 169 - // Add close button handler 170 - document.getElementById('detailClose').addEventListener('click', (e) => { 171 - e.stopPropagation(); 172 - detail.classList.remove('visible'); 173 - }); 174 - }); 175 - 176 - const field = document.getElementById('field'); 177 - field.innerHTML = ''; 178 - field.classList.remove('loading'); 179 - 180 - const appNames = Object.keys(apps).sort(); 181 - // Responsive radius: use viewport-relative sizing with min/max bounds 182 - const vmin = Math.min(window.innerWidth, window.innerHeight); 183 - const radius = Math.max(vmin * 0.35, 150); // 35% of smallest dimension, min 150px 184 - const centerX = window.innerWidth / 2; 185 - const centerY = window.innerHeight / 2; 186 - 187 - appNames.forEach((namespace, i) => { 188 - const angle = (i / appNames.length) * 2 * Math.PI - Math.PI / 2; // Start from top 189 - const x = centerX + radius * Math.cos(angle) - 30; 190 - const y = centerY + radius * Math.sin(angle) - 30; 191 - 192 - const div = document.createElement('div'); 193 - div.className = 'app-view'; 194 - div.style.left = `${x}px`; 195 - div.style.top = `${y}px`; 196 - 197 - const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase(); 198 - 199 - div.innerHTML = ` 200 - <div class="app-circle" data-namespace="${namespace}">${firstLetter}</div> 201 - <div class="app-name">${namespace}</div> 202 - `; 203 - 204 - // Try to fetch and display avatar 205 - fetchAppAvatar(namespace).then(avatarUrl => { 206 - if (avatarUrl) { 207 - const circle = div.querySelector('.app-circle'); 208 - circle.innerHTML = `<img src="${avatarUrl}" class="app-logo" alt="${namespace}" />`; 209 - } 210 - }); 211 - 212 - div.addEventListener('click', () => { 213 - const detail = document.getElementById('detail'); 214 - const collections = apps[namespace]; 215 - 216 - let html = ` 217 - <button class="detail-close" id="detailClose">ร—</button> 218 - <h3>${namespace}</h3> 219 - <div class="subtitle">records stored in your pds:</div> 220 - `; 221 - 222 - if (collections && collections.length > 0) { 223 - // Group collections by sub-namespace (third segment) 224 - const grouped = {}; 225 - collections.forEach(lexicon => { 226 - const parts = lexicon.split('.'); 227 - const subNamespace = parts.slice(2).join('.'); 228 - const firstPart = parts[2] || lexicon; 229 - 230 - if (!grouped[firstPart]) grouped[firstPart] = []; 231 - grouped[firstPart].push({ lexicon, subNamespace }); 232 - }); 233 - 234 - // Sort and display grouped items 235 - Object.keys(grouped).sort().forEach(group => { 236 - const items = grouped[group]; 237 - 238 - if (items.length === 1 && items[0].subNamespace === group) { 239 - // Single item with no further nesting 240 - html += ` 241 - <div class="tree-item" data-lexicon="${items[0].lexicon}"> 242 - <div class="tree-item-header"> 243 - <span>${group}</span> 244 - <span class="tree-item-count">loading...</span> 245 - </div> 246 - </div> 247 - `; 248 - } else { 249 - // Group header 250 - html += `<div style="margin-bottom: 0.75rem;">`; 251 - html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${group}</div>`; 252 - 253 - // Items in group 254 - items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => { 255 - const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace; 256 - html += ` 257 - <div class="tree-item" data-lexicon="${item.lexicon}" style="margin-left: 0.75rem;"> 258 - <div class="tree-item-header"> 259 - <span>${displayName}</span> 260 - <span class="tree-item-count">loading...</span> 261 - </div> 262 - </div> 263 - `; 264 - }); 265 - html += `</div>`; 266 - } 267 - }); 268 - } else { 269 - html += `<div class="tree-item">no collections found</div>`; 270 - } 271 - 272 - detail.innerHTML = html; 273 - detail.classList.add('visible'); 274 - 275 - // Add close button handler 276 - document.getElementById('detailClose').addEventListener('click', (e) => { 277 - e.stopPropagation(); 278 - detail.classList.remove('visible'); 279 - }); 280 - 281 - // Fetch record counts for each collection 282 - if (collections && collections.length > 0) { 283 - collections.forEach(lexicon => { 284 - fetch(`${globalPds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${lexicon}&limit=1`) 285 - .then(r => r.json()) 286 - .then(data => { 287 - const item = detail.querySelector(`[data-lexicon="${lexicon}"]`); 288 - if (item) { 289 - const countSpan = item.querySelector('.tree-item-count'); 290 - // The cursor field indicates there are more records 291 - countSpan.textContent = data.records?.length > 0 ? 'has records' : 'empty'; 292 - } 293 - }) 294 - .catch(e => { 295 - console.error('Error fetching count for', lexicon, e); 296 - const item = detail.querySelector(`[data-lexicon="${lexicon}"]`); 297 - if (item) { 298 - const countSpan = item.querySelector('.tree-item-count'); 299 - countSpan.textContent = 'error'; 300 - } 301 - }); 302 - }); 303 - } 304 - 305 - // Add click handlers to tree items to fetch actual records 306 - detail.querySelectorAll('.tree-item[data-lexicon]').forEach(item => { 307 - item.addEventListener('click', (e) => { 308 - e.stopPropagation(); 309 - const lexicon = item.dataset.lexicon; 310 - const existingRecords = item.querySelector('.record-list'); 311 - 312 - if (existingRecords) { 313 - existingRecords.remove(); 314 - return; 315 - } 316 - 317 - const recordListDiv = document.createElement('div'); 318 - recordListDiv.className = 'record-list'; 319 - recordListDiv.innerHTML = '<div class="loading">loading records...</div>'; 320 - item.appendChild(recordListDiv); 321 - 322 - fetch(`${globalPds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${lexicon}&limit=5`) 323 - .then(r => r.json()) 324 - .then(data => { 325 - if (data.records && data.records.length > 0) { 326 - let recordsHtml = ''; 327 - data.records.forEach((record, idx) => { 328 - const json = JSON.stringify(record.value, null, 2); 329 - const recordId = `record-${Date.now()}-${idx}`; 330 - recordsHtml += ` 331 - <div class="record"> 332 - <div class="record-header"> 333 - <span class="record-label">record</span> 334 - <button class="copy-btn" data-content="${encodeURIComponent(json)}" data-record-id="${recordId}">copy</button> 335 - </div> 336 - <div class="record-content"> 337 - <pre>${json}</pre> 338 - </div> 339 - </div> 340 - `; 341 - }); 342 - 343 - if (data.cursor && data.records.length === 5) { 344 - recordsHtml += `<button class="load-more" data-cursor="${data.cursor}" data-lexicon="${lexicon}">load more</button>`; 345 - } 346 - 347 - recordListDiv.innerHTML = recordsHtml; 348 - 349 - // Use event delegation for copy and load more buttons 350 - recordListDiv.addEventListener('click', (e) => { 351 - // Handle copy button 352 - if (e.target.classList.contains('copy-btn')) { 353 - e.stopPropagation(); 354 - const copyBtn = e.target; 355 - const content = decodeURIComponent(copyBtn.dataset.content); 356 - 357 - navigator.clipboard.writeText(content).then(() => { 358 - const originalText = copyBtn.textContent; 359 - copyBtn.textContent = 'copied!'; 360 - copyBtn.classList.add('copied'); 361 - setTimeout(() => { 362 - copyBtn.textContent = originalText; 363 - copyBtn.classList.remove('copied'); 364 - }, 1500); 365 - }).catch(err => { 366 - console.error('Failed to copy:', err); 367 - copyBtn.textContent = 'error'; 368 - setTimeout(() => { 369 - copyBtn.textContent = 'copy'; 370 - }, 1500); 371 - }); 372 - } 373 - 374 - // Handle load more button 375 - if (e.target.classList.contains('load-more')) { 376 - e.stopPropagation(); 377 - const loadMoreBtn = e.target; 378 - const cursor = loadMoreBtn.dataset.cursor; 379 - const lexicon = loadMoreBtn.dataset.lexicon; 380 - 381 - loadMoreBtn.textContent = 'loading...'; 382 - 383 - fetch(`${globalPds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${lexicon}&limit=5&cursor=${cursor}`) 384 - .then(r => r.json()) 385 - .then(moreData => { 386 - let moreHtml = ''; 387 - moreData.records.forEach((record, idx) => { 388 - const json = JSON.stringify(record.value, null, 2); 389 - const recordId = `record-more-${Date.now()}-${idx}`; 390 - moreHtml += ` 391 - <div class="record"> 392 - <div class="record-header"> 393 - <span class="record-label">record</span> 394 - <button class="copy-btn" data-content="${encodeURIComponent(json)}" data-record-id="${recordId}">copy</button> 395 - </div> 396 - <div class="record-content"> 397 - <pre>${json}</pre> 398 - </div> 399 - </div> 400 - `; 401 - }); 402 - 403 - loadMoreBtn.remove(); 404 - recordListDiv.insertAdjacentHTML('beforeend', moreHtml); 405 - 406 - if (moreData.cursor && moreData.records.length === 5) { 407 - recordListDiv.insertAdjacentHTML('beforeend', 408 - `<button class="load-more" data-cursor="${moreData.cursor}" data-lexicon="${lexicon}">load more</button>` 409 - ); 410 - } 411 - }); 412 - } 413 - }); 414 - } else { 415 - recordListDiv.innerHTML = '<div class="record">no records found</div>'; 416 - } 417 - }) 418 - .catch(e => { 419 - console.error('Error fetching records:', e); 420 - recordListDiv.innerHTML = '<div class="record">error loading records</div>'; 421 - }); 422 - }); 423 - }); 424 - }); 425 - 426 - field.appendChild(div); 427 - }); 428 - 429 - // Close detail panel when clicking canvas 430 - const canvas = document.querySelector('.canvas'); 431 - canvas.addEventListener('click', (e) => { 432 - if (e.target === canvas) { 433 - document.getElementById('detail').classList.remove('visible'); 434 - } 435 - }); 436 - }) 437 - .catch(e => { 438 - document.getElementById('field').innerHTML = 'error loading records'; 439 - console.error(e); 440 - });
···
-4
static/favicon.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> 2 - <rect width="100" height="100" fill="#0a0a0a"/> 3 - <text x="50" y="75" font-family="monospace" font-size="70" font-weight="600" fill="#e0e0e0" text-anchor="middle">@</text> 4 - </svg>
···
-157
static/login.js
··· 1 - // Check for saved session 2 - const savedDid = localStorage.getItem('atme_did'); 3 - if (savedDid) { 4 - document.getElementById('loginForm').classList.add('hidden'); 5 - document.getElementById('restoring').classList.remove('hidden'); 6 - 7 - fetch('/api/restore-session', { 8 - method: 'POST', 9 - headers: { 'Content-Type': 'application/json' }, 10 - body: JSON.stringify({ did: savedDid }) 11 - }).then(r => { 12 - if (r.ok) { 13 - window.location.href = '/'; 14 - } else { 15 - localStorage.removeItem('atme_did'); 16 - document.getElementById('loginForm').classList.remove('hidden'); 17 - document.getElementById('restoring').classList.add('hidden'); 18 - } 19 - }).catch(() => { 20 - localStorage.removeItem('atme_did'); 21 - document.getElementById('loginForm').classList.remove('hidden'); 22 - document.getElementById('restoring').classList.add('hidden'); 23 - }); 24 - } 25 - 26 - // Fetch and cache atmosphere data 27 - async function fetchAtmosphere() { 28 - const CACHE_KEY = 'atme_atmosphere'; 29 - const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours 30 - 31 - const cached = localStorage.getItem(CACHE_KEY); 32 - if (cached) { 33 - const { data, timestamp } = JSON.parse(cached); 34 - if (Date.now() - timestamp < CACHE_DURATION) { 35 - return data; 36 - } 37 - } 38 - 39 - try { 40 - const response = await fetch('https://ufos-api.microcosm.blue/collections?order=dids-estimate&limit=50'); 41 - const json = await response.json(); 42 - 43 - // Group by namespace (first two segments) 44 - const namespaces = {}; 45 - json.collections.forEach(col => { 46 - const parts = col.nsid.split('.'); 47 - if (parts.length >= 2) { 48 - const ns = `${parts[0]}.${parts[1]}`; 49 - if (!namespaces[ns]) { 50 - namespaces[ns] = { 51 - namespace: ns, 52 - dids_total: 0, 53 - records_total: 0, 54 - collections: [] 55 - }; 56 - } 57 - namespaces[ns].dids_total += col.dids_estimate; 58 - namespaces[ns].records_total += col.creates; 59 - namespaces[ns].collections.push(col.nsid); 60 - } 61 - }); 62 - 63 - const data = Object.values(namespaces).sort((a, b) => b.dids_total - a.dids_total).slice(0, 30); 64 - 65 - localStorage.setItem(CACHE_KEY, JSON.stringify({ 66 - data, 67 - timestamp: Date.now() 68 - })); 69 - 70 - return data; 71 - } catch (e) { 72 - console.error('Failed to fetch atmosphere data:', e); 73 - return []; 74 - } 75 - } 76 - 77 - // Try to fetch app avatar 78 - async function fetchAppAvatar(namespace) { 79 - const reversed = namespace.split('.').reverse().join('.'); 80 - const handles = [reversed, `${reversed}.bsky.social`]; 81 - 82 - for (const handle of handles) { 83 - try { 84 - const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`); 85 - if (!didRes.ok) continue; 86 - 87 - const { did } = await didRes.json(); 88 - const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`); 89 - if (!profileRes.ok) continue; 90 - 91 - const profile = await profileRes.json(); 92 - if (profile.avatar) return profile.avatar; 93 - } catch (e) { 94 - // Silently continue to next handle 95 - continue; 96 - } 97 - } 98 - return null; 99 - } 100 - 101 - // Render atmosphere 102 - async function renderAtmosphere() { 103 - const data = await fetchAtmosphere(); 104 - if (!data.length) return; 105 - 106 - const atmosphere = document.getElementById('atmosphere'); 107 - const maxSize = Math.max(...data.map(d => d.dids_total)); 108 - 109 - data.forEach((app, i) => { 110 - const orb = document.createElement('div'); 111 - orb.className = 'app-orb'; 112 - 113 - // Size based on user count (20-80px) 114 - const size = 20 + (app.dids_total / maxSize) * 60; 115 - 116 - // Position in 3D space 117 - const angle = (i / data.length) * Math.PI * 2; 118 - const radius = 250 + (i % 3) * 100; 119 - const y = (i % 5) * 80 - 160; 120 - const x = Math.cos(angle) * radius; 121 - const z = Math.sin(angle) * radius; 122 - 123 - orb.style.width = `${size}px`; 124 - orb.style.height = `${size}px`; 125 - orb.style.left = `calc(50% + ${x}px)`; 126 - orb.style.top = `calc(50% + ${y}px)`; 127 - orb.style.transform = `translateZ(${z}px) translate(-50%, -50%)`; 128 - orb.style.background = `radial-gradient(circle, rgba(255,255,255,0.1), rgba(255,255,255,0.02))`; 129 - orb.style.border = '1px solid rgba(255,255,255,0.1)'; 130 - orb.style.boxShadow = '0 0 20px rgba(255,255,255,0.1)'; 131 - 132 - // Fallback letter 133 - const letter = app.namespace.split('.')[1]?.[0]?.toUpperCase() || app.namespace[0].toUpperCase(); 134 - orb.innerHTML = `<div class="fallback">${letter}</div>`; 135 - 136 - // Tooltip 137 - const tooltip = document.createElement('div'); 138 - tooltip.className = 'app-tooltip'; 139 - const users = app.dids_total >= 1000000 140 - ? `${(app.dids_total / 1000000).toFixed(1)}M users` 141 - : `${(app.dids_total / 1000).toFixed(0)}K users`; 142 - tooltip.textContent = `${app.namespace} โ€ข ${users}`; 143 - orb.appendChild(tooltip); 144 - 145 - atmosphere.appendChild(orb); 146 - 147 - // Fetch and apply avatar 148 - fetchAppAvatar(app.namespace).then(avatarUrl => { 149 - if (avatarUrl) { 150 - orb.innerHTML = `<img src="${avatarUrl}" alt="${app.namespace}" />`; 151 - orb.appendChild(tooltip); 152 - } 153 - }); 154 - }); 155 - } 156 - 157 - renderAtmosphere();
···
static/og-image.png

This is a binary file and will not be displayed.

-31
static/og-image.svg
··· 1 - <svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg"> 2 - <!-- Background gradient --> 3 - <defs> 4 - <radialGradient id="bg" cx="50%" cy="50%"> 5 - <stop offset="0%" style="stop-color:#0a0a0f;stop-opacity:1" /> 6 - <stop offset="100%" style="stop-color:#000000;stop-opacity:1" /> 7 - </radialGradient> 8 - </defs> 9 - 10 - <!-- Background --> 11 - <rect width="1200" height="630" fill="url(#bg)"/> 12 - 13 - <!-- Orbital rings --> 14 - <circle cx="600" cy="315" r="180" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/> 15 - <circle cx="600" cy="315" r="240" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/> 16 - 17 - <!-- Center circle --> 18 - <circle cx="600" cy="315" r="120" fill="rgba(20,20,25,0.8)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/> 19 - 20 - <!-- @ symbol --> 21 - <text x="600" y="350" font-family="ui-monospace, 'SF Mono', Monaco, monospace" font-size="120" fill="#e5e5e5" text-anchor="middle" font-weight="300">@</text> 22 - 23 - <!-- Title --> 24 - <text x="600" y="480" font-family="ui-monospace, 'SF Mono', Monaco, monospace" font-size="32" fill="#e5e5e5" text-anchor="middle" font-weight="300" letter-spacing="0.05em">explore your atproto identity</text> 25 - 26 - <!-- Small decorative circles representing apps --> 27 - <circle cx="720" cy="260" r="20" fill="rgba(40,40,50,0.6)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/> 28 - <circle cx="480" cy="290" r="18" fill="rgba(40,40,50,0.6)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/> 29 - <circle cx="680" cy="390" r="22" fill="rgba(40,40,50,0.6)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/> 30 - <circle cx="520" cy="350" r="16" fill="rgba(40,40,50,0.6)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/> 31 - </svg>
···
-191
static/onboarding.js
··· 1 - // Onboarding overlay for first-time users 2 - const ONBOARDING_KEY = 'atme_onboarding_seen'; 3 - 4 - const steps = [ 5 - { 6 - target: '.identity', 7 - title: 'this is you', 8 - description: 'your global identity and handle. your data is hosted at your <a href="https://atproto.com/guides/overview" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Personal Data Server (PDS)</a>.', 9 - position: 'bottom' 10 - }, 11 - { 12 - target: '.canvas', 13 - title: 'third-party applications', 14 - description: 'these apps use your global identity to write public records to your PDS. they can also read records you\'ve created.', 15 - position: 'center' 16 - }, 17 - { 18 - target: '.app-view', 19 - title: 'explore your records', 20 - description: 'click any app to see what records it has written to your PDS.', 21 - position: 'bottom' 22 - } 23 - ]; 24 - 25 - let currentStep = 0; 26 - 27 - function showOnboarding() { 28 - const overlay = document.getElementById('onboardingOverlay'); 29 - if (!overlay) return; 30 - 31 - overlay.style.display = 'block'; 32 - setTimeout(() => { 33 - overlay.style.opacity = '1'; 34 - showStep(0); 35 - }, 50); 36 - } 37 - 38 - function hideOnboarding() { 39 - const overlay = document.getElementById('onboardingOverlay'); 40 - const spotlight = document.getElementById('onboardingSpotlight'); 41 - const content = document.getElementById('onboardingContent'); 42 - 43 - if (overlay) { 44 - overlay.style.opacity = '0'; 45 - setTimeout(() => { 46 - overlay.style.display = 'none'; 47 - }, 300); 48 - } 49 - 50 - if (spotlight) spotlight.classList.remove('active'); 51 - if (content) content.classList.remove('active'); 52 - 53 - localStorage.setItem(ONBOARDING_KEY, 'true'); 54 - } 55 - 56 - function showStep(stepIndex) { 57 - if (stepIndex >= steps.length) { 58 - hideOnboarding(); 59 - return; 60 - } 61 - 62 - currentStep = stepIndex; 63 - const step = steps[stepIndex]; 64 - const target = document.querySelector(step.target); 65 - 66 - if (!target) { 67 - console.warn('Onboarding target not found:', step.target); 68 - showStep(stepIndex + 1); 69 - return; 70 - } 71 - 72 - const spotlight = document.getElementById('onboardingSpotlight'); 73 - const content = document.getElementById('onboardingContent'); 74 - 75 - // Position spotlight on target 76 - const rect = target.getBoundingClientRect(); 77 - const padding = step.target === '.canvas' ? 100 : 20; 78 - 79 - spotlight.style.left = `${rect.left - padding}px`; 80 - spotlight.style.top = `${rect.top - padding}px`; 81 - spotlight.style.width = `${rect.width + padding * 2}px`; 82 - spotlight.style.height = `${rect.height + padding * 2}px`; 83 - spotlight.classList.add('active'); 84 - 85 - // Position content 86 - content.innerHTML = ` 87 - <h3>${step.title}</h3> 88 - <p>${step.description}</p> 89 - <div class="onboarding-actions"> 90 - <button id="skipOnboarding" class="onboarding-skip">skip</button> 91 - <button id="nextOnboarding" class="onboarding-next"> 92 - ${stepIndex === steps.length - 1 ? 'got it' : 'next'} 93 - </button> 94 - </div> 95 - <div class="onboarding-progress"> 96 - ${steps.map((_, i) => `<span class="${i === stepIndex ? 'active' : i < stepIndex ? 'done' : ''}"></span>`).join('')} 97 - </div> 98 - `; 99 - 100 - // Position content relative to spotlight 101 - let contentTop, contentLeft; 102 - const contentMaxWidth = Math.min(400, window.innerWidth * 0.9); // responsive max-width 103 - const contentHeight = 250; // approximate height 104 - const margin = Math.max(20, window.innerWidth * 0.05); // responsive margin 105 - 106 - if (step.position === 'bottom') { 107 - contentTop = rect.bottom + padding + margin; 108 - contentLeft = rect.left + rect.width / 2; 109 - 110 - // Check if it would go off bottom 111 - if (contentTop + contentHeight > window.innerHeight) { 112 - contentTop = rect.top - padding - contentHeight - margin; 113 - } 114 - } else if (step.position === 'center') { 115 - contentTop = window.innerHeight / 2 - contentHeight / 2; 116 - contentLeft = window.innerWidth / 2; 117 - } else { 118 - contentTop = rect.top - padding - contentHeight - margin; 119 - contentLeft = rect.left + rect.width / 2; 120 - 121 - // Check if it would go off top 122 - if (contentTop < margin) { 123 - contentTop = rect.bottom + padding + margin; 124 - } 125 - } 126 - 127 - // Ensure content stays on screen horizontally 128 - const halfWidth = contentMaxWidth / 2; 129 - if (contentLeft - halfWidth < margin) { 130 - contentLeft = halfWidth + margin; 131 - } else if (contentLeft + halfWidth > window.innerWidth - margin) { 132 - contentLeft = window.innerWidth - halfWidth - margin; 133 - } 134 - 135 - // Ensure content stays on screen vertically 136 - if (contentTop < margin) { 137 - contentTop = margin; 138 - } else if (contentTop + contentHeight > window.innerHeight - margin) { 139 - contentTop = window.innerHeight - contentHeight - margin; 140 - } 141 - 142 - content.style.top = `${contentTop}px`; 143 - content.style.left = `${contentLeft}px`; 144 - content.style.transform = 'translate(-50%, 0)'; 145 - content.classList.add('active'); 146 - 147 - // Add event listeners 148 - document.getElementById('skipOnboarding').addEventListener('click', hideOnboarding); 149 - document.getElementById('nextOnboarding').addEventListener('click', () => { 150 - showStep(stepIndex + 1); 151 - }); 152 - } 153 - 154 - // Initialize onboarding 155 - function initOnboarding() { 156 - const seen = localStorage.getItem(ONBOARDING_KEY); 157 - 158 - if (!seen) { 159 - // Wait for app circles to render 160 - setTimeout(() => { 161 - showOnboarding(); 162 - }, 1000); 163 - } 164 - } 165 - 166 - // ESC key handler 167 - document.addEventListener('keydown', (e) => { 168 - if (e.key === 'Escape') { 169 - const overlay = document.getElementById('onboardingOverlay'); 170 - if (overlay && overlay.style.display === 'block') { 171 - hideOnboarding(); 172 - } 173 - } 174 - }); 175 - 176 - // Help button handler to restart onboarding 177 - window.restartOnboarding = function() { 178 - localStorage.removeItem(ONBOARDING_KEY); 179 - document.getElementById('infoModal').classList.remove('visible'); 180 - document.getElementById('overlay').classList.remove('visible'); 181 - setTimeout(() => { 182 - showOnboarding(); 183 - }, 300); 184 - }; 185 - 186 - // Start onboarding after page loads 187 - if (document.readyState === 'loading') { 188 - document.addEventListener('DOMContentLoaded', initOnboarding); 189 - } else { 190 - initOnboarding(); 191 - }
···
+168
view/index.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>@me - explore your atproto identity</title> 8 + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 9 + 10 + <!-- Open Graph / Facebook --> 11 + <meta property="og:type" content="website"> 12 + <meta property="og:url" content="https://at-me.wisp.place/"> 13 + <meta property="og:title" content="@me - explore your atproto identity"> 14 + <meta property="og:description" 15 + content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 16 + <meta property="og:image" content="https://at-me.wisp.place/og-image.png"> 17 + 18 + <!-- Twitter --> 19 + <meta property="twitter:card" content="summary_large_image"> 20 + <meta property="twitter:url" content="https://at-me.wisp.place/"> 21 + <meta property="twitter:title" content="@me - explore your atproto identity"> 22 + <meta property="twitter:description" 23 + content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 24 + <meta property="twitter:image" content="https://at-me.wisp.place/og-image.png"> 25 + </head> 26 + 27 + <body> 28 + <a href="/" class="home-btn" title="back to landing"> 29 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 30 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 31 + <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> 32 + <polyline points="9 22 9 12 15 12 15 22" /> 33 + </svg> 34 + </a> 35 + <div class="info" id="infoBtn" title="learn about your data"> 36 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 37 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 38 + <circle cx="12" cy="12" r="10" /> 39 + <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /> 40 + <path d="M12 17h.01" /> 41 + </svg> 42 + </div> 43 + <div class="top-right-buttons"> 44 + <button class="filter-btn" id="filterBtn"> 45 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" 46 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 47 + <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" /> 48 + </svg> 49 + <span class="filter-label-text">filter</span> 50 + <span class="filter-count" id="filterCount" style="display: none;"></span> 51 + </button> 52 + <button class="watch-live-btn" id="watchLiveBtn"> 53 + <span class="watch-indicator"></span> 54 + <span class="watch-label">watch live</span> 55 + </button> 56 + </div> 57 + <div class="filter-panel" id="filterPanel"> 58 + <div class="filter-panel-header"> 59 + <span class="filter-panel-title">show apps</span> 60 + <div class="filter-panel-actions"> 61 + <button type="button" class="filter-action-btn" id="filterShowAll">all</button> 62 + <button type="button" class="filter-action-btn" id="filterHideUnresolved">valid</button> 63 + <button type="button" class="filter-action-btn" id="filterHideAll">none</button> 64 + </div> 65 + </div> 66 + <div class="filter-list" id="filterList"></div> 67 + </div> 68 + <div class="pov-indicator">point of view of <a class="pov-handle" id="povHandle" href="#" target="_blank" 69 + rel="noopener noreferrer"></a></div> 70 + <div class="guestbook-sign">sign the guest list</div> 71 + <div class="guestbook-buttons-container"> 72 + <button class="view-guestbook-btn" id="viewGuestbookBtn" title="view all signatures"> 73 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 74 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 75 + <line x1="8" x2="21" y1="6" y2="6" /> 76 + <line x1="8" x2="21" y1="12" y2="12" /> 77 + <line x1="8" x2="21" y1="18" y2="18" /> 78 + <line x1="3" x2="3.01" y1="6" y2="6" /> 79 + <line x1="3" x2="3.01" y1="12" y2="12" /> 80 + <line x1="3" x2="3.01" y1="18" y2="18" /> 81 + </svg> 82 + </button> 83 + <button class="sign-guestbook-btn" id="signGuestbookBtn" title="sign the guestbook"> 84 + <span class="guestbook-icon"> 85 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" 86 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 87 + <path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" /> 88 + </svg> 89 + </span> 90 + <span class="guestbook-text">sign guestbook</span> 91 + <img class="guestbook-avatar" id="guestbookAvatar" style="display: none;" /> 92 + </button> 93 + </div> 94 + 95 + <div class="firehose-toast" id="firehoseToast"> 96 + <div class="firehose-toast-action"></div> 97 + <div class="firehose-toast-collection"></div> 98 + <a class='firehose-toast-link' id='firehoseToastLink' href='#' target='_blank' rel='noopener noreferrer'>view 99 + record</a> 100 + </div> 101 + 102 + <div class="overlay" id="overlay"></div> 103 + <div class="info-modal" id="infoModal"> 104 + <h2>this is your data</h2> 105 + <p>this visualization shows your <a href="https://atproto.com/guides/data-repos" target="_blank" 106 + rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data 107 + Server</a> - where your social data actually lives. unlike traditional platforms that lock everything in 108 + their database, your posts, likes, and follows are stored here, on infrastructure you control.</p> 109 + <p>each circle represents an app that writes to your space. <a href="https://bsky.app" target="_blank" 110 + rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">bluesky</a> for 111 + microblogging. <a href="https://whtwnd.com" target="_blank" rel="noopener noreferrer" 112 + style="color: var(--text); text-decoration: underline;">whitewind</a> for long-form posts. <a 113 + href="https://tangled.org" target="_blank" rel="noopener noreferrer" 114 + style="color: var(--text); text-decoration: underline;">tangled.org</a> for code hosting. they're all 115 + just different views of the same underlying data - <strong>your</strong> data.</p> 116 + <p>this is what "<a href="https://overreacted.io/open-social/" target="_blank" rel="noopener noreferrer" 117 + style="color: var(--text); text-decoration: underline;">open social</a>" means: your followers, your 118 + content, your connections - they all belong to you, not the app. switch apps anytime and take everything 119 + with you. no platform can hold your social graph hostage.</p> 120 + <p style="margin-bottom: 1rem;"><strong>how to explore:</strong> click your avatar in the center to see the 121 + details of your identity. click any app to browse the records it's created in your repository.</p> 122 + <button id="closeInfo">got it</button> 123 + <p 124 + style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.7rem; color: var(--text-light); display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;"> 125 + <span>view <a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer" 126 + style="color: var(--text); text-decoration: underline;">the source code</a> on</span> 127 + <a href="https://tangled.org" target="_blank" rel="noopener noreferrer" 128 + style="color: var(--text); text-decoration: underline;">tangled.org</a> 129 + </p> 130 + </div> 131 + 132 + <div class="guestbook-modal" id="guestbookModal"> 133 + <button class="guestbook-close" id="guestbookClose">x</button> 134 + <div id="guestbookContent"></div> 135 + </div> 136 + 137 + <div class="canvas"> 138 + <div class="identity"> 139 + <img class="identity-avatar" id="identityAvatar" /> 140 + <div class="identity-handle" id="handleDisplay"></div> 141 + </div> 142 + <div id="field" class="loading"> 143 + <div class="loading-spinner"></div> 144 + <div class="loading-text">loading your data</div> 145 + <div class="loading-progress" id="status">resolving identity...</div> 146 + </div> 147 + </div> 148 + <div id="detail" class="detail-panel"></div> 149 + 150 + <script type="module" src="/src/view/main.js"></script> 151 + <script> 152 + // Info modal handlers (kept inline as they're simple UI toggles) 153 + document.getElementById('infoBtn').addEventListener('click', () => { 154 + document.getElementById('infoModal').classList.add('visible'); 155 + document.getElementById('overlay').classList.add('visible'); 156 + }); 157 + document.getElementById('closeInfo').addEventListener('click', () => { 158 + document.getElementById('infoModal').classList.remove('visible'); 159 + document.getElementById('overlay').classList.remove('visible'); 160 + }); 161 + document.getElementById('overlay').addEventListener('click', () => { 162 + document.getElementById('infoModal').classList.remove('visible'); 163 + document.getElementById('overlay').classList.remove('visible'); 164 + }); 165 + </script> 166 + </body> 167 + 168 + </html>
+168
view.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>@me - explore your atproto identity</title> 8 + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 9 + 10 + <!-- Open Graph / Facebook --> 11 + <meta property="og:type" content="website"> 12 + <meta property="og:url" content="https://at-me.wisp.place/"> 13 + <meta property="og:title" content="@me - explore your atproto identity"> 14 + <meta property="og:description" 15 + content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 16 + <meta property="og:image" content="https://at-me.wisp.place/og-image.png"> 17 + 18 + <!-- Twitter --> 19 + <meta property="twitter:card" content="summary_large_image"> 20 + <meta property="twitter:url" content="https://at-me.wisp.place/"> 21 + <meta property="twitter:title" content="@me - explore your atproto identity"> 22 + <meta property="twitter:description" 23 + content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server"> 24 + <meta property="twitter:image" content="https://at-me.wisp.place/og-image.png"> 25 + </head> 26 + 27 + <body> 28 + <a href="/" class="home-btn" title="back to landing"> 29 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 30 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 31 + <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> 32 + <polyline points="9 22 9 12 15 12 15 22" /> 33 + </svg> 34 + </a> 35 + <div class="info" id="infoBtn" title="learn about your data"> 36 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 37 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 38 + <circle cx="12" cy="12" r="10" /> 39 + <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /> 40 + <path d="M12 17h.01" /> 41 + </svg> 42 + </div> 43 + <div class="top-right-buttons"> 44 + <button class="filter-btn" id="filterBtn"> 45 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" 46 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 47 + <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" /> 48 + </svg> 49 + <span class="filter-label-text">filter</span> 50 + <span class="filter-count" id="filterCount" style="display: none;"></span> 51 + </button> 52 + <button class="watch-live-btn" id="watchLiveBtn"> 53 + <span class="watch-indicator"></span> 54 + <span class="watch-label">watch live</span> 55 + </button> 56 + </div> 57 + <div class="filter-panel" id="filterPanel"> 58 + <div class="filter-panel-header"> 59 + <span class="filter-panel-title">show apps</span> 60 + <div class="filter-panel-actions"> 61 + <button type="button" class="filter-action-btn" id="filterShowAll">all</button> 62 + <button type="button" class="filter-action-btn" id="filterHideUnresolved">valid</button> 63 + <button type="button" class="filter-action-btn" id="filterHideAll">none</button> 64 + </div> 65 + </div> 66 + <div class="filter-list" id="filterList"></div> 67 + </div> 68 + <div class="pov-indicator">point of view of <a class="pov-handle" id="povHandle" href="#" target="_blank" 69 + rel="noopener noreferrer"></a></div> 70 + <div class="guestbook-sign">sign the guest list</div> 71 + <div class="guestbook-buttons-container"> 72 + <button class="view-guestbook-btn" id="viewGuestbookBtn" title="view all signatures"> 73 + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" 74 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 75 + <line x1="8" x2="21" y1="6" y2="6" /> 76 + <line x1="8" x2="21" y1="12" y2="12" /> 77 + <line x1="8" x2="21" y1="18" y2="18" /> 78 + <line x1="3" x2="3.01" y1="6" y2="6" /> 79 + <line x1="3" x2="3.01" y1="12" y2="12" /> 80 + <line x1="3" x2="3.01" y1="18" y2="18" /> 81 + </svg> 82 + </button> 83 + <button class="sign-guestbook-btn" id="signGuestbookBtn" title="sign the guestbook"> 84 + <span class="guestbook-icon"> 85 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" 86 + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 87 + <path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" /> 88 + </svg> 89 + </span> 90 + <span class="guestbook-text">sign guestbook</span> 91 + <img class="guestbook-avatar" id="guestbookAvatar" style="display: none;" /> 92 + </button> 93 + </div> 94 + 95 + <div class="firehose-toast" id="firehoseToast"> 96 + <div class="firehose-toast-action"></div> 97 + <div class="firehose-toast-collection"></div> 98 + <a class='firehose-toast-link' id='firehoseToastLink' href='#' target='_blank' rel='noopener noreferrer'>view 99 + record</a> 100 + </div> 101 + 102 + <div class="overlay" id="overlay"></div> 103 + <div class="info-modal" id="infoModal"> 104 + <h2>this is your data</h2> 105 + <p>this visualization shows your <a href="https://atproto.com/guides/data-repos" target="_blank" 106 + rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data 107 + Server</a> - where your social data actually lives. unlike traditional platforms that lock everything in 108 + their database, your posts, likes, and follows are stored here, on infrastructure you control.</p> 109 + <p>each circle represents an app that writes to your space. <a href="https://bsky.app" target="_blank" 110 + rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">bluesky</a> for 111 + microblogging. <a href="https://whtwnd.com" target="_blank" rel="noopener noreferrer" 112 + style="color: var(--text); text-decoration: underline;">whitewind</a> for long-form posts. <a 113 + href="https://tangled.org" target="_blank" rel="noopener noreferrer" 114 + style="color: var(--text); text-decoration: underline;">tangled.org</a> for code hosting. they're all 115 + just different views of the same underlying data - <strong>your</strong> data.</p> 116 + <p>this is what "<a href="https://overreacted.io/open-social/" target="_blank" rel="noopener noreferrer" 117 + style="color: var(--text); text-decoration: underline;">open social</a>" means: your followers, your 118 + content, your connections - they all belong to you, not the app. switch apps anytime and take everything 119 + with you. no platform can hold your social graph hostage.</p> 120 + <p style="margin-bottom: 1rem;"><strong>how to explore:</strong> click your avatar in the center to see the 121 + details of your identity. click any app to browse the records it's created in your repository.</p> 122 + <button id="closeInfo">got it</button> 123 + <p 124 + style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.7rem; color: var(--text-light); display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;"> 125 + <span>view <a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer" 126 + style="color: var(--text); text-decoration: underline;">the source code</a> on</span> 127 + <a href="https://tangled.org" target="_blank" rel="noopener noreferrer" 128 + style="color: var(--text); text-decoration: underline;">tangled.org</a> 129 + </p> 130 + </div> 131 + 132 + <div class="guestbook-modal" id="guestbookModal"> 133 + <button class="guestbook-close" id="guestbookClose">x</button> 134 + <div id="guestbookContent"></div> 135 + </div> 136 + 137 + <div class="canvas"> 138 + <div class="identity"> 139 + <img class="identity-avatar" id="identityAvatar" /> 140 + <div class="identity-handle" id="handleDisplay"></div> 141 + </div> 142 + <div id="field" class="loading"> 143 + <div class="loading-spinner"></div> 144 + <div class="loading-text">loading your data</div> 145 + <div class="loading-progress" id="status">resolving identity...</div> 146 + </div> 147 + </div> 148 + <div id="detail" class="detail-panel"></div> 149 + 150 + <script type="module" src="/src/view/main.js"></script> 151 + <script> 152 + // Info modal handlers (kept inline as they're simple UI toggles) 153 + document.getElementById('infoBtn').addEventListener('click', () => { 154 + document.getElementById('infoModal').classList.add('visible'); 155 + document.getElementById('overlay').classList.add('visible'); 156 + }); 157 + document.getElementById('closeInfo').addEventListener('click', () => { 158 + document.getElementById('infoModal').classList.remove('visible'); 159 + document.getElementById('overlay').classList.remove('visible'); 160 + }); 161 + document.getElementById('overlay').addEventListener('click', () => { 162 + document.getElementById('infoModal').classList.remove('visible'); 163 + document.getElementById('overlay').classList.remove('visible'); 164 + }); 165 + </script> 166 + </body> 167 + 168 + </html>
+34
vite.config.js
···
··· 1 + import { defineConfig } from 'vite'; 2 + 3 + export default defineConfig({ 4 + root: '.', 5 + publicDir: 'public', 6 + base: './', 7 + build: { 8 + outDir: 'dist', 9 + rollupOptions: { 10 + input: { 11 + main: 'index.html', 12 + view: 'view.html', 13 + 'view-dir': 'view/index.html' 14 + } 15 + } 16 + }, 17 + server: { 18 + port: 3030 19 + }, 20 + appType: 'mpa', 21 + plugins: [ 22 + { 23 + name: 'rewrite-view', 24 + configureServer(server) { 25 + server.middlewares.use((req, res, next) => { 26 + if (req.url.startsWith('/view') && !req.url.includes('.html')) { 27 + req.url = req.url.replace('/view', '/view.html'); 28 + } 29 + next(); 30 + }); 31 + } 32 + } 33 + ] 34 + });