Highly ambitious ATProtocol AppView service and sdks

Compare changes

Choose any two refs to compare.

+366 -27
api/Cargo.lock
··· 3 version = 4 4 5 [[package]] 6 name = "addr2line" 7 version = "0.25.1" 8 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 60 checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 61 62 [[package]] 63 name = "async-trait" 64 version = "0.1.89" 65 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 213 214 [[package]] 215 name = "axum" 216 - version = "0.7.9" 217 source = "registry+https://github.com/rust-lang/crates.io-index" 218 - checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 219 dependencies = [ 220 - "async-trait", 221 "axum-core", 222 "axum-macros", 223 "base64 0.22.1", 224 "bytes", 225 "futures-util", 226 "http", 227 "http-body", ··· 234 "mime", 235 "percent-encoding", 236 "pin-project-lite", 237 - "rustversion", 238 - "serde", 239 "serde_json", 240 "serde_path_to_error", 241 "serde_urlencoded", ··· 251 252 [[package]] 253 name = "axum-core" 254 - version = "0.4.5" 255 source = "registry+https://github.com/rust-lang/crates.io-index" 256 - checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 257 dependencies = [ 258 - "async-trait", 259 "bytes", 260 - "futures-util", 261 "http", 262 "http-body", 263 "http-body-util", 264 "mime", 265 "pin-project-lite", 266 - "rustversion", 267 "sync_wrapper", 268 "tower-layer", 269 "tower-service", ··· 272 273 [[package]] 274 name = "axum-extra" 275 - version = "0.9.6" 276 source = "registry+https://github.com/rust-lang/crates.io-index" 277 - checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" 278 dependencies = [ 279 "axum", 280 "axum-core", 281 "bytes", 282 - "fastrand", 283 "futures-util", 284 "http", 285 "http-body", 286 "http-body-util", 287 "mime", 288 - "multer", 289 "pin-project-lite", 290 - "serde", 291 "serde_html_form", 292 - "tower", 293 "tower-layer", 294 "tower-service", 295 ] 296 297 [[package]] 298 name = "axum-macros" 299 - version = "0.4.2" 300 source = "registry+https://github.com/rust-lang/crates.io-index" 301 - checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" 302 dependencies = [ 303 "proc-macro2", 304 "quote", ··· 394 version = "1.10.1" 395 source = "registry+https://github.com/rust-lang/crates.io-index" 396 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 397 398 [[package]] 399 name = "cbor4ii" ··· 606 ] 607 608 [[package]] 609 name = "data-encoding" 610 version = "2.9.0" 611 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 779 ] 780 781 [[package]] 782 name = "fastrand" 783 version = "2.3.0" 784 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 930 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 931 932 [[package]] 933 name = "futures-util" 934 version = "0.3.31" 935 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1022 ] 1023 1024 [[package]] 1025 name = "hashbrown" 1026 version = "0.15.5" 1027 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1371 ] 1372 1373 [[package]] 1374 name = "idna" 1375 version = "1.1.0" 1376 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1399 dependencies = [ 1400 "equivalent", 1401 "hashbrown 0.16.0", 1402 ] 1403 1404 [[package]] ··· 1587 1588 [[package]] 1589 name = "matchit" 1590 - version = "0.7.3" 1591 source = "registry+https://github.com/rust-lang/crates.io-index" 1592 - checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 1593 1594 [[package]] 1595 name = "md-5" ··· 1917 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1918 1919 [[package]] 1920 name = "pin-project-lite" 1921 version = "0.2.16" 1922 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1990 ] 1991 1992 [[package]] 1993 name = "proc-macro2" 1994 version = "1.0.101" 1995 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2670 version = "0.1.0" 2671 dependencies = [ 2672 "anyhow", 2673 "async-trait", 2674 "atproto-client", 2675 "atproto-identity", ··· 2681 "chrono", 2682 "dotenvy", 2683 "futures-util", 2684 "redis", 2685 "regex", 2686 "reqwest", ··· 2997 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2998 2999 [[package]] 3000 name = "stringprep" 3001 version = "0.1.5" 3002 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3008 ] 3009 3010 [[package]] 3011 name = "subtle" 3012 version = "2.6.1" 3013 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3233 3234 [[package]] 3235 name = "tokio-tungstenite" 3236 - version = "0.24.0" 3237 source = "registry+https://github.com/rust-lang/crates.io-index" 3238 - checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" 3239 dependencies = [ 3240 "futures-util", 3241 "log", ··· 3251 dependencies = [ 3252 "bytes", 3253 "futures-core", 3254 "futures-sink", 3255 "pin-project-lite", 3256 "tokio", ··· 3279 ] 3280 3281 [[package]] 3282 name = "tower" 3283 version = "0.5.2" 3284 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3395 3396 [[package]] 3397 name = "tungstenite" 3398 - version = "0.24.0" 3399 source = "registry+https://github.com/rust-lang/crates.io-index" 3400 - checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" 3401 dependencies = [ 3402 - "byteorder", 3403 "bytes", 3404 "data-encoding", 3405 "http", 3406 "httparse", 3407 "log", 3408 - "rand 0.8.5", 3409 "sha1", 3410 - "thiserror 1.0.69", 3411 "utf-8", 3412 ] 3413 ··· 3416 version = "1.18.0" 3417 source = "registry+https://github.com/rust-lang/crates.io-index" 3418 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 3419 3420 [[package]] 3421 name = "ulid" ··· 4040 version = "0.53.0" 4041 source = "registry+https://github.com/rust-lang/crates.io-index" 4042 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 4043 4044 [[package]] 4045 name = "winreg"
··· 3 version = 4 4 5 [[package]] 6 + name = "Inflector" 7 + version = "0.11.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 + dependencies = [ 11 + "lazy_static", 12 + "regex", 13 + ] 14 + 15 + [[package]] 16 name = "addr2line" 17 version = "0.25.1" 18 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 70 checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 71 72 [[package]] 73 + name = "ascii_utils" 74 + version = "0.9.3" 75 + source = "registry+https://github.com/rust-lang/crates.io-index" 76 + checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" 77 + 78 + [[package]] 79 + name = "async-graphql" 80 + version = "7.0.17" 81 + source = "registry+https://github.com/rust-lang/crates.io-index" 82 + checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" 83 + dependencies = [ 84 + "async-graphql-derive", 85 + "async-graphql-parser", 86 + "async-graphql-value", 87 + "async-stream", 88 + "async-trait", 89 + "base64 0.22.1", 90 + "bytes", 91 + "fast_chemail", 92 + "fnv", 93 + "futures-channel", 94 + "futures-timer", 95 + "futures-util", 96 + "handlebars", 97 + "http", 98 + "indexmap", 99 + "lru", 100 + "mime", 101 + "multer", 102 + "num-traits", 103 + "pin-project-lite", 104 + "regex", 105 + "serde", 106 + "serde_json", 107 + "serde_urlencoded", 108 + "static_assertions_next", 109 + "tempfile", 110 + "thiserror 1.0.69", 111 + ] 112 + 113 + [[package]] 114 + name = "async-graphql-axum" 115 + version = "7.0.17" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "8725874ecfbf399e071150b8619c4071d7b2b7a2f117e173dddef53c6bdb6bb1" 118 + dependencies = [ 119 + "async-graphql", 120 + "axum", 121 + "bytes", 122 + "futures-util", 123 + "serde_json", 124 + "tokio", 125 + "tokio-stream", 126 + "tokio-util", 127 + "tower-service", 128 + ] 129 + 130 + [[package]] 131 + name = "async-graphql-derive" 132 + version = "7.0.17" 133 + source = "registry+https://github.com/rust-lang/crates.io-index" 134 + checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" 135 + dependencies = [ 136 + "Inflector", 137 + "async-graphql-parser", 138 + "darling", 139 + "proc-macro-crate", 140 + "proc-macro2", 141 + "quote", 142 + "strum", 143 + "syn 2.0.106", 144 + "thiserror 1.0.69", 145 + ] 146 + 147 + [[package]] 148 + name = "async-graphql-parser" 149 + version = "7.0.17" 150 + source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" 152 + dependencies = [ 153 + "async-graphql-value", 154 + "pest", 155 + "serde", 156 + "serde_json", 157 + ] 158 + 159 + [[package]] 160 + name = "async-graphql-value" 161 + version = "7.0.17" 162 + source = "registry+https://github.com/rust-lang/crates.io-index" 163 + checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" 164 + dependencies = [ 165 + "bytes", 166 + "indexmap", 167 + "serde", 168 + "serde_json", 169 + ] 170 + 171 + [[package]] 172 + name = "async-stream" 173 + version = "0.3.6" 174 + source = "registry+https://github.com/rust-lang/crates.io-index" 175 + checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 176 + dependencies = [ 177 + "async-stream-impl", 178 + "futures-core", 179 + "pin-project-lite", 180 + ] 181 + 182 + [[package]] 183 + name = "async-stream-impl" 184 + version = "0.3.6" 185 + source = "registry+https://github.com/rust-lang/crates.io-index" 186 + checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 187 + dependencies = [ 188 + "proc-macro2", 189 + "quote", 190 + "syn 2.0.106", 191 + ] 192 + 193 + [[package]] 194 name = "async-trait" 195 version = "0.1.89" 196 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 344 345 [[package]] 346 name = "axum" 347 + version = "0.8.6" 348 source = "registry+https://github.com/rust-lang/crates.io-index" 349 + checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 350 dependencies = [ 351 "axum-core", 352 "axum-macros", 353 "base64 0.22.1", 354 "bytes", 355 + "form_urlencoded", 356 "futures-util", 357 "http", 358 "http-body", ··· 365 "mime", 366 "percent-encoding", 367 "pin-project-lite", 368 + "serde_core", 369 "serde_json", 370 "serde_path_to_error", 371 "serde_urlencoded", ··· 381 382 [[package]] 383 name = "axum-core" 384 + version = "0.5.5" 385 source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 387 dependencies = [ 388 "bytes", 389 + "futures-core", 390 "http", 391 "http-body", 392 "http-body-util", 393 "mime", 394 "pin-project-lite", 395 "sync_wrapper", 396 "tower-layer", 397 "tower-service", ··· 400 401 [[package]] 402 name = "axum-extra" 403 + version = "0.10.3" 404 source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" 406 dependencies = [ 407 "axum", 408 "axum-core", 409 "bytes", 410 + "form_urlencoded", 411 "futures-util", 412 "http", 413 "http-body", 414 "http-body-util", 415 "mime", 416 "pin-project-lite", 417 + "rustversion", 418 + "serde_core", 419 "serde_html_form", 420 + "serde_path_to_error", 421 "tower-layer", 422 "tower-service", 423 + "tracing", 424 ] 425 426 [[package]] 427 name = "axum-macros" 428 + version = "0.5.0" 429 source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 431 dependencies = [ 432 "proc-macro2", 433 "quote", ··· 523 version = "1.10.1" 524 source = "registry+https://github.com/rust-lang/crates.io-index" 525 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 526 + dependencies = [ 527 + "serde", 528 + ] 529 530 [[package]] 531 name = "cbor4ii" ··· 738 ] 739 740 [[package]] 741 + name = "darling" 742 + version = "0.20.11" 743 + source = "registry+https://github.com/rust-lang/crates.io-index" 744 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 745 + dependencies = [ 746 + "darling_core", 747 + "darling_macro", 748 + ] 749 + 750 + [[package]] 751 + name = "darling_core" 752 + version = "0.20.11" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 755 + dependencies = [ 756 + "fnv", 757 + "ident_case", 758 + "proc-macro2", 759 + "quote", 760 + "strsim", 761 + "syn 2.0.106", 762 + ] 763 + 764 + [[package]] 765 + name = "darling_macro" 766 + version = "0.20.11" 767 + source = "registry+https://github.com/rust-lang/crates.io-index" 768 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 769 + dependencies = [ 770 + "darling_core", 771 + "quote", 772 + "syn 2.0.106", 773 + ] 774 + 775 + [[package]] 776 name = "data-encoding" 777 version = "2.9.0" 778 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 946 ] 947 948 [[package]] 949 + name = "fast_chemail" 950 + version = "0.9.6" 951 + source = "registry+https://github.com/rust-lang/crates.io-index" 952 + checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" 953 + dependencies = [ 954 + "ascii_utils", 955 + ] 956 + 957 + [[package]] 958 name = "fastrand" 959 version = "2.3.0" 960 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1106 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 1107 1108 [[package]] 1109 + name = "futures-timer" 1110 + version = "3.0.3" 1111 + source = "registry+https://github.com/rust-lang/crates.io-index" 1112 + checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 1113 + 1114 + [[package]] 1115 name = "futures-util" 1116 version = "0.3.31" 1117 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1204 ] 1205 1206 [[package]] 1207 + name = "handlebars" 1208 + version = "5.1.2" 1209 + source = "registry+https://github.com/rust-lang/crates.io-index" 1210 + checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" 1211 + dependencies = [ 1212 + "log", 1213 + "pest", 1214 + "pest_derive", 1215 + "serde", 1216 + "serde_json", 1217 + "thiserror 1.0.69", 1218 + ] 1219 + 1220 + [[package]] 1221 name = "hashbrown" 1222 version = "0.15.5" 1223 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1567 ] 1568 1569 [[package]] 1570 + name = "ident_case" 1571 + version = "1.0.1" 1572 + source = "registry+https://github.com/rust-lang/crates.io-index" 1573 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1574 + 1575 + [[package]] 1576 name = "idna" 1577 version = "1.1.0" 1578 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1601 dependencies = [ 1602 "equivalent", 1603 "hashbrown 0.16.0", 1604 + "serde", 1605 + "serde_core", 1606 ] 1607 1608 [[package]] ··· 1791 1792 [[package]] 1793 name = "matchit" 1794 + version = "0.8.4" 1795 source = "registry+https://github.com/rust-lang/crates.io-index" 1796 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1797 1798 [[package]] 1799 name = "md-5" ··· 2121 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 2122 2123 [[package]] 2124 + name = "pest" 2125 + version = "2.8.2" 2126 + source = "registry+https://github.com/rust-lang/crates.io-index" 2127 + checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" 2128 + dependencies = [ 2129 + "memchr", 2130 + "thiserror 2.0.16", 2131 + "ucd-trie", 2132 + ] 2133 + 2134 + [[package]] 2135 + name = "pest_derive" 2136 + version = "2.8.2" 2137 + source = "registry+https://github.com/rust-lang/crates.io-index" 2138 + checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" 2139 + dependencies = [ 2140 + "pest", 2141 + "pest_generator", 2142 + ] 2143 + 2144 + [[package]] 2145 + name = "pest_generator" 2146 + version = "2.8.2" 2147 + source = "registry+https://github.com/rust-lang/crates.io-index" 2148 + checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" 2149 + dependencies = [ 2150 + "pest", 2151 + "pest_meta", 2152 + "proc-macro2", 2153 + "quote", 2154 + "syn 2.0.106", 2155 + ] 2156 + 2157 + [[package]] 2158 + name = "pest_meta" 2159 + version = "2.8.2" 2160 + source = "registry+https://github.com/rust-lang/crates.io-index" 2161 + checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" 2162 + dependencies = [ 2163 + "pest", 2164 + "sha2", 2165 + ] 2166 + 2167 + [[package]] 2168 name = "pin-project-lite" 2169 version = "0.2.16" 2170 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2238 ] 2239 2240 [[package]] 2241 + name = "proc-macro-crate" 2242 + version = "3.4.0" 2243 + source = "registry+https://github.com/rust-lang/crates.io-index" 2244 + checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" 2245 + dependencies = [ 2246 + "toml_edit", 2247 + ] 2248 + 2249 + [[package]] 2250 name = "proc-macro2" 2251 version = "1.0.101" 2252 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2927 version = "0.1.0" 2928 dependencies = [ 2929 "anyhow", 2930 + "async-graphql", 2931 + "async-graphql-axum", 2932 "async-trait", 2933 "atproto-client", 2934 "atproto-identity", ··· 2940 "chrono", 2941 "dotenvy", 2942 "futures-util", 2943 + "lazy_static", 2944 "redis", 2945 "regex", 2946 "reqwest", ··· 3257 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3258 3259 [[package]] 3260 + name = "static_assertions_next" 3261 + version = "1.1.2" 3262 + source = "registry+https://github.com/rust-lang/crates.io-index" 3263 + checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" 3264 + 3265 + [[package]] 3266 name = "stringprep" 3267 version = "0.1.5" 3268 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3274 ] 3275 3276 [[package]] 3277 + name = "strsim" 3278 + version = "0.11.1" 3279 + source = "registry+https://github.com/rust-lang/crates.io-index" 3280 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 3281 + 3282 + [[package]] 3283 + name = "strum" 3284 + version = "0.26.3" 3285 + source = "registry+https://github.com/rust-lang/crates.io-index" 3286 + checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 3287 + dependencies = [ 3288 + "strum_macros", 3289 + ] 3290 + 3291 + [[package]] 3292 + name = "strum_macros" 3293 + version = "0.26.4" 3294 + source = "registry+https://github.com/rust-lang/crates.io-index" 3295 + checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 3296 + dependencies = [ 3297 + "heck", 3298 + "proc-macro2", 3299 + "quote", 3300 + "rustversion", 3301 + "syn 2.0.106", 3302 + ] 3303 + 3304 + [[package]] 3305 name = "subtle" 3306 version = "2.6.1" 3307 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3527 3528 [[package]] 3529 name = "tokio-tungstenite" 3530 + version = "0.28.0" 3531 source = "registry+https://github.com/rust-lang/crates.io-index" 3532 + checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" 3533 dependencies = [ 3534 "futures-util", 3535 "log", ··· 3545 dependencies = [ 3546 "bytes", 3547 "futures-core", 3548 + "futures-io", 3549 "futures-sink", 3550 "pin-project-lite", 3551 "tokio", ··· 3574 ] 3575 3576 [[package]] 3577 + name = "toml_datetime" 3578 + version = "0.7.2" 3579 + source = "registry+https://github.com/rust-lang/crates.io-index" 3580 + checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" 3581 + dependencies = [ 3582 + "serde_core", 3583 + ] 3584 + 3585 + [[package]] 3586 + name = "toml_edit" 3587 + version = "0.23.6" 3588 + source = "registry+https://github.com/rust-lang/crates.io-index" 3589 + checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" 3590 + dependencies = [ 3591 + "indexmap", 3592 + "toml_datetime", 3593 + "toml_parser", 3594 + "winnow", 3595 + ] 3596 + 3597 + [[package]] 3598 + name = "toml_parser" 3599 + version = "1.0.3" 3600 + source = "registry+https://github.com/rust-lang/crates.io-index" 3601 + checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" 3602 + dependencies = [ 3603 + "winnow", 3604 + ] 3605 + 3606 + [[package]] 3607 name = "tower" 3608 version = "0.5.2" 3609 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3720 3721 [[package]] 3722 name = "tungstenite" 3723 + version = "0.28.0" 3724 source = "registry+https://github.com/rust-lang/crates.io-index" 3725 + checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" 3726 dependencies = [ 3727 "bytes", 3728 "data-encoding", 3729 "http", 3730 "httparse", 3731 "log", 3732 + "rand 0.9.2", 3733 "sha1", 3734 + "thiserror 2.0.16", 3735 "utf-8", 3736 ] 3737 ··· 3740 version = "1.18.0" 3741 source = "registry+https://github.com/rust-lang/crates.io-index" 3742 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 3743 + 3744 + [[package]] 3745 + name = "ucd-trie" 3746 + version = "0.1.7" 3747 + source = "registry+https://github.com/rust-lang/crates.io-index" 3748 + checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 3749 3750 [[package]] 3751 name = "ulid" ··· 4370 version = "0.53.0" 4371 source = "registry+https://github.com/rust-lang/crates.io-index" 4372 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 4373 + 4374 + [[package]] 4375 + name = "winnow" 4376 + version = "0.7.13" 4377 + source = "registry+https://github.com/rust-lang/crates.io-index" 4378 + checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 4379 + dependencies = [ 4380 + "memchr", 4381 + ] 4382 4383 [[package]] 4384 name = "winreg"
+7 -2
api/Cargo.toml
··· 19 20 # HTTP client and server 21 reqwest = { version = "0.12", features = ["json", "stream"] } 22 - axum = { version = "0.7", features = ["ws", "macros"] } 23 - axum-extra = { version = "0.9", features = ["form"] } 24 tower = "0.5" 25 tower-http = { version = "0.6", features = ["cors", "trace"] } 26 ··· 65 66 # Redis for caching 67 redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
··· 19 20 # HTTP client and server 21 reqwest = { version = "0.12", features = ["json", "stream"] } 22 + axum = { version = "0.8", features = ["ws", "macros"] } 23 + axum-extra = { version = "0.10", features = ["form"] } 24 tower = "0.5" 25 tower-http = { version = "0.6", features = ["cors", "trace"] } 26 ··· 65 66 # Redis for caching 67 redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] } 68 + 69 + # GraphQL server 70 + async-graphql = { version = "7.0", features = ["dynamic-schema", "dataloader"] } 71 + async-graphql-axum = "7.0" 72 + lazy_static = "1.5"
+35
api/src/database/actors.rs
··· 251 .await?; 252 Ok(result.rows_affected()) 253 } 254 } 255 256 /// Builds WHERE conditions specifically for actor queries.
··· 251 .await?; 252 Ok(result.rows_affected()) 253 } 254 + 255 + /// Resolves actor handles to DIDs for a specific slice. 256 + /// 257 + /// # Arguments 258 + /// * `handles` - List of handles to resolve 259 + /// * `slice_uri` - AT-URI of the slice 260 + /// 261 + /// # Returns 262 + /// Vec of DIDs corresponding to the handles 263 + pub async fn resolve_handles_to_dids( 264 + &self, 265 + handles: &[String], 266 + slice_uri: &str, 267 + ) -> Result<Vec<String>, DatabaseError> { 268 + if handles.is_empty() { 269 + return Ok(Vec::new()); 270 + } 271 + 272 + let placeholders: Vec<String> = (1..=handles.len()) 273 + .map(|i| format!("${}", i)) 274 + .collect(); 275 + let query_sql = format!( 276 + "SELECT DISTINCT did FROM actor WHERE handle = ANY(ARRAY[{}]) AND slice_uri = ${}", 277 + placeholders.join(", "), 278 + handles.len() + 1 279 + ); 280 + 281 + let mut query = sqlx::query_scalar::<_, String>(&query_sql); 282 + for handle in handles { 283 + query = query.bind(handle); 284 + } 285 + query = query.bind(slice_uri); 286 + 287 + Ok(query.fetch_all(&self.pool).await?) 288 + } 289 } 290 291 /// Builds WHERE conditions specifically for actor queries.
+8 -2
api/src/database/records.rs
··· 347 348 query_builder = query_builder.bind(limit as i64); 349 350 - let records = query_builder.fetch_all(&self.pool).await?; 351 352 // Only return cursor if we got a full page, indicating there might be more 353 let cursor = if records.len() < limit as usize { ··· 496 &self, 497 slice_uri: &str, 498 ) -> Result<u64, DatabaseError> { 499 - let result = sqlx::query("DELETE FROM record WHERE slice_uri = $1") 500 .bind(slice_uri) 501 .execute(&self.pool) 502 .await?;
··· 347 348 query_builder = query_builder.bind(limit as i64); 349 350 + let mut records = query_builder.fetch_all(&self.pool).await?; 351 + 352 + // Deduplicate lexicon records by URI (same URI can exist with different slice_uri values) 353 + if is_lexicon { 354 + let mut seen_uris = std::collections::HashSet::new(); 355 + records.retain(|record| seen_uris.insert(record.uri.clone())); 356 + } 357 358 // Only return cursor if we got a full page, indicating there might be more 359 let cursor = if records.len() < limit as usize { ··· 502 &self, 503 slice_uri: &str, 504 ) -> Result<u64, DatabaseError> { 505 + let result = sqlx::query("DELETE FROM record WHERE slice_uri = $1 AND collection NOT LIKE 'network.slices.%'") 506 .bind(slice_uri) 507 .execute(&self.pool) 508 .await?;
+4 -5
api/src/database/slices.rs
··· 147 Ok(count.count.unwrap_or(0)) 148 } 149 150 - /// Gets all slice URIs that have lexicons defined. 151 /// 152 - /// Useful for discovering all active slices in the system. 153 pub async fn get_all_slices(&self) -> Result<Vec<String>, DatabaseError> { 154 let rows: Vec<(String,)> = sqlx::query_as( 155 r#" 156 - SELECT DISTINCT json->>'slice' as slice_uri 157 FROM record 158 - WHERE collection = 'network.slices.lexicon' 159 - AND json->>'slice' IS NOT NULL 160 "#, 161 ) 162 .fetch_all(&self.pool)
··· 147 Ok(count.count.unwrap_or(0)) 148 } 149 150 + /// Gets all slice URIs from network.slices.slice records. 151 /// 152 + /// Returns all slices that exist in the system 153 pub async fn get_all_slices(&self) -> Result<Vec<String>, DatabaseError> { 154 let rows: Vec<(String,)> = sqlx::query_as( 155 r#" 156 + SELECT DISTINCT uri as slice_uri 157 FROM record 158 + WHERE collection = 'network.slices.slice' 159 "#, 160 ) 161 .fetch_all(&self.pool)
+27
api/src/graphql/dataloaders.rs
···
··· 1 + //! DataLoader utilities for extracting references from records 2 + 3 + use serde_json::Value; 4 + 5 + /// Extract URI from a strongRef value 6 + /// strongRef format: { "$type": "com.atproto.repo.strongRef", "uri": "at://...", "cid": "..." } 7 + pub fn extract_uri_from_strong_ref(value: &Value) -> Option<String> { 8 + if let Some(obj) = value.as_object() { 9 + // Check if this is a strongRef 10 + if let Some(type_val) = obj.get("$type") { 11 + if type_val.as_str() == Some("com.atproto.repo.strongRef") { 12 + return obj.get("uri").and_then(|u| u.as_str()).map(|s| s.to_string()); 13 + } 14 + } 15 + 16 + // Also support direct uri field (some lexicons might use this) 17 + if let Some(uri) = obj.get("uri") { 18 + if let Some(uri_str) = uri.as_str() { 19 + if uri_str.starts_with("at://") { 20 + return Some(uri_str.to_string()); 21 + } 22 + } 23 + } 24 + } 25 + 26 + None 27 + }
+147
api/src/graphql/handler.rs
···
··· 1 + //! GraphQL HTTP handler for Axum 2 + 3 + use async_graphql::dynamic::Schema; 4 + use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; 5 + use axum::{ 6 + extract::{Query, State}, 7 + http::{HeaderMap, StatusCode}, 8 + response::Html, 9 + }; 10 + use serde::Deserialize; 11 + use std::sync::Arc; 12 + use tokio::sync::RwLock; 13 + 14 + use crate::errors::AppError; 15 + use crate::AppState; 16 + 17 + /// Global schema cache (one schema per slice) 18 + /// This prevents rebuilding the schema on every request 19 + type SchemaCache = Arc<RwLock<std::collections::HashMap<String, Schema>>>; 20 + 21 + lazy_static::lazy_static! { 22 + static ref SCHEMA_CACHE: SchemaCache = Arc::new(RwLock::new(std::collections::HashMap::new())); 23 + } 24 + 25 + #[derive(Deserialize, Default)] 26 + pub struct GraphQLParams { 27 + pub slice: Option<String>, 28 + } 29 + 30 + /// GraphQL query handler 31 + /// Accepts slice URI from either query parameter (?slice=...) or HTTP header (X-Slice-Uri) 32 + pub async fn graphql_handler( 33 + State(state): State<AppState>, 34 + Query(params): Query<GraphQLParams>, 35 + headers: HeaderMap, 36 + req: GraphQLRequest, 37 + ) -> Result<GraphQLResponse, (StatusCode, String)> { 38 + // Get slice URI from query param or header 39 + let slice_uri = params 40 + .slice 41 + .or_else(|| { 42 + headers 43 + .get("x-slice-uri") 44 + .and_then(|h| h.to_str().ok()) 45 + .map(|s| s.to_string()) 46 + }) 47 + .ok_or_else(|| { 48 + ( 49 + StatusCode::BAD_REQUEST, 50 + "Missing slice parameter. Provide either ?slice=... query parameter or X-Slice-Uri header".to_string(), 51 + ) 52 + })?; 53 + 54 + let schema = match get_or_build_schema(&state, &slice_uri).await { 55 + Ok(s) => s, 56 + Err(e) => { 57 + tracing::error!("Failed to get GraphQL schema: {:?}", e); 58 + return Ok(async_graphql::Response::from_errors(vec![async_graphql::ServerError::new( 59 + format!("Schema error: {:?}", e), 60 + None, 61 + )]) 62 + .into()); 63 + } 64 + }; 65 + 66 + Ok(schema.execute(req.into_inner()).await.into()) 67 + } 68 + 69 + /// GraphQL Playground UI handler 70 + /// Configures the playground with the slice URI in headers 71 + pub async fn graphql_playground( 72 + Query(params): Query<GraphQLParams>, 73 + ) -> Result<Html<String>, (StatusCode, String)> { 74 + let slice_uri = params.slice.ok_or_else(|| { 75 + ( 76 + StatusCode::BAD_REQUEST, 77 + "Missing slice parameter. Provide ?slice=... query parameter".to_string(), 78 + ) 79 + })?; 80 + 81 + // Create playground with pre-configured headers 82 + let playground_html = format!( 83 + r#"<!DOCTYPE html> 84 + <html> 85 + <head> 86 + <meta charset=utf-8 /> 87 + <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui"> 88 + <title>Slices GraphQL Playground</title> 89 + <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" /> 90 + <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" /> 91 + <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script> 92 + </head> 93 + <body> 94 + <div id="root"></div> 95 + <script> 96 + window.addEventListener('load', function (event) {{ 97 + GraphQLPlayground.init(document.getElementById('root'), {{ 98 + endpoint: '/graphql', 99 + settings: {{ 100 + 'request.credentials': 'omit', 101 + }}, 102 + tabs: [{{ 103 + endpoint: '/graphql', 104 + headers: {{ 105 + 'X-Slice-Uri': '{}' 106 + }} 107 + }}] 108 + }}) 109 + }}) 110 + </script> 111 + </body> 112 + </html>"#, 113 + slice_uri.replace("'", "\\'").replace("\"", "\\\"") 114 + ); 115 + 116 + Ok(Html(playground_html)) 117 + } 118 + 119 + /// Gets schema from cache or builds it if not cached 120 + async fn get_or_build_schema( 121 + state: &AppState, 122 + slice_uri: &str, 123 + ) -> Result<Schema, AppError> { 124 + // Check cache first 125 + { 126 + let cache = SCHEMA_CACHE.read().await; 127 + if let Some(schema) = cache.get(slice_uri) { 128 + return Ok(schema.clone()); 129 + } 130 + } 131 + 132 + // Build schema 133 + let schema = crate::graphql::build_graphql_schema( 134 + state.database.clone(), 135 + slice_uri.to_string(), 136 + ) 137 + .await 138 + .map_err(|e| AppError::Internal(format!("Failed to build GraphQL schema: {}", e)))?; 139 + 140 + // Cache it 141 + { 142 + let mut cache = SCHEMA_CACHE.write().await; 143 + cache.insert(slice_uri.to_string(), schema.clone()); 144 + } 145 + 146 + Ok(schema) 147 + }
+12
api/src/graphql/mod.rs
···
··· 1 + //! GraphQL endpoint implementation for Slices 2 + //! 3 + //! This module provides a GraphQL interface to query slice records with support 4 + //! for joining linked records through AT Protocol strongRef references. 5 + 6 + mod schema_builder; 7 + mod dataloaders; 8 + mod types; 9 + pub mod handler; 10 + 11 + pub use schema_builder::build_graphql_schema; 12 + pub use handler::{graphql_handler, graphql_playground};
+1445
api/src/graphql/schema_builder.rs
···
··· 1 + //! Dynamic GraphQL schema builder from AT Protocol lexicons 2 + //! 3 + //! This module generates GraphQL schemas at runtime based on lexicon definitions 4 + //! stored in the database, enabling flexible querying of slice records. 5 + 6 + use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, Schema, Scalar, TypeRef, InputObject, InputValue, Enum, EnumItem}; 7 + use async_graphql::{Error, Value as GraphQLValue}; 8 + use base64::engine::general_purpose; 9 + use base64::Engine; 10 + use serde_json; 11 + use std::collections::HashMap; 12 + use std::sync::Arc; 13 + use tokio::sync::Mutex; 14 + 15 + use crate::database::Database; 16 + use crate::graphql::types::{extract_collection_fields, extract_record_key, GraphQLField, GraphQLType}; 17 + 18 + /// Metadata about a collection for cross-referencing 19 + #[derive(Clone)] 20 + struct CollectionMeta { 21 + nsid: String, 22 + key_type: String, // "tid", "literal:self", or "any" 23 + type_name: String, // GraphQL type name for this collection 24 + } 25 + 26 + /// Builds a dynamic GraphQL schema from lexicons for a given slice 27 + pub async fn build_graphql_schema( 28 + database: Database, 29 + slice_uri: String, 30 + ) -> Result<Schema, String> { 31 + // Fetch all lexicons for this slice 32 + let all_lexicons = database 33 + .get_lexicons_by_slice(&slice_uri) 34 + .await 35 + .map_err(|e| format!("Failed to load lexicons: {}", e))?; 36 + 37 + // Deduplicate by NSID for schema building (keep most recent due to ORDER BY indexed_at DESC) 38 + // This prevents duplicate type registration errors without hiding duplicates from users 39 + let mut seen_nsids = std::collections::HashSet::new(); 40 + let lexicons: Vec<serde_json::Value> = all_lexicons 41 + .into_iter() 42 + .filter(|lexicon| { 43 + if let Some(nsid) = lexicon.get("id").and_then(|n| n.as_str()) { 44 + seen_nsids.insert(nsid.to_string()) 45 + } else { 46 + true // Keep lexicons without an ID (will fail validation later) 47 + } 48 + }) 49 + .collect(); 50 + 51 + // Build Query root type and collect all object types 52 + let mut query = Object::new("Query"); 53 + let mut objects_to_register = Vec::new(); 54 + 55 + // First pass: collect metadata about all collections for cross-referencing 56 + let mut all_collections: Vec<CollectionMeta> = Vec::new(); 57 + for lexicon in &lexicons { 58 + let nsid = lexicon 59 + .get("id") 60 + .and_then(|n| n.as_str()) 61 + .ok_or_else(|| "Lexicon missing id".to_string())?; 62 + 63 + let defs = lexicon 64 + .get("defs") 65 + .ok_or_else(|| format!("Lexicon {} missing defs", nsid))?; 66 + 67 + let fields = extract_collection_fields(defs); 68 + if !fields.is_empty() { 69 + if let Some(key_type) = extract_record_key(defs) { 70 + all_collections.push(CollectionMeta { 71 + nsid: nsid.to_string(), 72 + key_type, 73 + type_name: nsid_to_type_name(nsid), 74 + }); 75 + } 76 + } 77 + } 78 + 79 + // Second pass: create types and queries 80 + for lexicon in &lexicons { 81 + // get_lexicons_by_slice returns {lexicon: 1, id: "nsid", defs: {...}} 82 + let nsid = lexicon 83 + .get("id") 84 + .and_then(|n| n.as_str()) 85 + .ok_or_else(|| "Lexicon missing id".to_string())?; 86 + 87 + let defs = lexicon 88 + .get("defs") 89 + .ok_or_else(|| format!("Lexicon {} missing defs", nsid))? 90 + .clone(); 91 + 92 + // Extract fields from lexicon 93 + let fields = extract_collection_fields(&defs); 94 + 95 + if !fields.is_empty() { 96 + // Create a GraphQL type for this collection 97 + let type_name = nsid_to_type_name(nsid); 98 + let record_type = create_record_type(&type_name, &fields, database.clone(), slice_uri.clone(), &all_collections); 99 + 100 + // Create edge and connection types for this collection (Relay standard) 101 + let edge_type = create_edge_type(&type_name); 102 + let connection_type = create_connection_type(&type_name); 103 + 104 + // Collect the types to register with schema later 105 + objects_to_register.push(record_type); 106 + objects_to_register.push(edge_type); 107 + objects_to_register.push(connection_type); 108 + 109 + // Add query field for this collection 110 + let collection_query_name = nsid_to_query_name(nsid); 111 + let db_clone = database.clone(); 112 + let slice_clone = slice_uri.clone(); 113 + let nsid_clone = nsid.to_string(); 114 + 115 + let connection_type_name = format!("{}Connection", &type_name); 116 + query = query.field( 117 + Field::new( 118 + &collection_query_name, 119 + TypeRef::named_nn(&connection_type_name), 120 + move |ctx| { 121 + let db = db_clone.clone(); 122 + let slice = slice_clone.clone(); 123 + let collection = nsid_clone.clone(); 124 + 125 + FieldFuture::new(async move { 126 + // Get Relay-standard pagination arguments 127 + let first: i32 = match ctx.args.get("first") { 128 + Some(val) => val.i64().unwrap_or(50) as i32, 129 + None => 50, 130 + }; 131 + 132 + let after: Option<&str> = match ctx.args.get("after") { 133 + Some(val) => val.string().ok(), 134 + None => None, 135 + }; 136 + 137 + // Parse sortBy argument 138 + let sort_by: Option<Vec<crate::models::SortField>> = match ctx.args.get("sortBy") { 139 + Some(val) => { 140 + if let Ok(list) = val.list() { 141 + let mut sort_fields = Vec::new(); 142 + for item in list.iter() { 143 + if let Ok(obj) = item.object() { 144 + let field = obj.get("field") 145 + .and_then(|v| v.string().ok()) 146 + .unwrap_or("indexedAt") 147 + .to_string(); 148 + let direction = obj.get("direction") 149 + .and_then(|v| v.string().ok()) 150 + .unwrap_or("desc") 151 + .to_string(); 152 + sort_fields.push(crate::models::SortField { field, direction }); 153 + } 154 + } 155 + Some(sort_fields) 156 + } else { 157 + None 158 + } 159 + }, 160 + None => None, 161 + }; 162 + 163 + // Build where clause for this collection 164 + let mut where_clause = crate::models::WhereClause { 165 + conditions: HashMap::new(), 166 + or_conditions: None, 167 + }; 168 + 169 + // Always filter by collection 170 + where_clause.conditions.insert( 171 + "collection".to_string(), 172 + crate::models::WhereCondition { 173 + eq: Some(serde_json::Value::String(collection.clone())), 174 + in_values: None, 175 + contains: None, 176 + }, 177 + ); 178 + 179 + // Parse where argument if provided 180 + if let Some(where_val) = ctx.args.get("where") { 181 + // Try to parse as JSON object 182 + if let Ok(where_obj) = where_val.object() { 183 + for (field_name, condition_val) in where_obj.iter() { 184 + if let Ok(condition_obj) = condition_val.object() { 185 + let mut where_condition = crate::models::WhereCondition { 186 + eq: None, 187 + in_values: None, 188 + contains: None, 189 + }; 190 + 191 + // Parse eq condition 192 + if let Some(eq_val) = condition_obj.get("eq") { 193 + if let Ok(eq_str) = eq_val.string() { 194 + where_condition.eq = Some(serde_json::Value::String(eq_str.to_string())); 195 + } else if let Ok(eq_i64) = eq_val.i64() { 196 + where_condition.eq = Some(serde_json::Value::Number(eq_i64.into())); 197 + } 198 + } 199 + 200 + // Parse in condition 201 + if let Some(in_val) = condition_obj.get("in") { 202 + if let Ok(in_list) = in_val.list() { 203 + let mut values = Vec::new(); 204 + for item in in_list.iter() { 205 + if let Ok(s) = item.string() { 206 + values.push(serde_json::Value::String(s.to_string())); 207 + } else if let Ok(i) = item.i64() { 208 + values.push(serde_json::Value::Number(i.into())); 209 + } 210 + } 211 + where_condition.in_values = Some(values); 212 + } 213 + } 214 + 215 + // Parse contains condition 216 + if let Some(contains_val) = condition_obj.get("contains") { 217 + if let Ok(contains_str) = contains_val.string() { 218 + where_condition.contains = Some(contains_str.to_string()); 219 + } 220 + } 221 + 222 + where_clause.conditions.insert(field_name.to_string(), where_condition); 223 + } 224 + } 225 + } 226 + } 227 + 228 + // Resolve actorHandle to did if present 229 + if let Some(actor_handle_condition) = where_clause.conditions.remove("actorHandle") { 230 + // Collect handles to resolve 231 + let mut handles = Vec::new(); 232 + if let Some(eq_value) = &actor_handle_condition.eq { 233 + if let Some(handle_str) = eq_value.as_str() { 234 + handles.push(handle_str.to_string()); 235 + } 236 + } 237 + if let Some(in_values) = &actor_handle_condition.in_values { 238 + for value in in_values { 239 + if let Some(handle_str) = value.as_str() { 240 + handles.push(handle_str.to_string()); 241 + } 242 + } 243 + } 244 + 245 + // Resolve handles to DIDs from actor table 246 + if !handles.is_empty() { 247 + match db.resolve_handles_to_dids(&handles, &slice).await { 248 + Ok(dids) => { 249 + if !dids.is_empty() { 250 + // Replace actorHandle condition with did condition 251 + let did_condition = if dids.len() == 1 { 252 + crate::models::WhereCondition { 253 + eq: Some(serde_json::Value::String(dids[0].clone())), 254 + in_values: None, 255 + contains: None, 256 + } 257 + } else { 258 + crate::models::WhereCondition { 259 + eq: None, 260 + in_values: Some(dids.into_iter().map(|d| serde_json::Value::String(d)).collect()), 261 + contains: None, 262 + } 263 + }; 264 + where_clause.conditions.insert("did".to_string(), did_condition); 265 + } 266 + // If no DIDs found, the query will return 0 results naturally 267 + } 268 + Err(_) => { 269 + // If resolution fails, skip the condition (will return 0 results) 270 + } 271 + } 272 + } 273 + } 274 + 275 + // Query database for records 276 + let (records, next_cursor) = db 277 + .get_slice_collections_records( 278 + &slice, 279 + Some(first), 280 + after, 281 + sort_by.as_ref(), 282 + Some(&where_clause), 283 + ) 284 + .await 285 + .map_err(|e| { 286 + Error::new(format!("Database query failed: {}", e)) 287 + })?; 288 + 289 + // Query database for total count 290 + let total_count = db 291 + .count_slice_collections_records(&slice, Some(&where_clause)) 292 + .await 293 + .map_err(|e| { 294 + Error::new(format!("Count query failed: {}", e)) 295 + })? as i32; 296 + 297 + // Convert records to RecordContainers 298 + let record_containers: Vec<RecordContainer> = records 299 + .into_iter() 300 + .map(|record| { 301 + // Convert Record to IndexedRecord 302 + let indexed_record = crate::models::IndexedRecord { 303 + uri: record.uri, 304 + cid: record.cid, 305 + did: record.did, 306 + collection: record.collection, 307 + value: record.json, 308 + indexed_at: record.indexed_at.to_rfc3339(), 309 + }; 310 + RecordContainer { 311 + record: indexed_record, 312 + } 313 + }) 314 + .collect(); 315 + 316 + // Build Connection data 317 + let connection_data = ConnectionData { 318 + total_count, 319 + has_next_page: next_cursor.is_some(), 320 + end_cursor: next_cursor, 321 + nodes: record_containers, 322 + }; 323 + 324 + Ok(Some(FieldValue::owned_any(connection_data))) 325 + }) 326 + }, 327 + ) 328 + .argument(async_graphql::dynamic::InputValue::new( 329 + "first", 330 + TypeRef::named(TypeRef::INT), 331 + )) 332 + .argument(async_graphql::dynamic::InputValue::new( 333 + "after", 334 + TypeRef::named(TypeRef::STRING), 335 + )) 336 + .argument(async_graphql::dynamic::InputValue::new( 337 + "last", 338 + TypeRef::named(TypeRef::INT), 339 + )) 340 + .argument(async_graphql::dynamic::InputValue::new( 341 + "before", 342 + TypeRef::named(TypeRef::STRING), 343 + )) 344 + .argument(async_graphql::dynamic::InputValue::new( 345 + "sortBy", 346 + TypeRef::named_list("SortField"), 347 + )) 348 + .argument(async_graphql::dynamic::InputValue::new( 349 + "where", 350 + TypeRef::named("JSON"), 351 + )) 352 + .description(format!("Query {} records", nsid)), 353 + ); 354 + } 355 + } 356 + 357 + // Build Mutation type 358 + let mutation = create_mutation_type(database.clone(), slice_uri.clone()); 359 + 360 + // Build and return the schema 361 + let mut schema_builder = Schema::build(query.type_name(), Some(mutation.type_name()), None) 362 + .register(query) 363 + .register(mutation); 364 + 365 + // Register JSON scalar type for complex fields 366 + let json_scalar = Scalar::new("JSON"); 367 + schema_builder = schema_builder.register(json_scalar); 368 + 369 + // Register Blob type 370 + let blob_type = create_blob_type(); 371 + schema_builder = schema_builder.register(blob_type); 372 + 373 + // Register SyncResult type for mutations 374 + let sync_result_type = create_sync_result_type(); 375 + schema_builder = schema_builder.register(sync_result_type); 376 + 377 + // Register SortDirection enum 378 + let sort_direction_enum = create_sort_direction_enum(); 379 + schema_builder = schema_builder.register(sort_direction_enum); 380 + 381 + // Register SortField input type 382 + let sort_field_input = create_sort_field_input(); 383 + schema_builder = schema_builder.register(sort_field_input); 384 + 385 + // Register condition input types for where clauses 386 + let string_condition_input = create_string_condition_input(); 387 + schema_builder = schema_builder.register(string_condition_input); 388 + 389 + let int_condition_input = create_int_condition_input(); 390 + schema_builder = schema_builder.register(int_condition_input); 391 + 392 + // Register PageInfo type 393 + let page_info_type = create_page_info_type(); 394 + schema_builder = schema_builder.register(page_info_type); 395 + 396 + // Register all object types 397 + for obj in objects_to_register { 398 + schema_builder = schema_builder.register(obj); 399 + } 400 + 401 + schema_builder 402 + .finish() 403 + .map_err(|e| format!("Schema build error: {:?}", e)) 404 + } 405 + 406 + /// Container to hold record data for resolvers 407 + #[derive(Clone)] 408 + struct RecordContainer { 409 + record: crate::models::IndexedRecord, 410 + } 411 + 412 + /// Container to hold blob data and DID for URL generation 413 + #[derive(Clone)] 414 + struct BlobContainer { 415 + blob_ref: String, // CID reference 416 + mime_type: String, // MIME type 417 + size: i64, // Size in bytes 418 + did: String, // DID for CDN URL generation 419 + } 420 + 421 + /// Creates a GraphQL Object type for a record collection 422 + fn create_record_type( 423 + type_name: &str, 424 + fields: &[GraphQLField], 425 + database: Database, 426 + slice_uri: String, 427 + all_collections: &[CollectionMeta], 428 + ) -> Object { 429 + let mut object = Object::new(type_name); 430 + 431 + // Check which field names exist in lexicon to avoid conflicts 432 + let lexicon_field_names: std::collections::HashSet<&str> = 433 + fields.iter().map(|f| f.name.as_str()).collect(); 434 + 435 + // Add standard AT Protocol fields only if they don't conflict with lexicon fields 436 + if !lexicon_field_names.contains("uri") { 437 + object = object.field(Field::new("uri", TypeRef::named_nn(TypeRef::STRING), |ctx| { 438 + FieldFuture::new(async move { 439 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 440 + Ok(Some(GraphQLValue::from(container.record.uri.clone()))) 441 + }) 442 + })); 443 + } 444 + 445 + if !lexicon_field_names.contains("cid") { 446 + object = object.field(Field::new("cid", TypeRef::named_nn(TypeRef::STRING), |ctx| { 447 + FieldFuture::new(async move { 448 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 449 + Ok(Some(GraphQLValue::from(container.record.cid.clone()))) 450 + }) 451 + })); 452 + } 453 + 454 + if !lexicon_field_names.contains("did") { 455 + object = object.field(Field::new("did", TypeRef::named_nn(TypeRef::STRING), |ctx| { 456 + FieldFuture::new(async move { 457 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 458 + Ok(Some(GraphQLValue::from(container.record.did.clone()))) 459 + }) 460 + })); 461 + } 462 + 463 + if !lexicon_field_names.contains("indexedAt") { 464 + object = object.field(Field::new( 465 + "indexedAt", 466 + TypeRef::named_nn(TypeRef::STRING), 467 + |ctx| { 468 + FieldFuture::new(async move { 469 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 470 + Ok(Some(GraphQLValue::from( 471 + container.record.indexed_at.clone(), 472 + ))) 473 + }) 474 + }, 475 + )); 476 + } 477 + 478 + // Add actor metadata field (handle from actors table) 479 + // Always add as "actorHandle" to avoid conflicts with lexicon fields 480 + let db_for_actor = database.clone(); 481 + let slice_for_actor = slice_uri.clone(); 482 + object = object.field(Field::new( 483 + "actorHandle", 484 + TypeRef::named(TypeRef::STRING), 485 + move |ctx| { 486 + let db = db_for_actor.clone(); 487 + let slice = slice_for_actor.clone(); 488 + FieldFuture::new(async move { 489 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 490 + let did = &container.record.did; 491 + 492 + // Build where clause to find actor by DID 493 + let mut where_clause = crate::models::WhereClause { 494 + conditions: std::collections::HashMap::new(), 495 + or_conditions: None, 496 + }; 497 + where_clause.conditions.insert( 498 + "did".to_string(), 499 + crate::models::WhereCondition { 500 + eq: Some(serde_json::Value::String(did.clone())), 501 + in_values: None, 502 + contains: None, 503 + }, 504 + ); 505 + 506 + match db.get_slice_actors(&slice, Some(1), None, Some(&where_clause)).await { 507 + Ok((actors, _cursor)) => { 508 + if let Some(actor) = actors.first() { 509 + if let Some(handle) = &actor.handle { 510 + Ok(Some(GraphQLValue::from(handle.clone()))) 511 + } else { 512 + Ok(None) 513 + } 514 + } else { 515 + Ok(None) 516 + } 517 + } 518 + Err(e) => { 519 + tracing::debug!("Actor not found for {}: {}", did, e); 520 + Ok(None) 521 + } 522 + } 523 + }) 524 + }, 525 + )); 526 + 527 + // Add fields from lexicon 528 + for field in fields { 529 + let field_name = field.name.clone(); 530 + let field_name_for_field = field_name.clone(); // Need separate clone for Field::new 531 + let field_type = field.field_type.clone(); 532 + let db_clone = database.clone(); 533 + 534 + let type_ref = graphql_type_to_typeref(&field.field_type, field.is_required); 535 + 536 + object = object.field(Field::new(&field_name_for_field, type_ref, move |ctx| { 537 + let field_name = field_name.clone(); 538 + let field_type = field_type.clone(); 539 + let db = db_clone.clone(); 540 + 541 + FieldFuture::new(async move { 542 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 543 + let value = container.record.value.get(&field_name); 544 + 545 + if let Some(val) = value { 546 + // Check for explicit null value 547 + if val.is_null() { 548 + return Ok(Some(FieldValue::NULL)); 549 + } 550 + 551 + // Check if this is a blob field 552 + if matches!(field_type, GraphQLType::Blob) { 553 + // Extract blob fields from JSON object 554 + if let Some(obj) = val.as_object() { 555 + let blob_ref = obj 556 + .get("ref") 557 + .and_then(|r| r.as_object()) 558 + .and_then(|r| r.get("$link")) 559 + .and_then(|l| l.as_str()) 560 + .unwrap_or("") 561 + .to_string(); 562 + 563 + let mime_type = obj 564 + .get("mimeType") 565 + .and_then(|m| m.as_str()) 566 + .unwrap_or("image/jpeg") 567 + .to_string(); 568 + 569 + let size = obj 570 + .get("size") 571 + .and_then(|s| s.as_i64()) 572 + .unwrap_or(0); 573 + 574 + let blob_container = BlobContainer { 575 + blob_ref, 576 + mime_type, 577 + size, 578 + did: container.record.did.clone(), 579 + }; 580 + 581 + return Ok(Some(FieldValue::owned_any(blob_container))); 582 + } 583 + 584 + // If not a proper blob object, return NULL 585 + return Ok(Some(FieldValue::NULL)); 586 + } 587 + 588 + // Check if this is a reference field that needs joining 589 + if matches!(field_type, GraphQLType::Ref) { 590 + // Extract URI from strongRef and fetch the linked record 591 + if let Some(uri) = 592 + crate::graphql::dataloaders::extract_uri_from_strong_ref(val) 593 + { 594 + match db.get_record(&uri).await { 595 + Ok(Some(linked_record)) => { 596 + // Convert the linked record to a JSON value 597 + let record_json = serde_json::to_value(linked_record) 598 + .map_err(|e| { 599 + Error::new(format!("Serialization error: {}", e)) 600 + })?; 601 + 602 + // Convert serde_json::Value to async_graphql::Value 603 + let graphql_val = json_to_graphql_value(&record_json); 604 + return Ok(Some(FieldValue::value(graphql_val))); 605 + } 606 + Ok(None) => { 607 + return Ok(Some(FieldValue::NULL)); 608 + } 609 + Err(e) => { 610 + tracing::error!("Error fetching linked record: {}", e); 611 + return Ok(Some(FieldValue::NULL)); 612 + } 613 + } 614 + } 615 + } 616 + 617 + // For non-ref fields, return the raw JSON value 618 + let graphql_val = json_to_graphql_value(val); 619 + Ok(Some(FieldValue::value(graphql_val))) 620 + } else { 621 + Ok(Some(FieldValue::NULL)) 622 + } 623 + }) 624 + })); 625 + } 626 + 627 + // Add join fields for cross-referencing other collections by DID 628 + for collection in all_collections { 629 + let field_name = nsid_to_join_field_name(&collection.nsid); 630 + 631 + // Skip if this would conflict with existing field 632 + if lexicon_field_names.contains(field_name.as_str()) { 633 + continue; 634 + } 635 + 636 + let collection_nsid = collection.nsid.clone(); 637 + let key_type = collection.key_type.clone(); 638 + let db_for_join = database.clone(); 639 + let slice_for_join = slice_uri.clone(); 640 + 641 + // Determine type and resolver based on key_type 642 + match key_type.as_str() { 643 + "literal:self" => { 644 + // Single record per DID - return nullable object of the collection's type 645 + object = object.field(Field::new( 646 + &field_name, 647 + TypeRef::named(&collection.type_name), 648 + move |ctx| { 649 + let db = db_for_join.clone(); 650 + let nsid = collection_nsid.clone(); 651 + FieldFuture::new(async move { 652 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 653 + let uri = format!("at://{}/{}/self", container.record.did, nsid); 654 + 655 + match db.get_record(&uri).await { 656 + Ok(Some(record)) => { 657 + let new_container = RecordContainer { 658 + record, 659 + }; 660 + Ok(Some(FieldValue::owned_any(new_container))) 661 + } 662 + Ok(None) => Ok(None), 663 + Err(e) => { 664 + tracing::debug!("Record not found for {}: {}", uri, e); 665 + Ok(None) 666 + } 667 + } 668 + }) 669 + }, 670 + )); 671 + } 672 + "tid" | "any" => { 673 + // Multiple records per DID - return array of the collection's type 674 + object = object.field( 675 + Field::new( 676 + &field_name, 677 + TypeRef::named_nn_list_nn(&collection.type_name), 678 + move |ctx| { 679 + let db = db_for_join.clone(); 680 + let nsid = collection_nsid.clone(); 681 + let slice = slice_for_join.clone(); 682 + FieldFuture::new(async move { 683 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 684 + let did = &container.record.did; 685 + 686 + // Get limit from argument, default to 50 687 + let limit = ctx.args.get("limit") 688 + .and_then(|v| v.i64().ok()) 689 + .map(|i| i as i32) 690 + .unwrap_or(50) 691 + .min(100); // Cap at 100 to prevent abuse 692 + 693 + // Build where clause to find all records of this collection for this DID 694 + let mut where_clause = crate::models::WhereClause { 695 + conditions: HashMap::new(), 696 + or_conditions: None, 697 + }; 698 + where_clause.conditions.insert( 699 + "collection".to_string(), 700 + crate::models::WhereCondition { 701 + eq: Some(serde_json::Value::String(nsid.clone())), 702 + in_values: None, 703 + contains: None, 704 + }, 705 + ); 706 + where_clause.conditions.insert( 707 + "did".to_string(), 708 + crate::models::WhereCondition { 709 + eq: Some(serde_json::Value::String(did.clone())), 710 + in_values: None, 711 + contains: None, 712 + }, 713 + ); 714 + 715 + match db.get_slice_collections_records( 716 + &slice, 717 + Some(limit), 718 + None, // cursor 719 + None, // sort 720 + Some(&where_clause), 721 + ).await { 722 + Ok((records, _cursor)) => { 723 + let values: Vec<FieldValue> = records 724 + .into_iter() 725 + .map(|record| { 726 + // Convert Record to IndexedRecord 727 + let indexed_record = crate::models::IndexedRecord { 728 + uri: record.uri, 729 + cid: record.cid, 730 + did: record.did, 731 + collection: record.collection, 732 + value: record.json, 733 + indexed_at: record.indexed_at.to_rfc3339(), 734 + }; 735 + let container = RecordContainer { 736 + record: indexed_record, 737 + }; 738 + FieldValue::owned_any(container) 739 + }) 740 + .collect(); 741 + Ok(Some(FieldValue::list(values))) 742 + } 743 + Err(e) => { 744 + tracing::debug!("Error querying {}: {}", nsid, e); 745 + Ok(Some(FieldValue::list(Vec::<FieldValue>::new()))) 746 + } 747 + } 748 + }) 749 + }, 750 + ) 751 + .argument(async_graphql::dynamic::InputValue::new( 752 + "limit", 753 + TypeRef::named(TypeRef::INT), 754 + )) 755 + ); 756 + } 757 + _ => { 758 + // Unknown key type, skip 759 + continue; 760 + } 761 + } 762 + } 763 + 764 + // Add reverse joins: for every other collection, add a field to query records by DID 765 + // This enables bidirectional traversal (e.g., profile.plays and play.profile) 766 + for collection in all_collections { 767 + let reverse_field_name = format!("{}s", nsid_to_join_field_name(&collection.nsid)); 768 + let db_for_reverse = database.clone(); 769 + let slice_for_reverse = slice_uri.clone(); 770 + let collection_nsid = collection.nsid.clone(); 771 + let collection_type = collection.type_name.clone(); 772 + 773 + object = object.field( 774 + Field::new( 775 + &reverse_field_name, 776 + TypeRef::named_nn_list_nn(&collection_type), 777 + move |ctx| { 778 + let db = db_for_reverse.clone(); 779 + let slice = slice_for_reverse.clone(); 780 + let nsid = collection_nsid.clone(); 781 + FieldFuture::new(async move { 782 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 783 + let did = &container.record.did; 784 + 785 + // Get limit from argument, default to 50 786 + let limit = ctx.args.get("limit") 787 + .and_then(|v| v.i64().ok()) 788 + .map(|i| i as i32) 789 + .unwrap_or(50) 790 + .min(100); // Cap at 100 to prevent abuse 791 + 792 + // Build where clause to find all records of this collection for this DID 793 + let mut where_clause = crate::models::WhereClause { 794 + conditions: HashMap::new(), 795 + or_conditions: None, 796 + }; 797 + where_clause.conditions.insert( 798 + "collection".to_string(), 799 + crate::models::WhereCondition { 800 + eq: Some(serde_json::Value::String(nsid.clone())), 801 + in_values: None, 802 + contains: None, 803 + }, 804 + ); 805 + where_clause.conditions.insert( 806 + "did".to_string(), 807 + crate::models::WhereCondition { 808 + eq: Some(serde_json::Value::String(did.clone())), 809 + in_values: None, 810 + contains: None, 811 + }, 812 + ); 813 + 814 + match db.get_slice_collections_records( 815 + &slice, 816 + Some(limit), 817 + None, // cursor 818 + None, // sort 819 + Some(&where_clause), 820 + ).await { 821 + Ok((records, _cursor)) => { 822 + let values: Vec<FieldValue> = records 823 + .into_iter() 824 + .map(|record| { 825 + // Convert Record to IndexedRecord 826 + let indexed_record = crate::models::IndexedRecord { 827 + uri: record.uri, 828 + cid: record.cid, 829 + did: record.did, 830 + collection: record.collection, 831 + value: record.json, 832 + indexed_at: record.indexed_at.to_rfc3339(), 833 + }; 834 + let container = RecordContainer { 835 + record: indexed_record, 836 + }; 837 + FieldValue::owned_any(container) 838 + }) 839 + .collect(); 840 + Ok(Some(FieldValue::list(values))) 841 + } 842 + Err(e) => { 843 + tracing::debug!("Error querying {}: {}", nsid, e); 844 + Ok(Some(FieldValue::list(Vec::<FieldValue>::new()))) 845 + } 846 + } 847 + }) 848 + }, 849 + ) 850 + .argument(async_graphql::dynamic::InputValue::new( 851 + "limit", 852 + TypeRef::named(TypeRef::INT), 853 + )) 854 + ); 855 + } 856 + 857 + object 858 + } 859 + 860 + /// Convert serde_json::Value to async_graphql::Value 861 + fn json_to_graphql_value(val: &serde_json::Value) -> GraphQLValue { 862 + match val { 863 + serde_json::Value::Null => GraphQLValue::Null, 864 + serde_json::Value::Bool(b) => GraphQLValue::Boolean(*b), 865 + serde_json::Value::Number(n) => { 866 + if let Some(i) = n.as_i64() { 867 + GraphQLValue::Number((i as i32).into()) 868 + } else if let Some(f) = n.as_f64() { 869 + GraphQLValue::Number(serde_json::Number::from_f64(f).unwrap().into()) 870 + } else { 871 + GraphQLValue::Null 872 + } 873 + } 874 + serde_json::Value::String(s) => GraphQLValue::String(s.clone()), 875 + serde_json::Value::Array(arr) => { 876 + GraphQLValue::List(arr.iter().map(json_to_graphql_value).collect()) 877 + } 878 + serde_json::Value::Object(obj) => { 879 + let mut map = async_graphql::indexmap::IndexMap::new(); 880 + for (k, v) in obj { 881 + map.insert( 882 + async_graphql::Name::new(k.as_str()), 883 + json_to_graphql_value(v), 884 + ); 885 + } 886 + GraphQLValue::Object(map) 887 + } 888 + } 889 + } 890 + 891 + /// Converts GraphQL type to TypeRef for async-graphql 892 + fn graphql_type_to_typeref(gql_type: &GraphQLType, is_required: bool) -> TypeRef { 893 + match gql_type { 894 + GraphQLType::String => { 895 + if is_required { 896 + TypeRef::named_nn(TypeRef::STRING) 897 + } else { 898 + TypeRef::named(TypeRef::STRING) 899 + } 900 + } 901 + GraphQLType::Int => { 902 + if is_required { 903 + TypeRef::named_nn(TypeRef::INT) 904 + } else { 905 + TypeRef::named(TypeRef::INT) 906 + } 907 + } 908 + GraphQLType::Boolean => { 909 + if is_required { 910 + TypeRef::named_nn(TypeRef::BOOLEAN) 911 + } else { 912 + TypeRef::named(TypeRef::BOOLEAN) 913 + } 914 + } 915 + GraphQLType::Float => { 916 + if is_required { 917 + TypeRef::named_nn(TypeRef::FLOAT) 918 + } else { 919 + TypeRef::named(TypeRef::FLOAT) 920 + } 921 + } 922 + GraphQLType::Blob => { 923 + // Blob object type with url resolver 924 + if is_required { 925 + TypeRef::named_nn("Blob") 926 + } else { 927 + TypeRef::named("Blob") 928 + } 929 + } 930 + GraphQLType::Json | GraphQLType::Ref | GraphQLType::Object(_) | GraphQLType::Union => { 931 + // JSON scalar type - linked records and complex objects return as JSON 932 + if is_required { 933 + TypeRef::named_nn("JSON") 934 + } else { 935 + TypeRef::named("JSON") 936 + } 937 + } 938 + GraphQLType::Array(inner) => { 939 + // For arrays of primitives, use typed arrays 940 + // For arrays of complex types, use JSON scalar 941 + match inner.as_ref() { 942 + GraphQLType::String | GraphQLType::Int | GraphQLType::Boolean | GraphQLType::Float => { 943 + let inner_ref = match inner.as_ref() { 944 + GraphQLType::String => TypeRef::STRING, 945 + GraphQLType::Int => TypeRef::INT, 946 + GraphQLType::Boolean => TypeRef::BOOLEAN, 947 + GraphQLType::Float => TypeRef::FLOAT, 948 + _ => unreachable!(), 949 + }; 950 + 951 + if is_required { 952 + TypeRef::named_nn_list_nn(inner_ref) 953 + } else { 954 + TypeRef::named_list(inner_ref) 955 + } 956 + } 957 + _ => { 958 + // Arrays of complex types (objects, etc.) are just JSON 959 + if is_required { 960 + TypeRef::named_nn("JSON") 961 + } else { 962 + TypeRef::named("JSON") 963 + } 964 + } 965 + } 966 + } 967 + } 968 + } 969 + 970 + /// Creates the Blob GraphQL type with url resolver 971 + fn create_blob_type() -> Object { 972 + let mut blob = Object::new("Blob"); 973 + 974 + // ref field - CID reference 975 + blob = blob.field(Field::new("ref", TypeRef::named_nn(TypeRef::STRING), |ctx| { 976 + FieldFuture::new(async move { 977 + let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?; 978 + Ok(Some(GraphQLValue::from(container.blob_ref.clone()))) 979 + }) 980 + })); 981 + 982 + // mimeType field 983 + blob = blob.field(Field::new("mimeType", TypeRef::named_nn(TypeRef::STRING), |ctx| { 984 + FieldFuture::new(async move { 985 + let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?; 986 + Ok(Some(GraphQLValue::from(container.mime_type.clone()))) 987 + }) 988 + })); 989 + 990 + // size field 991 + blob = blob.field(Field::new("size", TypeRef::named_nn(TypeRef::INT), |ctx| { 992 + FieldFuture::new(async move { 993 + let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?; 994 + Ok(Some(GraphQLValue::from(container.size as i32))) 995 + }) 996 + })); 997 + 998 + // url(preset) field with argument 999 + blob = blob.field( 1000 + Field::new("url", TypeRef::named_nn(TypeRef::STRING), |ctx| { 1001 + FieldFuture::new(async move { 1002 + let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?; 1003 + 1004 + // Get preset argument, default to "feed_fullsize" 1005 + let preset: String = match ctx.args.get("preset") { 1006 + Some(val) => val.string().unwrap_or("feed_fullsize").to_string(), 1007 + None => "feed_fullsize".to_string(), 1008 + }; 1009 + 1010 + // Build CDN URL: https://cdn.bsky.app/img/{preset}/plain/{did}/{cid}@jpeg 1011 + let cdn_base_url = "https://cdn.bsky.app/img"; 1012 + let url = format!( 1013 + "{}/{}/plain/{}/{}@jpeg", 1014 + cdn_base_url, 1015 + preset, 1016 + container.did, 1017 + container.blob_ref 1018 + ); 1019 + 1020 + Ok(Some(GraphQLValue::from(url))) 1021 + }) 1022 + }) 1023 + .argument(async_graphql::dynamic::InputValue::new( 1024 + "preset", 1025 + TypeRef::named(TypeRef::STRING), 1026 + )) 1027 + .description("Generate CDN URL for the blob with the specified preset (avatar, banner, feed_thumbnail, feed_fullsize)"), 1028 + ); 1029 + 1030 + blob 1031 + } 1032 + 1033 + /// Creates the SyncResult GraphQL type for mutation responses 1034 + fn create_sync_result_type() -> Object { 1035 + let mut sync_result = Object::new("SyncResult"); 1036 + 1037 + sync_result = sync_result.field(Field::new("success", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { 1038 + FieldFuture::new(async move { 1039 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1040 + .ok_or_else(|| Error::new("Failed to downcast sync result"))?; 1041 + if let GraphQLValue::Object(obj) = value { 1042 + if let Some(success) = obj.get("success") { 1043 + return Ok(Some(success.clone())); 1044 + } 1045 + } 1046 + Ok(None) 1047 + }) 1048 + })); 1049 + 1050 + sync_result = sync_result.field(Field::new("reposProcessed", TypeRef::named_nn(TypeRef::INT), |ctx| { 1051 + FieldFuture::new(async move { 1052 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1053 + .ok_or_else(|| Error::new("Failed to downcast sync result"))?; 1054 + if let GraphQLValue::Object(obj) = value { 1055 + if let Some(repos) = obj.get("reposProcessed") { 1056 + return Ok(Some(repos.clone())); 1057 + } 1058 + } 1059 + Ok(None) 1060 + }) 1061 + })); 1062 + 1063 + sync_result = sync_result.field(Field::new("recordsSynced", TypeRef::named_nn(TypeRef::INT), |ctx| { 1064 + FieldFuture::new(async move { 1065 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1066 + .ok_or_else(|| Error::new("Failed to downcast sync result"))?; 1067 + if let GraphQLValue::Object(obj) = value { 1068 + if let Some(records) = obj.get("recordsSynced") { 1069 + return Ok(Some(records.clone())); 1070 + } 1071 + } 1072 + Ok(None) 1073 + }) 1074 + })); 1075 + 1076 + sync_result = sync_result.field(Field::new("timedOut", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { 1077 + FieldFuture::new(async move { 1078 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1079 + .ok_or_else(|| Error::new("Failed to downcast sync result"))?; 1080 + if let GraphQLValue::Object(obj) = value { 1081 + if let Some(timed_out) = obj.get("timedOut") { 1082 + return Ok(Some(timed_out.clone())); 1083 + } 1084 + } 1085 + Ok(None) 1086 + }) 1087 + })); 1088 + 1089 + sync_result = sync_result.field(Field::new("message", TypeRef::named_nn(TypeRef::STRING), |ctx| { 1090 + FieldFuture::new(async move { 1091 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1092 + .ok_or_else(|| Error::new("Failed to downcast sync result"))?; 1093 + if let GraphQLValue::Object(obj) = value { 1094 + if let Some(message) = obj.get("message") { 1095 + return Ok(Some(message.clone())); 1096 + } 1097 + } 1098 + Ok(None) 1099 + }) 1100 + })); 1101 + 1102 + sync_result 1103 + } 1104 + 1105 + /// Creates the SortDirection enum type 1106 + fn create_sort_direction_enum() -> Enum { 1107 + Enum::new("SortDirection") 1108 + .item(EnumItem::new("asc")) 1109 + .item(EnumItem::new("desc")) 1110 + } 1111 + 1112 + /// Creates the SortField input type 1113 + fn create_sort_field_input() -> InputObject { 1114 + InputObject::new("SortField") 1115 + .field(InputValue::new("field", TypeRef::named_nn(TypeRef::STRING))) 1116 + .field(InputValue::new( 1117 + "direction", 1118 + TypeRef::named_nn("SortDirection"), 1119 + )) 1120 + } 1121 + 1122 + /// Creates the StringCondition input type for string field filtering 1123 + fn create_string_condition_input() -> InputObject { 1124 + InputObject::new("StringCondition") 1125 + .field(InputValue::new("eq", TypeRef::named(TypeRef::STRING))) 1126 + .field(InputValue::new("in", TypeRef::named_list(TypeRef::STRING))) 1127 + .field(InputValue::new("contains", TypeRef::named(TypeRef::STRING))) 1128 + } 1129 + 1130 + /// Creates the IntCondition input type for int field filtering 1131 + fn create_int_condition_input() -> InputObject { 1132 + InputObject::new("IntCondition") 1133 + .field(InputValue::new("eq", TypeRef::named(TypeRef::INT))) 1134 + .field(InputValue::new("in", TypeRef::named_list(TypeRef::INT))) 1135 + } 1136 + 1137 + /// Creates the PageInfo type for connection pagination 1138 + fn create_page_info_type() -> Object { 1139 + let mut page_info = Object::new("PageInfo"); 1140 + 1141 + page_info = page_info.field(Field::new("hasNextPage", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { 1142 + FieldFuture::new(async move { 1143 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1144 + .ok_or_else(|| Error::new("Failed to downcast PageInfo"))?; 1145 + if let GraphQLValue::Object(obj) = value { 1146 + if let Some(has_next) = obj.get("hasNextPage") { 1147 + return Ok(Some(has_next.clone())); 1148 + } 1149 + } 1150 + Ok(Some(GraphQLValue::from(false))) 1151 + }) 1152 + })); 1153 + 1154 + page_info = page_info.field(Field::new("hasPreviousPage", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { 1155 + FieldFuture::new(async move { 1156 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1157 + .ok_or_else(|| Error::new("Failed to downcast PageInfo"))?; 1158 + if let GraphQLValue::Object(obj) = value { 1159 + if let Some(has_prev) = obj.get("hasPreviousPage") { 1160 + return Ok(Some(has_prev.clone())); 1161 + } 1162 + } 1163 + Ok(Some(GraphQLValue::from(false))) 1164 + }) 1165 + })); 1166 + 1167 + page_info = page_info.field(Field::new("startCursor", TypeRef::named(TypeRef::STRING), |ctx| { 1168 + FieldFuture::new(async move { 1169 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1170 + .ok_or_else(|| Error::new("Failed to downcast PageInfo"))?; 1171 + if let GraphQLValue::Object(obj) = value { 1172 + if let Some(cursor) = obj.get("startCursor") { 1173 + return Ok(Some(cursor.clone())); 1174 + } 1175 + } 1176 + Ok(None) 1177 + }) 1178 + })); 1179 + 1180 + page_info = page_info.field(Field::new("endCursor", TypeRef::named(TypeRef::STRING), |ctx| { 1181 + FieldFuture::new(async move { 1182 + let value = ctx.parent_value.downcast_ref::<GraphQLValue>() 1183 + .ok_or_else(|| Error::new("Failed to downcast PageInfo"))?; 1184 + if let GraphQLValue::Object(obj) = value { 1185 + if let Some(cursor) = obj.get("endCursor") { 1186 + return Ok(Some(cursor.clone())); 1187 + } 1188 + } 1189 + Ok(None) 1190 + }) 1191 + })); 1192 + 1193 + page_info 1194 + } 1195 + 1196 + /// Connection data structure that holds all connection fields 1197 + #[derive(Clone)] 1198 + struct ConnectionData { 1199 + total_count: i32, 1200 + has_next_page: bool, 1201 + end_cursor: Option<String>, 1202 + nodes: Vec<RecordContainer>, 1203 + } 1204 + 1205 + /// Edge data structure for Relay connections 1206 + #[derive(Clone)] 1207 + struct EdgeData { 1208 + node: RecordContainer, 1209 + cursor: String, 1210 + } 1211 + 1212 + /// Creates an Edge type for a given record type 1213 + /// Example: For "Post" creates "PostEdge" with node and cursor 1214 + fn create_edge_type(record_type_name: &str) -> Object { 1215 + let edge_name = format!("{}Edge", record_type_name); 1216 + let mut edge = Object::new(&edge_name); 1217 + 1218 + // Add node field 1219 + let record_type = record_type_name.to_string(); 1220 + edge = edge.field(Field::new("node", TypeRef::named_nn(&record_type), |ctx| { 1221 + FieldFuture::new(async move { 1222 + let edge_data = ctx.parent_value.try_downcast_ref::<EdgeData>()?; 1223 + Ok(Some(FieldValue::owned_any(edge_data.node.clone()))) 1224 + }) 1225 + })); 1226 + 1227 + // Add cursor field 1228 + edge = edge.field(Field::new("cursor", TypeRef::named_nn(TypeRef::STRING), |ctx| { 1229 + FieldFuture::new(async move { 1230 + let edge_data = ctx.parent_value.try_downcast_ref::<EdgeData>()?; 1231 + Ok(Some(GraphQLValue::from(edge_data.cursor.clone()))) 1232 + }) 1233 + })); 1234 + 1235 + edge 1236 + } 1237 + 1238 + /// Creates a Connection type for a given record type 1239 + /// Example: For "Post" creates "PostConnection" with edges, pageInfo, and totalCount 1240 + fn create_connection_type(record_type_name: &str) -> Object { 1241 + let connection_name = format!("{}Connection", record_type_name); 1242 + let mut connection = Object::new(&connection_name); 1243 + 1244 + // Add totalCount field 1245 + connection = connection.field(Field::new("totalCount", TypeRef::named_nn(TypeRef::INT), |ctx| { 1246 + FieldFuture::new(async move { 1247 + let data = ctx.parent_value.try_downcast_ref::<ConnectionData>()?; 1248 + Ok(Some(GraphQLValue::from(data.total_count))) 1249 + }) 1250 + })); 1251 + 1252 + // Add pageInfo field 1253 + connection = connection.field(Field::new("pageInfo", TypeRef::named_nn("PageInfo"), |ctx| { 1254 + FieldFuture::new(async move { 1255 + let data = ctx.parent_value.try_downcast_ref::<ConnectionData>()?; 1256 + 1257 + let mut page_info = async_graphql::indexmap::IndexMap::new(); 1258 + page_info.insert( 1259 + async_graphql::Name::new("hasNextPage"), 1260 + GraphQLValue::from(data.has_next_page) 1261 + ); 1262 + // For forward pagination only, hasPreviousPage is always false 1263 + page_info.insert( 1264 + async_graphql::Name::new("hasPreviousPage"), 1265 + GraphQLValue::from(false) 1266 + ); 1267 + 1268 + // Add startCursor (first node's cid if available) 1269 + if !data.nodes.is_empty() { 1270 + if let Some(first_record) = data.nodes.first() { 1271 + let start_cursor = general_purpose::URL_SAFE_NO_PAD.encode(first_record.record.cid.clone()); 1272 + page_info.insert( 1273 + async_graphql::Name::new("startCursor"), 1274 + GraphQLValue::from(start_cursor) 1275 + ); 1276 + } 1277 + } 1278 + 1279 + // Add endCursor 1280 + if let Some(ref cursor) = data.end_cursor { 1281 + page_info.insert( 1282 + async_graphql::Name::new("endCursor"), 1283 + GraphQLValue::from(cursor.clone()) 1284 + ); 1285 + } 1286 + 1287 + Ok(Some(FieldValue::owned_any(GraphQLValue::Object(page_info)))) 1288 + }) 1289 + })); 1290 + 1291 + // Add edges field (Relay standard) 1292 + let edge_type = format!("{}Edge", record_type_name); 1293 + connection = connection.field(Field::new("edges", TypeRef::named_nn_list_nn(&edge_type), |ctx| { 1294 + FieldFuture::new(async move { 1295 + let data = ctx.parent_value.try_downcast_ref::<ConnectionData>()?; 1296 + 1297 + let field_values: Vec<FieldValue<'_>> = data.nodes.iter() 1298 + .map(|node| { 1299 + // Use base64-encoded CID as cursor 1300 + let cursor = general_purpose::URL_SAFE_NO_PAD.encode(node.record.cid.clone()); 1301 + let edge = EdgeData { 1302 + node: node.clone(), 1303 + cursor, 1304 + }; 1305 + FieldValue::owned_any(edge) 1306 + }) 1307 + .collect(); 1308 + 1309 + Ok(Some(FieldValue::list(field_values))) 1310 + }) 1311 + })); 1312 + 1313 + // Add nodes field (convenience, direct access to records without edges wrapper) 1314 + connection = connection.field(Field::new("nodes", TypeRef::named_nn_list_nn(record_type_name), |ctx| { 1315 + FieldFuture::new(async move { 1316 + let data = ctx.parent_value.try_downcast_ref::<ConnectionData>()?; 1317 + 1318 + let field_values: Vec<FieldValue<'_>> = data.nodes.iter() 1319 + .map(|node| FieldValue::owned_any(node.clone())) 1320 + .collect(); 1321 + 1322 + Ok(Some(FieldValue::list(field_values))) 1323 + }) 1324 + })); 1325 + 1326 + connection 1327 + } 1328 + 1329 + /// Creates the Mutation root type with sync operations 1330 + fn create_mutation_type(database: Database, slice_uri: String) -> Object { 1331 + let mut mutation = Object::new("Mutation"); 1332 + 1333 + // Add syncUserCollections mutation 1334 + let db_clone = database.clone(); 1335 + let slice_clone = slice_uri.clone(); 1336 + 1337 + mutation = mutation.field( 1338 + Field::new( 1339 + "syncUserCollections", 1340 + TypeRef::named_nn("SyncResult"), 1341 + move |ctx| { 1342 + let db = db_clone.clone(); 1343 + let slice = slice_clone.clone(); 1344 + 1345 + FieldFuture::new(async move { 1346 + let did = ctx.args.get("did") 1347 + .and_then(|v| v.string().ok()) 1348 + .ok_or_else(|| Error::new("did argument is required"))?; 1349 + 1350 + // Create sync service and call sync_user_collections 1351 + let cache_backend = crate::cache::CacheFactory::create_cache( 1352 + crate::cache::CacheBackend::InMemory { ttl_seconds: None } 1353 + ).await.map_err(|e| Error::new(format!("Failed to create cache: {}", e)))?; 1354 + let cache = Arc::new(Mutex::new(crate::cache::SliceCache::new(cache_backend))); 1355 + let sync_service = crate::sync::SyncService::with_cache( 1356 + db.clone(), 1357 + std::env::var("RELAY_ENDPOINT") 1358 + .unwrap_or_else(|_| "https://relay1.us-west.bsky.network".to_string()), 1359 + cache, 1360 + ); 1361 + 1362 + let result = sync_service 1363 + .sync_user_collections(did, &slice, 30) // 30 second timeout 1364 + .await 1365 + .map_err(|e| Error::new(format!("Sync failed: {}", e)))?; 1366 + 1367 + // Convert result to GraphQL object 1368 + let mut obj = async_graphql::indexmap::IndexMap::new(); 1369 + obj.insert(async_graphql::Name::new("success"), GraphQLValue::from(result.success)); 1370 + obj.insert(async_graphql::Name::new("reposProcessed"), GraphQLValue::from(result.repos_processed)); 1371 + obj.insert(async_graphql::Name::new("recordsSynced"), GraphQLValue::from(result.records_synced)); 1372 + obj.insert(async_graphql::Name::new("timedOut"), GraphQLValue::from(result.timed_out)); 1373 + obj.insert(async_graphql::Name::new("message"), GraphQLValue::from(result.message)); 1374 + 1375 + Ok(Some(FieldValue::owned_any(GraphQLValue::Object(obj)))) 1376 + }) 1377 + }, 1378 + ) 1379 + .argument(async_graphql::dynamic::InputValue::new( 1380 + "did", 1381 + TypeRef::named_nn(TypeRef::STRING), 1382 + )) 1383 + .description("Sync user collections for a given DID") 1384 + ); 1385 + 1386 + mutation 1387 + } 1388 + 1389 + /// Converts NSID to GraphQL type name 1390 + /// Example: app.bsky.feed.post -> AppBskyFeedPost 1391 + fn nsid_to_type_name(nsid: &str) -> String { 1392 + nsid.split('.') 1393 + .map(|part| { 1394 + let mut chars = part.chars(); 1395 + match chars.next() { 1396 + None => String::new(), 1397 + Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(), 1398 + } 1399 + }) 1400 + .collect::<Vec<_>>() 1401 + .join("") 1402 + } 1403 + 1404 + /// Converts NSID to GraphQL query name in camelCase and pluralized 1405 + /// Example: app.bsky.feed.post -> appBskyFeedPosts 1406 + /// Example: fm.teal.alpha.feed.play -> fmTealAlphaFeedPlays 1407 + fn nsid_to_query_name(nsid: &str) -> String { 1408 + // First convert to camelCase like join fields 1409 + let camel_case = nsid_to_join_field_name(nsid); 1410 + 1411 + // Then pluralize the end 1412 + if camel_case.ends_with("s") || camel_case.ends_with("x") || camel_case.ends_with("ch") || camel_case.ends_with("sh") { 1413 + format!("{}es", camel_case) // status -> statuses, box -> boxes 1414 + } else if camel_case.ends_with("y") && camel_case.len() > 1 { 1415 + let chars: Vec<char> = camel_case.chars().collect(); 1416 + if chars.len() > 1 && !['a', 'e', 'i', 'o', 'u'].contains(&chars[chars.len() - 2]) { 1417 + format!("{}ies", &camel_case[..camel_case.len() - 1]) // party -> parties 1418 + } else { 1419 + format!("{}s", camel_case) // day -> days 1420 + } 1421 + } else { 1422 + format!("{}s", camel_case) // post -> posts 1423 + } 1424 + } 1425 + 1426 + /// Converts NSID to GraphQL join field name in camelCase 1427 + /// Example: app.bsky.actor.profile -> appBskyActorProfile 1428 + fn nsid_to_join_field_name(nsid: &str) -> String { 1429 + let parts: Vec<&str> = nsid.split('.').collect(); 1430 + if parts.is_empty() { 1431 + return nsid.to_string(); 1432 + } 1433 + 1434 + // First part is lowercase, rest are capitalized 1435 + let mut result = parts[0].to_string(); 1436 + for part in &parts[1..] { 1437 + let mut chars = part.chars(); 1438 + if let Some(first) = chars.next() { 1439 + result.push_str(&first.to_uppercase().collect::<String>()); 1440 + result.push_str(chars.as_str()); 1441 + } 1442 + } 1443 + 1444 + result 1445 + }
+166
api/src/graphql/types.rs
···
··· 1 + //! GraphQL type definitions and mappings from AT Protocol lexicons 2 + 3 + use serde_json::Value; 4 + 5 + /// Represents a mapped GraphQL field from a lexicon property 6 + #[derive(Debug, Clone)] 7 + pub struct GraphQLField { 8 + pub name: String, 9 + pub field_type: GraphQLType, 10 + pub is_required: bool, 11 + } 12 + 13 + /// GraphQL type representation mapped from lexicon types 14 + #[derive(Debug, Clone)] 15 + pub enum GraphQLType { 16 + String, 17 + Int, 18 + Boolean, 19 + Float, 20 + /// Reference to another record (for strongRef) 21 + Ref, 22 + /// Array of a type 23 + Array(Box<GraphQLType>), 24 + /// Object with nested fields 25 + Object(Vec<GraphQLField>), 26 + /// Union of multiple types 27 + Union, 28 + /// Blob reference with CDN URL support 29 + Blob, 30 + /// Any JSON value 31 + Json, 32 + } 33 + 34 + /// Maps AT Protocol lexicon type to GraphQL type 35 + pub fn map_lexicon_type_to_graphql( 36 + type_name: &str, 37 + lexicon_def: &Value, 38 + ) -> GraphQLType { 39 + match type_name { 40 + "string" => GraphQLType::String, 41 + "integer" => GraphQLType::Int, 42 + "boolean" => GraphQLType::Boolean, 43 + "number" => GraphQLType::Float, 44 + "bytes" => GraphQLType::String, // Base64 encoded 45 + "cid-link" => GraphQLType::String, 46 + "blob" => GraphQLType::Blob, 47 + "unknown" => GraphQLType::Json, 48 + "null" => GraphQLType::Json, 49 + "ref" => { 50 + // Check if this is a strongRef (link to another record) 51 + let ref_name = lexicon_def 52 + .get("ref") 53 + .and_then(|r| r.as_str()) 54 + .unwrap_or(""); 55 + 56 + if ref_name == "com.atproto.repo.strongRef" { 57 + GraphQLType::Ref 58 + } else { 59 + GraphQLType::Json 60 + } 61 + } 62 + "array" => { 63 + let items = lexicon_def.get("items"); 64 + let item_type = if let Some(items_obj) = items { 65 + let item_type_name = items_obj 66 + .get("type") 67 + .and_then(|t| t.as_str()) 68 + .unwrap_or("unknown"); 69 + map_lexicon_type_to_graphql(item_type_name, items_obj) 70 + } else { 71 + GraphQLType::Json 72 + }; 73 + GraphQLType::Array(Box::new(item_type)) 74 + } 75 + "object" => { 76 + let properties = lexicon_def 77 + .get("properties") 78 + .and_then(|p| p.as_object()); 79 + 80 + let required_fields: Vec<String> = lexicon_def 81 + .get("required") 82 + .and_then(|r| r.as_array()) 83 + .map(|arr| { 84 + arr.iter() 85 + .filter_map(|v| v.as_str().map(|s| s.to_string())) 86 + .collect() 87 + }) 88 + .unwrap_or_default(); 89 + 90 + if let Some(props) = properties { 91 + let fields = props 92 + .iter() 93 + .map(|(field_name, field_def)| { 94 + let field_type_name = field_def 95 + .get("type") 96 + .and_then(|t| t.as_str()) 97 + .unwrap_or("unknown"); 98 + 99 + GraphQLField { 100 + name: field_name.clone(), 101 + field_type: map_lexicon_type_to_graphql( 102 + field_type_name, 103 + field_def, 104 + ), 105 + is_required: required_fields.contains(&field_name.to_string()), 106 + } 107 + }) 108 + .collect(); 109 + 110 + GraphQLType::Object(fields) 111 + } else { 112 + GraphQLType::Json 113 + } 114 + } 115 + "union" => { 116 + GraphQLType::Union 117 + } 118 + _ => GraphQLType::Json, 119 + } 120 + } 121 + 122 + /// Extract collection schema from lexicon definitions 123 + pub fn extract_collection_fields( 124 + lexicon_defs: &Value, 125 + ) -> Vec<GraphQLField> { 126 + let main_def = lexicon_defs 127 + .get("main") 128 + .or_else(|| lexicon_defs.get("record")); 129 + 130 + if let Some(main) = main_def { 131 + let type_name = main 132 + .get("type") 133 + .and_then(|t| t.as_str()) 134 + .unwrap_or("object"); 135 + 136 + // For "record" type, the actual object definition is nested under "record" field 137 + let object_def = if type_name == "record" { 138 + main.get("record").unwrap_or(main) 139 + } else { 140 + main 141 + }; 142 + 143 + if type_name == "record" || type_name == "object" { 144 + let object_type_name = object_def 145 + .get("type") 146 + .and_then(|t| t.as_str()) 147 + .unwrap_or("object"); 148 + 149 + if let GraphQLType::Object(fields) = map_lexicon_type_to_graphql(object_type_name, object_def) { 150 + return fields; 151 + } 152 + } 153 + } 154 + 155 + vec![] 156 + } 157 + 158 + /// Extract the record key type from lexicon definitions 159 + /// Returns Some("tid"), Some("literal:self"), Some("any"), or None 160 + pub fn extract_record_key(lexicon_defs: &Value) -> Option<String> { 161 + lexicon_defs 162 + .get("main")? 163 + .get("key") 164 + .and_then(|k| k.as_str()) 165 + .map(|s| s.to_string()) 166 + }
+43 -15
api/src/jetstream.rs
··· 453 } else { 454 format!("Record updated in {}", commit.collection) 455 }; 456 - let operation = if is_insert { "insert" } else { "update" }; 457 Logger::global().log_jetstream_with_slice( 458 LogLevel::Info, 459 &message, 460 Some(serde_json::json!({ 461 - "operation": operation, 462 - "collection": commit.collection, 463 - "slice_uri": slice_uri, 464 "did": did, 465 "record_type": "primary" 466 })), 467 Some(&slice_uri), ··· 473 LogLevel::Error, 474 message, 475 Some(serde_json::json!({ 476 - "operation": "upsert", 477 - "collection": commit.collection, 478 - "slice_uri": slice_uri, 479 "did": did, 480 "error": e.to_string(), 481 "record_type": "primary" 482 })), ··· 517 } else { 518 format!("Record updated in {}", commit.collection) 519 }; 520 - let operation = if is_insert { "insert" } else { "update" }; 521 Logger::global().log_jetstream_with_slice( 522 LogLevel::Info, 523 &message, 524 Some(serde_json::json!({ 525 - "operation": operation, 526 - "collection": commit.collection, 527 - "slice_uri": slice_uri, 528 "did": did, 529 "record_type": "external" 530 })), 531 Some(&slice_uri), ··· 537 LogLevel::Error, 538 message, 539 Some(serde_json::json!({ 540 - "operation": "upsert", 541 - "collection": commit.collection, 542 - "slice_uri": slice_uri, 543 "did": did, 544 "error": e.to_string(), 545 "record_type": "external" 546 })), ··· 623 } 624 625 // Handle cascade deletion before deleting the record 626 - if let Err(e) = self.database.handle_cascade_deletion(&uri, &commit.collection).await { 627 warn!("Cascade deletion failed for {}: {}", uri, e); 628 } 629
··· 453 } else { 454 format!("Record updated in {}", commit.collection) 455 }; 456 Logger::global().log_jetstream_with_slice( 457 LogLevel::Info, 458 &message, 459 Some(serde_json::json!({ 460 "did": did, 461 + "kind": "commit", 462 + "commit": { 463 + "rev": commit.rev, 464 + "operation": commit.operation, 465 + "collection": commit.collection, 466 + "rkey": commit.rkey, 467 + "record": commit.record, 468 + "cid": commit.cid 469 + }, 470 + "indexed_operation": if is_insert { "insert" } else { "update" }, 471 "record_type": "primary" 472 })), 473 Some(&slice_uri), ··· 479 LogLevel::Error, 480 message, 481 Some(serde_json::json!({ 482 "did": did, 483 + "kind": "commit", 484 + "commit": { 485 + "rev": commit.rev, 486 + "operation": commit.operation, 487 + "collection": commit.collection, 488 + "rkey": commit.rkey, 489 + "record": commit.record, 490 + "cid": commit.cid 491 + }, 492 "error": e.to_string(), 493 "record_type": "primary" 494 })), ··· 529 } else { 530 format!("Record updated in {}", commit.collection) 531 }; 532 Logger::global().log_jetstream_with_slice( 533 LogLevel::Info, 534 &message, 535 Some(serde_json::json!({ 536 "did": did, 537 + "kind": "commit", 538 + "commit": { 539 + "rev": commit.rev, 540 + "operation": commit.operation, 541 + "collection": commit.collection, 542 + "rkey": commit.rkey, 543 + "record": commit.record, 544 + "cid": commit.cid 545 + }, 546 + "indexed_operation": if is_insert { "insert" } else { "update" }, 547 "record_type": "external" 548 })), 549 Some(&slice_uri), ··· 555 LogLevel::Error, 556 message, 557 Some(serde_json::json!({ 558 "did": did, 559 + "kind": "commit", 560 + "commit": { 561 + "rev": commit.rev, 562 + "operation": commit.operation, 563 + "collection": commit.collection, 564 + "rkey": commit.rkey, 565 + "record": commit.record, 566 + "cid": commit.cid 567 + }, 568 "error": e.to_string(), 569 "record_type": "external" 570 })), ··· 647 } 648 649 // Handle cascade deletion before deleting the record 650 + if let Err(e) = self 651 + .database 652 + .handle_cascade_deletion(&uri, &commit.collection) 653 + .await 654 + { 655 warn!("Cascade deletion failed for {}: {}", uri, e); 656 } 657
+2 -10
api/src/logging.rs
··· 460 let limit = limit.unwrap_or(100); 461 462 let rows = if let Some(slice_uri) = slice_filter { 463 - tracing::info!("Querying jetstream logs with slice filter: {}", slice_uri); 464 // Include both slice-specific logs and global connection logs for context 465 - let results = sqlx::query_as!( 466 LogEntry, 467 r#" 468 SELECT id, created_at, log_type, job_id, user_did, slice_uri, level, message, metadata ··· 476 limit 477 ) 478 .fetch_all(pool) 479 - .await?; 480 - 481 - tracing::info!( 482 - "Found {} jetstream logs for slice {}", 483 - results.len(), 484 - slice_uri 485 - ); 486 - results 487 } else { 488 // No filter provided, return all Jetstream logs across all slices 489 sqlx::query_as!(
··· 460 let limit = limit.unwrap_or(100); 461 462 let rows = if let Some(slice_uri) = slice_filter { 463 // Include both slice-specific logs and global connection logs for context 464 + sqlx::query_as!( 465 LogEntry, 466 r#" 467 SELECT id, created_at, log_type, job_id, user_did, slice_uri, level, message, metadata ··· 475 limit 476 ) 477 .fetch_all(pool) 478 + .await? 479 } else { 480 // No filter provided, return all Jetstream logs across all slices 481 sqlx::query_as!(
+8 -2
api/src/main.rs
··· 5 mod cache; 6 mod database; 7 mod errors; 8 mod jetstream; 9 mod jetstream_cursor; 10 mod jobs; ··· 389 "/xrpc/network.slices.slice.getSyncSummary", 390 get(xrpc::network::slices::slice::get_sync_summary::handler), 391 ) 392 // Dynamic collection-specific XRPC endpoints (wildcard routes must come last) 393 .route( 394 - "/xrpc/*method", 395 get(api::xrpc_dynamic::dynamic_xrpc_handler), 396 ) 397 .route( 398 - "/xrpc/*method", 399 post(api::xrpc_dynamic::dynamic_xrpc_post_handler), 400 ) 401 .layer(TraceLayer::new_for_http())
··· 5 mod cache; 6 mod database; 7 mod errors; 8 + mod graphql; 9 mod jetstream; 10 mod jetstream_cursor; 11 mod jobs; ··· 390 "/xrpc/network.slices.slice.getSyncSummary", 391 get(xrpc::network::slices::slice::get_sync_summary::handler), 392 ) 393 + // GraphQL endpoint 394 + .route( 395 + "/graphql", 396 + get(graphql::graphql_playground).post(graphql::graphql_handler), 397 + ) 398 // Dynamic collection-specific XRPC endpoints (wildcard routes must come last) 399 .route( 400 + "/xrpc/{*method}", 401 get(api::xrpc_dynamic::dynamic_xrpc_handler), 402 ) 403 .route( 404 + "/xrpc/{*method}", 405 post(api::xrpc_dynamic::dynamic_xrpc_post_handler), 406 ) 407 .layer(TraceLayer::new_for_http())
+1 -1
api/src/models.rs
··· 16 pub slice_uri: Option<String>, 17 } 18 19 - #[derive(Debug, Serialize, Deserialize)] 20 #[serde(rename_all = "camelCase")] 21 pub struct IndexedRecord { 22 pub uri: String,
··· 16 pub slice_uri: Option<String>, 17 } 18 19 + #[derive(Debug, Clone, Serialize, Deserialize)] 20 #[serde(rename_all = "camelCase")] 21 pub struct IndexedRecord { 22 pub uri: String,
+19 -4
docs/getting-started.md
··· 10 11 ```bash 12 # Install from JSR 13 - deno install -g jsr:@slices/cli --name slices 14 ``` 15 16 ### Create Your Project 17 ··· 364 365 ### Deno Deploy 366 367 - Create a free account at [deno.com/deploy](https://deno.com/deploy). Push your code to GitHub, then connect your repository through the Deno Deploy dashboard to deploy your app. 368 369 - For production use with Deno Deploy, switch from SQLite to Deno KV for OAuth and session storage: 370 371 ```typescript 372 import { DenoKVOAuthStorage } from "@slices/oauth"; ··· 391 }); 392 ``` 393 394 - Deno KV provides serverless-compatible storage that scales automatically with your deployment. 395 396 ## Manual Setup (Advanced) 397
··· 10 11 ```bash 12 # Install from JSR 13 + deno install -g -A jsr:@slices/cli --name slices 14 + ``` 15 + 16 + ### Login to Slices 17 + 18 + Before creating a project, authenticate with your AT Protocol account: 19 + 20 + ```bash 21 + slices login 22 ``` 23 + 24 + This will open a browser window where you can authorize the CLI with your AT 25 + Protocol handle. 26 27 ### Create Your Project 28 ··· 375 376 ### Deno Deploy 377 378 + Create a free account at [deno.com/deploy](https://deno.com/deploy). Push your 379 + code to GitHub, then connect your repository through the Deno Deploy dashboard 380 + to deploy your app. 381 382 + For production use with Deno Deploy, switch from SQLite to Deno KV for OAuth and 383 + session storage: 384 385 ```typescript 386 import { DenoKVOAuthStorage } from "@slices/oauth"; ··· 405 }); 406 ``` 407 408 + Deno KV provides serverless-compatible storage that scales automatically with 409 + your deployment. 410 411 ## Manual Setup (Advanced) 412
+12 -6
docs/intro.md
··· 37 38 ```bash 39 # Install the CLI globally 40 - deno install -g jsr:@slices/cli --name slices 41 42 # Initialize a new slice project 43 slices init my-app ··· 47 deno task dev 48 ``` 49 50 - The `slices init` command creates a full-stack Deno app with OAuth authentication, automatically creates your slice on the network, and generates a type-safe TypeScript SDK. 51 52 ## Simple Example 53 ··· 105 106 ## Key Features 107 108 - - **Schema Validation**: Define lexicons that enforce data structure and constraints 109 - - **Auto-generated APIs**: REST endpoints created automatically from your schemas 110 - **TypeScript SDKs**: Type-safe clients generated from your lexicons 111 - **Real-time Sync**: Automatic synchronization across the AT Protocol network 112 - - **Advanced Querying**: Filter, sort, and paginate records with a powerful query API 113 - **OAuth Built-in**: Authentication with any AT Protocol account 114 115 ## When to Use Slices 116 117 Slices is ideal for: 118 119 - - **Social Applications**: Build specialized communities, forums, or social features 120 - **Content Platforms**: Create blogs, documentation sites, or media libraries 121 - **SaaS Products**: Develop collaborative tools with structured data needs 122 - **Web APIs**: Design REST APIs with automatic validation and documentation
··· 37 38 ```bash 39 # Install the CLI globally 40 + deno install -g -A jsr:@slices/cli --name slices 41 42 # Initialize a new slice project 43 slices init my-app ··· 47 deno task dev 48 ``` 49 50 + The `slices init` command creates a full-stack Deno app with OAuth 51 + authentication, automatically creates your slice on the network, and generates a 52 + type-safe TypeScript SDK. 53 54 ## Simple Example 55 ··· 107 108 ## Key Features 109 110 + - **Schema Validation**: Define lexicons that enforce data structure and 111 + constraints 112 + - **Auto-generated APIs**: REST endpoints created automatically from your 113 + schemas 114 - **TypeScript SDKs**: Type-safe clients generated from your lexicons 115 - **Real-time Sync**: Automatic synchronization across the AT Protocol network 116 + - **Advanced Querying**: Filter, sort, and paginate records with a powerful 117 + query API 118 - **OAuth Built-in**: Authentication with any AT Protocol account 119 120 ## When to Use Slices 121 122 Slices is ideal for: 123 124 + - **Social Applications**: Build specialized communities, forums, or social 125 + features 126 - **Content Platforms**: Create blogs, documentation sites, or media libraries 127 - **SaaS Products**: Develop collaborative tools with structured data needs 128 - **Web APIs**: Design REST APIs with automatic validation and documentation
+12 -130
frontend/src/features/slices/jetstream/handlers.tsx
··· 1 import type { Route } from "@std/http/unstable-route"; 2 - import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 3 import { 4 requireSliceAccess, 5 withSliceAccess, ··· 7 import { getSliceClient } from "../../../utils/client.ts"; 8 import { publicClient } from "../../../config.ts"; 9 import { renderHTML } from "../../../utils/render.tsx"; 10 - import { Layout } from "../../../shared/fragments/Layout.tsx"; 11 import { extractSliceParams } from "../../../utils/slice-params.ts"; 12 import { JetstreamLogsPage } from "./templates/JetstreamLogsPage.tsx"; 13 - import { JetstreamLogs } from "./templates/fragments/JetstreamLogs.tsx"; 14 - import { JetstreamStatus } from "./templates/fragments/JetstreamStatus.tsx"; 15 - import { JetstreamStatusDisplay } from "./templates/fragments/JetstreamStatusDisplay.tsx"; 16 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts"; 17 - import { buildSliceUri } from "../../../utils/at-uri.ts"; 18 - 19 - async function handleJetstreamLogs( 20 - req: Request, 21 - params?: URLPatternResult 22 - ): Promise<Response> { 23 - const context = await withAuth(req); 24 - const authResponse = requireAuth(context); 25 - if (authResponse) return authResponse; 26 - 27 - const sliceId = params?.pathname.groups.id; 28 - if (!sliceId) { 29 - return renderHTML( 30 - <div className="p-8 text-center text-red-600">โŒ Invalid slice ID</div>, 31 - { status: 400 } 32 - ); 33 - } 34 - 35 - try { 36 - // Use the slice-specific client 37 - const sliceClient = getSliceClient(context, sliceId); 38 - 39 - // Build slice URI from the user's DID and sliceId 40 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 41 - 42 - // Get Jetstream logs 43 - const result = await sliceClient.network.slices.slice.getJetstreamLogs({ 44 - slice: sliceUri, 45 - limit: 100, 46 - }); 47 - 48 - const logs = result?.logs || []; 49 - 50 - // Sort logs in descending order (newest first) 51 - const sortedLogs = logs.sort( 52 - (a, b) => 53 - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() 54 - ); 55 - 56 - // Render the log content 57 - return renderHTML(<JetstreamLogs logs={sortedLogs} />); 58 - } catch (error) { 59 - console.error("Failed to get Jetstream logs:", error); 60 - const errorMessage = error instanceof Error ? error.message : String(error); 61 - return renderHTML( 62 - <Layout title="Error"> 63 - <div className="max-w-6xl mx-auto"> 64 - <div className="flex items-center gap-4 mb-6"> 65 - <a 66 - href={`/profile/${context.currentUser.handle}/slice/${sliceId}`} 67 - className="text-blue-600 hover:text-blue-800" 68 - > 69 - โ† Back to Slice 70 - </a> 71 - <h1 className="text-2xl font-semibold text-gray-900"> 72 - โœˆ๏ธ Jetstream Logs 73 - </h1> 74 - </div> 75 - <div className="p-8 text-center text-red-600"> 76 - โŒ Error loading Jetstream logs: {errorMessage} 77 - </div> 78 - </div> 79 - </Layout>, 80 - { status: 500 } 81 - ); 82 - } 83 - } 84 - 85 - async function handleJetstreamStatus( 86 - req: Request, 87 - _params?: URLPatternResult 88 - ): Promise<Response> { 89 - try { 90 - // Extract parameters from query 91 - const url = new URL(req.url); 92 - const isCompact = url.searchParams.get("compact") === "true"; 93 - const sliceId = url.searchParams.get("sliceId") || undefined; 94 - const handle = url.searchParams.get("handle") || undefined; 95 - 96 - // Fetch jetstream status using the public client 97 - const data = await publicClient.network.slices.slice.getJetstreamStatus(); 98 - 99 - // Render compact version for logs page 100 - if (isCompact) { 101 - return renderHTML( 102 - <JetstreamStatusDisplay connected={data.connected} isCompact /> 103 - ); 104 - } 105 - 106 - // Render full version for main page 107 - return renderHTML( 108 - <JetstreamStatus 109 - connected={data.connected} 110 - sliceId={sliceId} 111 - handle={handle} 112 - /> 113 - ); 114 - } catch (_error) { 115 - // Extract parameters for error case too 116 - const url = new URL(req.url); 117 - const isCompact = url.searchParams.get("compact") === "true"; 118 - const sliceId = url.searchParams.get("sliceId") || undefined; 119 - const handle = url.searchParams.get("handle") || undefined; 120 - 121 - // Render compact error version 122 - if (isCompact) { 123 - return renderHTML(<JetstreamStatusDisplay connected={false} isCompact />); 124 - } 125 - 126 - // Fallback to disconnected state on error for full version 127 - return renderHTML( 128 - <JetstreamStatus connected={false} sliceId={sliceId} handle={handle} /> 129 - ); 130 - } 131 - } 132 133 async function handleJetstreamLogsPage( 134 req: Request, ··· 167 console.error("Failed to fetch Jetstream logs:", error); 168 } 169 170 return renderHTML( 171 <JetstreamLogsPage 172 slice={context.sliceContext!.slice!} 173 logs={logs} 174 sliceId={sliceParams.sliceId} 175 currentUser={authContext.currentUser} 176 /> 177 ); 178 } ··· 184 pathname: "/profile/:handle/slice/:rkey/jetstream", 185 }), 186 handler: handleJetstreamLogsPage, 187 - }, 188 - { 189 - method: "GET", 190 - pattern: new URLPattern({ pathname: "/api/jetstream/status" }), 191 - handler: handleJetstreamStatus, 192 - }, 193 - { 194 - method: "GET", 195 - pattern: new URLPattern({ pathname: "/api/slices/:id/jetstream/logs" }), 196 - handler: handleJetstreamLogs, 197 }, 198 ];
··· 1 import type { Route } from "@std/http/unstable-route"; 2 + import { withAuth } from "../../../routes/middleware.ts"; 3 import { 4 requireSliceAccess, 5 withSliceAccess, ··· 7 import { getSliceClient } from "../../../utils/client.ts"; 8 import { publicClient } from "../../../config.ts"; 9 import { renderHTML } from "../../../utils/render.tsx"; 10 import { extractSliceParams } from "../../../utils/slice-params.ts"; 11 import { JetstreamLogsPage } from "./templates/JetstreamLogsPage.tsx"; 12 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts"; 13 14 async function handleJetstreamLogsPage( 15 req: Request, ··· 48 console.error("Failed to fetch Jetstream logs:", error); 49 } 50 51 + // Fetch jetstream status 52 + let jetstreamConnected = false; 53 + try { 54 + const jetstreamStatus = 55 + await publicClient.network.slices.slice.getJetstreamStatus(); 56 + jetstreamConnected = jetstreamStatus.connected; 57 + } catch (error) { 58 + console.error("Failed to fetch Jetstream status:", error); 59 + } 60 + 61 return renderHTML( 62 <JetstreamLogsPage 63 slice={context.sliceContext!.slice!} 64 logs={logs} 65 sliceId={sliceParams.sliceId} 66 currentUser={authContext.currentUser} 67 + jetstreamConnected={jetstreamConnected} 68 /> 69 ); 70 } ··· 76 pathname: "/profile/:handle/slice/:rkey/jetstream", 77 }), 78 handler: handleJetstreamLogsPage, 79 }, 80 ];
+6 -9
frontend/src/features/slices/jetstream/templates/JetstreamLogsPage.tsx
··· 13 logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 14 sliceId: string; 15 currentUser?: AuthenticatedUser; 16 } 17 18 export function JetstreamLogsPage({ ··· 20 logs, 21 sliceId, 22 currentUser, 23 }: JetstreamLogsPageProps) { 24 return ( 25 <SliceLogPage 26 slice={slice} ··· 28 currentUser={currentUser} 29 title="Jetstream Logs" 30 breadcrumbItems={[ 31 - { label: slice.name, href: buildSliceUrlFromView(slice, sliceId) }, 32 { label: "Jetstream Logs" }, 33 ]} 34 - headerActions={<JetstreamStatusCompact sliceId={sliceId} />} 35 > 36 - <div 37 - hx-get={`/api/slices/${sliceId}/jetstream/logs`} 38 - hx-trigger="load, every 20s" 39 - hx-swap="innerHTML" 40 - > 41 - <JetstreamLogs logs={logs} /> 42 - </div> 43 </SliceLogPage> 44 ); 45 }
··· 13 logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 14 sliceId: string; 15 currentUser?: AuthenticatedUser; 16 + jetstreamConnected?: boolean; 17 } 18 19 export function JetstreamLogsPage({ ··· 21 logs, 22 sliceId, 23 currentUser, 24 + jetstreamConnected = false, 25 }: JetstreamLogsPageProps) { 26 + const sliceUrl = buildSliceUrlFromView(slice, sliceId); 27 return ( 28 <SliceLogPage 29 slice={slice} ··· 31 currentUser={currentUser} 32 title="Jetstream Logs" 33 breadcrumbItems={[ 34 + { label: slice.name, href: sliceUrl }, 35 { label: "Jetstream Logs" }, 36 ]} 37 + headerActions={<JetstreamStatusCompact connected={jetstreamConnected} />} 38 > 39 + <JetstreamLogs logs={logs} /> 40 </SliceLogPage> 41 ); 42 }
-2
frontend/src/features/slices/jetstream/templates/fragments/JetstreamLogs.tsx
··· 1 - import { formatTimestamp } from "../../../../../utils/time.ts"; 2 import { LogViewer } from "../../../../../shared/fragments/LogViewer.tsx"; 3 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../../../client.ts"; 4 ··· 11 <LogViewer 12 logs={logs} 13 emptyMessage="No Jetstream logs available for this slice." 14 - formatTimestamp={formatTimestamp} 15 /> 16 ); 17 }
··· 1 import { LogViewer } from "../../../../../shared/fragments/LogViewer.tsx"; 2 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../../../client.ts"; 3 ··· 10 <LogViewer 11 logs={logs} 12 emptyMessage="No Jetstream logs available for this slice." 13 /> 14 ); 15 }
-91
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
··· 1 - import { Card } from "../../../../../shared/fragments/Card.tsx"; 2 - import { Text } from "../../../../../shared/fragments/Text.tsx"; 3 - import { Button } from "../../../../../shared/fragments/Button.tsx"; 4 - 5 - interface JetstreamStatusProps { 6 - connected: boolean; 7 - sliceId?: string; 8 - handle?: string; 9 - } 10 - 11 - export function JetstreamStatus({ connected, sliceId, handle }: JetstreamStatusProps) { 12 - const showViewLogs = sliceId && handle; 13 - 14 - if (connected) { 15 - return ( 16 - <Card 17 - padding="sm" 18 - className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 mb-6" 19 - > 20 - <div className="flex items-center justify-between"> 21 - <div className="flex items-center"> 22 - <div className="w-3 h-3 bg-green-500 rounded-full mr-3 animate-pulse"></div> 23 - <div> 24 - <Text 25 - as="h3" 26 - size="sm" 27 - variant="success" 28 - className="font-semibold block" 29 - > 30 - โœˆ๏ธ Jetstream Connected 31 - </Text> 32 - <Text as="p" size="xs" variant="success"> 33 - Real-time indexing active - new records are automatically indexed 34 - </Text> 35 - </div> 36 - </div> 37 - {showViewLogs && ( 38 - <div className="flex items-center gap-3"> 39 - <Button 40 - href={`/profile/${handle}/slice/${sliceId}/jetstream`} 41 - variant="success" 42 - size="sm" 43 - className="whitespace-nowrap" 44 - > 45 - View Logs 46 - </Button> 47 - </div> 48 - )} 49 - </div> 50 - </Card> 51 - ); 52 - } else { 53 - return ( 54 - <Card 55 - padding="sm" 56 - className="bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 mb-6" 57 - > 58 - <div className="flex items-center justify-between"> 59 - <div className="flex items-center"> 60 - <div className="w-3 h-3 bg-red-500 rounded-full mr-3"></div> 61 - <div> 62 - <Text 63 - as="h3" 64 - size="sm" 65 - variant="error" 66 - className="font-semibold block" 67 - > 68 - ๐ŸŒŠ Jetstream Disconnected 69 - </Text> 70 - <Text as="p" size="xs" variant="error"> 71 - Real-time indexing not active 72 - </Text> 73 - </div> 74 - </div> 75 - {showViewLogs && ( 76 - <div className="flex items-center gap-3"> 77 - <Button 78 - href={`/profile/${handle}/slice/${sliceId}/jetstream`} 79 - variant="danger" 80 - size="sm" 81 - className="whitespace-nowrap" 82 - > 83 - View Logs 84 - </Button> 85 - </div> 86 - )} 87 - </div> 88 - </Card> 89 - ); 90 - } 91 - }
···
+21 -9
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusCompact.tsx
··· 1 import { Text } from "../../../../../shared/fragments/Text.tsx"; 2 3 - export function JetstreamStatusCompact({ sliceId }: { sliceId: string }) { 4 return ( 5 - <div 6 - hx-get={`/api/jetstream/status?sliceId=${sliceId}&compact=true`} 7 - hx-trigger="load, every 2m" 8 - hx-swap="outerHTML" 9 - className="inline-flex items-center gap-2 text-xs" 10 - > 11 - <div className="w-2 h-2 bg-zinc-400 dark:bg-zinc-500 rounded-full"></div> 12 - <Text as="span" variant="muted" size="xs">Checking status...</Text> 13 </div> 14 ); 15 }
··· 1 import { Text } from "../../../../../shared/fragments/Text.tsx"; 2 3 + interface JetstreamStatusCompactProps { 4 + connected: boolean; 5 + } 6 + 7 + export function JetstreamStatusCompact({ connected }: JetstreamStatusCompactProps) { 8 return ( 9 + <div className="inline-flex items-center gap-2 text-xs"> 10 + {connected ? ( 11 + <> 12 + <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> 13 + <Text as="span" variant="success" size="xs"> 14 + Jetstream Connected 15 + </Text> 16 + </> 17 + ) : ( 18 + <> 19 + <div className="w-2 h-2 bg-red-500 rounded-full"></div> 20 + <Text as="span" variant="error" size="xs"> 21 + Jetstream Offline 22 + </Text> 23 + </> 24 + )} 25 </div> 26 ); 27 }
-33
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusDisplay.tsx
··· 1 - import { Text } from "../../../../../shared/fragments/Text.tsx"; 2 - 3 - interface JetstreamStatusDisplayProps { 4 - connected: boolean; 5 - isCompact?: boolean; 6 - } 7 - 8 - export function JetstreamStatusDisplay({ connected, isCompact = false }: JetstreamStatusDisplayProps) { 9 - if (isCompact) { 10 - return ( 11 - <div className="inline-flex items-center gap-2 text-xs"> 12 - {connected ? ( 13 - <> 14 - <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> 15 - <Text as="span" variant="success" size="xs"> 16 - Jetstream Connected 17 - </Text> 18 - </> 19 - ) : ( 20 - <> 21 - <div className="w-2 h-2 bg-red-500 rounded-full"></div> 22 - <Text as="span" variant="error" size="xs"> 23 - Jetstream Offline 24 - </Text> 25 - </> 26 - )} 27 - </div> 28 - ); 29 - } 30 - 31 - // Full version would be handled by the existing JetstreamStatus component 32 - return null; 33 - }
···
+11
frontend/src/features/slices/overview/handlers.tsx
··· 7 withSliceAccess, 8 } from "../../../routes/slice-middleware.ts"; 9 import { extractSliceParams } from "../../../utils/slice-params.ts"; 10 11 async function handleSliceOverview( 12 req: Request, ··· 44 actors: stat.actors, 45 })); 46 47 return renderHTML( 48 <SliceOverview 49 slice={context.sliceContext!.slice!} ··· 52 currentTab="overview" 53 currentUser={authContext.currentUser} 54 hasSliceAccess={context.sliceContext?.hasAccess} 55 />, 56 ); 57 }
··· 7 withSliceAccess, 8 } from "../../../routes/slice-middleware.ts"; 9 import { extractSliceParams } from "../../../utils/slice-params.ts"; 10 + import { publicClient } from "../../../config.ts"; 11 12 async function handleSliceOverview( 13 req: Request, ··· 45 actors: stat.actors, 46 })); 47 48 + // Fetch jetstream status 49 + let jetstreamConnected = false; 50 + try { 51 + const jetstreamStatus = await publicClient.network.slices.slice.getJetstreamStatus(); 52 + jetstreamConnected = jetstreamStatus.connected; 53 + } catch (error) { 54 + console.error("Failed to fetch Jetstream status:", error); 55 + } 56 + 57 return renderHTML( 58 <SliceOverview 59 slice={context.sliceContext!.slice!} ··· 62 currentTab="overview" 63 currentUser={authContext.currentUser} 64 hasSliceAccess={context.sliceContext?.hasAccess} 65 + jetstreamConnected={jetstreamConnected} 66 />, 67 ); 68 }
+8 -29
frontend/src/features/slices/overview/templates/SliceOverview.tsx
··· 6 import { Text } from "../../../../shared/fragments/Text.tsx"; 7 import { Link } from "../../../../shared/fragments/Link.tsx"; 8 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 9 10 function formatNumber(num: number): string { 11 return num.toLocaleString(); ··· 24 currentTab?: string; 25 currentUser?: AuthenticatedUser; 26 hasSliceAccess?: boolean; 27 } 28 29 export function SliceOverview({ ··· 33 currentTab = "overview", 34 currentUser, 35 hasSliceAccess, 36 }: SliceOverviewProps) { 37 return ( 38 <SlicePage ··· 42 currentUser={currentUser} 43 hasSliceAccess={hasSliceAccess} 44 > 45 - <div 46 - hx-get={`/api/jetstream/status?sliceId=${sliceId}&handle=${slice.creator?.handle}`} 47 - hx-trigger="load, every 2m" 48 - hx-swap="outerHTML" 49 - > 50 - <Card padding="sm" className="mb-6"> 51 - <div className="flex items-center justify-between"> 52 - <div className="flex items-center"> 53 - <div className="w-3 h-3 bg-zinc-400 dark:bg-zinc-500 rounded-full mr-3"></div> 54 - <div> 55 - <Text 56 - as="h3" 57 - size="sm" 58 - variant="secondary" 59 - className="font-semibold block" 60 - > 61 - ๐ŸŒŠ Checking Jetstream Status... 62 - </Text> 63 - <Text as="p" size="xs" variant="muted"> 64 - Loading connection status 65 - </Text> 66 - </div> 67 - </div> 68 - <Text as="span" size="xs" variant="muted"> 69 - Checking... 70 - </Text> 71 - </div> 72 - </Card> 73 - </div> 74 75 {(slice.indexedRecordCount ?? 0) > 0 && ( 76 <Card padding="md" className="mb-8">
··· 6 import { Text } from "../../../../shared/fragments/Text.tsx"; 7 import { Link } from "../../../../shared/fragments/Link.tsx"; 8 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 9 + import { JetstreamStatus } from "./fragments/JetstreamStatus.tsx"; 10 11 function formatNumber(num: number): string { 12 return num.toLocaleString(); ··· 25 currentTab?: string; 26 currentUser?: AuthenticatedUser; 27 hasSliceAccess?: boolean; 28 + jetstreamConnected?: boolean; 29 } 30 31 export function SliceOverview({ ··· 35 currentTab = "overview", 36 currentUser, 37 hasSliceAccess, 38 + jetstreamConnected = false, 39 }: SliceOverviewProps) { 40 return ( 41 <SlicePage ··· 45 currentUser={currentUser} 46 hasSliceAccess={hasSliceAccess} 47 > 48 + <JetstreamStatus 49 + connected={jetstreamConnected} 50 + sliceId={sliceId} 51 + handle={slice.creator?.handle} 52 + /> 53 54 {(slice.indexedRecordCount ?? 0) > 0 && ( 55 <Card padding="md" className="mb-8">
+91
frontend/src/features/slices/overview/templates/fragments/JetstreamStatus.tsx
···
··· 1 + import { Card } from "../../../../../shared/fragments/Card.tsx"; 2 + import { Text } from "../../../../../shared/fragments/Text.tsx"; 3 + import { Button } from "../../../../../shared/fragments/Button.tsx"; 4 + 5 + interface JetstreamStatusProps { 6 + connected: boolean; 7 + sliceId?: string; 8 + handle?: string; 9 + } 10 + 11 + export function JetstreamStatus({ connected, sliceId, handle }: JetstreamStatusProps) { 12 + const showViewLogs = sliceId && handle; 13 + 14 + if (connected) { 15 + return ( 16 + <Card 17 + padding="sm" 18 + className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 mb-6" 19 + > 20 + <div className="flex items-center justify-between"> 21 + <div className="flex items-center"> 22 + <div className="w-3 h-3 bg-green-500 rounded-full mr-3 animate-pulse"></div> 23 + <div> 24 + <Text 25 + as="h3" 26 + size="sm" 27 + variant="success" 28 + className="font-semibold block" 29 + > 30 + โœˆ๏ธ Jetstream Connected 31 + </Text> 32 + <Text as="p" size="xs" variant="success"> 33 + Real-time indexing active - new records are automatically indexed 34 + </Text> 35 + </div> 36 + </div> 37 + {showViewLogs && ( 38 + <div className="flex items-center gap-3"> 39 + <Button 40 + href={`/profile/${handle}/slice/${sliceId}/jetstream`} 41 + variant="success" 42 + size="sm" 43 + className="whitespace-nowrap" 44 + > 45 + View Logs 46 + </Button> 47 + </div> 48 + )} 49 + </div> 50 + </Card> 51 + ); 52 + } else { 53 + return ( 54 + <Card 55 + padding="sm" 56 + className="bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 mb-6" 57 + > 58 + <div className="flex items-center justify-between"> 59 + <div className="flex items-center"> 60 + <div className="w-3 h-3 bg-red-500 rounded-full mr-3"></div> 61 + <div> 62 + <Text 63 + as="h3" 64 + size="sm" 65 + variant="error" 66 + className="font-semibold block" 67 + > 68 + ๐ŸŒŠ Jetstream Disconnected 69 + </Text> 70 + <Text as="p" size="xs" variant="error"> 71 + Real-time indexing not active 72 + </Text> 73 + </div> 74 + </div> 75 + {showViewLogs && ( 76 + <div className="flex items-center gap-3"> 77 + <Button 78 + href={`/profile/${handle}/slice/${sliceId}/jetstream`} 79 + variant="danger" 80 + size="sm" 81 + className="whitespace-nowrap" 82 + > 83 + View Logs 84 + </Button> 85 + </div> 86 + )} 87 + </div> 88 + </Card> 89 + ); 90 + } 91 + }
+108 -58
frontend/src/features/slices/sync/handlers.tsx
··· 2 import { renderHTML } from "../../../utils/render.tsx"; 3 import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 4 import { getSliceClient } from "../../../utils/client.ts"; 5 - import { buildSliceUri } from "../../../utils/at-uri.ts"; 6 - import { publicClient } from "../../../config.ts"; 7 import { 8 requireSliceAccess, 9 withSliceAccess, ··· 23 req: Request, 24 params?: URLPatternResult 25 ): Promise<Response> { 26 - const context = await withAuth(req); 27 - const authResponse = requireAuth(context); 28 if (authResponse) return authResponse; 29 30 - const sliceId = params?.pathname.groups.id; 31 - 32 - if (!sliceId) { 33 - return renderHTML(<SyncResult success={false} error="Invalid slice ID" />); 34 } 35 36 try { 37 const formData = await req.formData(); 38 const collections = formData.getAll("collections") as string[]; ··· 55 ); 56 } 57 58 - const sliceClient = getSliceClient(context, sliceId); 59 await sliceClient.network.slices.slice.startSync({ 60 - slice: buildSliceUri(context.currentUser.sub!, sliceId), 61 collections: collections.length > 0 ? collections : undefined, 62 externalCollections: 63 externalCollections.length > 0 ? externalCollections : undefined, 64 repos: repos.length > 0 ? repos : undefined, 65 }); 66 67 - const handle = context.currentUser?.handle; 68 - if (!handle) { 69 - throw new Error("Unable to determine user handle"); 70 - } 71 - 72 - const redirectUrl = buildSliceUrl(handle, sliceId, "sync"); 73 return hxRedirect(redirectUrl); 74 } catch (error) { 75 console.error("Failed to start sync:", error); ··· 82 req: Request, 83 params?: URLPatternResult 84 ): Promise<Response> { 85 - const context = await withAuth(req); 86 - const authResponse = requireAuth(context); 87 if (authResponse) return authResponse; 88 89 - const sliceId = params?.pathname.groups.id; 90 - 91 - if (!sliceId) { 92 return renderHTML( 93 - <div className="p-8 text-center text-red-600">Invalid slice ID</div>, 94 { status: 400 } 95 ); 96 } 97 98 - // Extract handle from query parameters 99 - const url = new URL(req.url); 100 - const handle = url.searchParams.get("handle"); 101 102 try { 103 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 104 - const sliceClient = getSliceClient(context, sliceId); 105 const jobsResponse = await sliceClient.network.slices.slice.getJobHistory({ 106 - userDid: context.currentUser.sub!, 107 - sliceUri: sliceUri, 108 limit: 10, 109 }); 110 111 return renderHTML( 112 <JobHistory 113 jobs={jobsResponse || []} 114 - sliceId={sliceId} 115 - handle={handle || undefined} 116 /> 117 ); 118 } catch (error) { ··· 191 req: Request, 192 params?: URLPatternResult 193 ): Promise<Response> { 194 - const context = await withAuth(req); 195 - const authResponse = requireAuth(context); 196 if (authResponse) return authResponse; 197 198 - const sliceId = params?.pathname.groups.id; 199 - if (!sliceId) { 200 - return new Response("Invalid slice ID", { status: 400 }); 201 } 202 203 try { 204 - const sliceClient = getSliceClient(context, sliceId); 205 const collections: string[] = []; 206 const externalCollections: string[] = []; 207 208 - // Get slice info for domain comparison 209 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 210 - const sliceRecord = await publicClient.network.slices.slice.getRecord({ 211 - uri: sliceUri, 212 - }); 213 - const sliceDomain = sliceRecord.value.domain; 214 215 // Get all lexicons and filter by record types 216 try { ··· 241 242 return renderHTML( 243 <SyncFormModal 244 - sliceId={sliceId} 245 collections={collections} 246 externalCollections={externalCollections} 247 /> 248 ); 249 } catch (error) { 250 console.error("Error loading sync modal:", error); 251 - return renderHTML(<SyncFormModal sliceId={sliceId} />); 252 } 253 } 254 ··· 256 req: Request, 257 params?: URLPatternResult 258 ): Promise<Response> { 259 - const context = await withAuth(req); 260 - const authResponse = requireAuth(context); 261 if (authResponse) return authResponse; 262 263 - const sliceId = params?.pathname.groups.id; 264 - if (!sliceId) { 265 - return new Response("Invalid slice ID", { status: 400 }); 266 } 267 268 try { 269 const formData = await req.formData(); ··· 287 ); 288 } 289 290 - const sliceClient = getSliceClient(context, sliceId); 291 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 292 293 // Call the getSyncSummary endpoint 294 const requestParams = { 295 - slice: sliceUri, 296 collections: collections.length > 0 ? collections : undefined, 297 externalCollections: 298 externalCollections.length > 0 ? externalCollections : undefined, ··· 304 305 return renderHTML( 306 <SyncSummaryModal 307 - sliceId={sliceId} 308 summary={summaryResponse} 309 collections={collections} 310 externalCollections={externalCollections} ··· 326 }, 327 { 328 method: "GET", 329 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/modal" }), 330 handler: handleShowSyncModal, 331 }, 332 { 333 method: "POST", 334 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync" }), 335 handler: handleSliceSync, 336 }, 337 { 338 method: "POST", 339 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/summary" }), 340 handler: handleSyncSummary, 341 }, 342 { 343 method: "GET", 344 - pattern: new URLPattern({ pathname: "/api/slices/:id/job-history" }), 345 handler: handleJobHistory, 346 }, 347 ];
··· 2 import { renderHTML } from "../../../utils/render.tsx"; 3 import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 4 import { getSliceClient } from "../../../utils/client.ts"; 5 import { 6 requireSliceAccess, 7 withSliceAccess, ··· 21 req: Request, 22 params?: URLPatternResult 23 ): Promise<Response> { 24 + const authContext = await withAuth(req); 25 + const authResponse = requireAuth(authContext); 26 if (authResponse) return authResponse; 27 28 + const sliceParams = extractSliceParams(params); 29 + if (!sliceParams) { 30 + return renderHTML( 31 + <SyncResult success={false} error="Invalid slice parameters" /> 32 + ); 33 } 34 35 + const context = await withSliceAccess( 36 + authContext, 37 + sliceParams.handle, 38 + sliceParams.sliceId 39 + ); 40 + const accessError = requireSliceAccess(context); 41 + if (accessError) return accessError; 42 + 43 try { 44 const formData = await req.formData(); 45 const collections = formData.getAll("collections") as string[]; ··· 62 ); 63 } 64 65 + const sliceClient = getSliceClient( 66 + authContext, 67 + sliceParams.sliceId, 68 + context.sliceContext!.profileDid 69 + ); 70 await sliceClient.network.slices.slice.startSync({ 71 + slice: context.sliceContext!.sliceUri, 72 collections: collections.length > 0 ? collections : undefined, 73 externalCollections: 74 externalCollections.length > 0 ? externalCollections : undefined, 75 repos: repos.length > 0 ? repos : undefined, 76 }); 77 78 + const redirectUrl = buildSliceUrl( 79 + sliceParams.handle, 80 + sliceParams.sliceId, 81 + "sync" 82 + ); 83 return hxRedirect(redirectUrl); 84 } catch (error) { 85 console.error("Failed to start sync:", error); ··· 92 req: Request, 93 params?: URLPatternResult 94 ): Promise<Response> { 95 + const authContext = await withAuth(req); 96 + const authResponse = requireAuth(authContext); 97 if (authResponse) return authResponse; 98 99 + const sliceParams = extractSliceParams(params); 100 + if (!sliceParams) { 101 return renderHTML( 102 + <div className="p-8 text-center text-red-600"> 103 + Invalid slice parameters 104 + </div>, 105 { status: 400 } 106 ); 107 } 108 109 + const context = await withSliceAccess( 110 + authContext, 111 + sliceParams.handle, 112 + sliceParams.sliceId 113 + ); 114 + const accessError = requireSliceAccess(context); 115 + if (accessError) return accessError; 116 117 try { 118 + const sliceClient = getSliceClient( 119 + authContext, 120 + sliceParams.sliceId, 121 + context.sliceContext!.profileDid 122 + ); 123 const jobsResponse = await sliceClient.network.slices.slice.getJobHistory({ 124 + userDid: authContext.currentUser.sub!, 125 + sliceUri: context.sliceContext!.sliceUri, 126 limit: 10, 127 }); 128 129 return renderHTML( 130 <JobHistory 131 jobs={jobsResponse || []} 132 + sliceId={sliceParams.sliceId} 133 + handle={sliceParams.handle} 134 /> 135 ); 136 } catch (error) { ··· 209 req: Request, 210 params?: URLPatternResult 211 ): Promise<Response> { 212 + const authContext = await withAuth(req); 213 + const authResponse = requireAuth(authContext); 214 if (authResponse) return authResponse; 215 216 + const sliceParams = extractSliceParams(params); 217 + if (!sliceParams) { 218 + return new Response("Invalid slice parameters", { status: 400 }); 219 } 220 221 + const context = await withSliceAccess( 222 + authContext, 223 + sliceParams.handle, 224 + sliceParams.sliceId 225 + ); 226 + const accessError = requireSliceAccess(context); 227 + if (accessError) return accessError; 228 + 229 try { 230 + const sliceClient = getSliceClient( 231 + authContext, 232 + sliceParams.sliceId, 233 + context.sliceContext!.profileDid 234 + ); 235 const collections: string[] = []; 236 const externalCollections: string[] = []; 237 238 + // Get slice domain from context 239 + const sliceDomain = context.sliceContext!.slice!.domain; 240 241 // Get all lexicons and filter by record types 242 try { ··· 267 268 return renderHTML( 269 <SyncFormModal 270 + sliceId={sliceParams.sliceId} 271 + handle={sliceParams.handle} 272 collections={collections} 273 externalCollections={externalCollections} 274 /> 275 ); 276 } catch (error) { 277 console.error("Error loading sync modal:", error); 278 + return renderHTML( 279 + <SyncFormModal 280 + sliceId={sliceParams.sliceId} 281 + handle={sliceParams.handle} 282 + /> 283 + ); 284 } 285 } 286 ··· 288 req: Request, 289 params?: URLPatternResult 290 ): Promise<Response> { 291 + const authContext = await withAuth(req); 292 + const authResponse = requireAuth(authContext); 293 if (authResponse) return authResponse; 294 295 + const sliceParams = extractSliceParams(params); 296 + if (!sliceParams) { 297 + return new Response("Invalid slice parameters", { status: 400 }); 298 } 299 + 300 + const context = await withSliceAccess( 301 + authContext, 302 + sliceParams.handle, 303 + sliceParams.sliceId 304 + ); 305 + const accessError = requireSliceAccess(context); 306 + if (accessError) return accessError; 307 308 try { 309 const formData = await req.formData(); ··· 327 ); 328 } 329 330 + const sliceClient = getSliceClient( 331 + authContext, 332 + sliceParams.sliceId, 333 + context.sliceContext!.profileDid 334 + ); 335 336 // Call the getSyncSummary endpoint 337 const requestParams = { 338 + slice: context.sliceContext!.sliceUri, 339 collections: collections.length > 0 ? collections : undefined, 340 externalCollections: 341 externalCollections.length > 0 ? externalCollections : undefined, ··· 347 348 return renderHTML( 349 <SyncSummaryModal 350 + sliceId={sliceParams.sliceId} 351 + handle={sliceParams.handle} 352 summary={summaryResponse} 353 collections={collections} 354 externalCollections={externalCollections} ··· 370 }, 371 { 372 method: "GET", 373 + pattern: new URLPattern({ 374 + pathname: "/profile/:handle/slice/:rkey/sync/modal", 375 + }), 376 handler: handleShowSyncModal, 377 }, 378 { 379 method: "POST", 380 + pattern: new URLPattern({ pathname: "/profile/:handle/slice/:rkey/sync" }), 381 handler: handleSliceSync, 382 }, 383 { 384 method: "POST", 385 + pattern: new URLPattern({ 386 + pathname: "/profile/:handle/slice/:rkey/sync/summary", 387 + }), 388 handler: handleSyncSummary, 389 }, 390 { 391 method: "GET", 392 + pattern: new URLPattern({ 393 + pathname: "/profile/:handle/slice/:rkey/job-history", 394 + }), 395 handler: handleJobHistory, 396 }, 397 ];
+2 -2
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
··· 31 <div className="flex justify-end mb-4"> 32 <Button 33 variant="success" 34 - hx-get={`/api/slices/${sliceId}/sync/modal`} 35 hx-target="#modal-container" 36 hx-swap="innerHTML" 37 > ··· 41 <Card> 42 <Card.Header title="Recent Sync History" /> 43 <Card.Content 44 - hx-get={`/api/slices/${sliceId}/job-history?handle=${slice.creator?.handle}`} 45 hx-trigger="load, every 10s" 46 hx-swap="innerHTML" 47 >
··· 31 <div className="flex justify-end mb-4"> 32 <Button 33 variant="success" 34 + hx-get={`/profile/${slice.creator?.handle}/slice/${sliceId}/sync/modal`} 35 hx-target="#modal-container" 36 hx-swap="innerHTML" 37 > ··· 41 <Card> 42 <Card.Header title="Recent Sync History" /> 43 <Card.Content 44 + hx-get={`/profile/${slice.creator?.handle}/slice/${sliceId}/job-history`} 45 hx-trigger="load, every 10s" 46 hx-swap="innerHTML" 47 >
+3 -1
frontend/src/features/slices/sync/templates/fragments/SyncFormModal.tsx
··· 5 6 interface SyncFormModalProps { 7 sliceId: string; 8 collections?: string[]; 9 externalCollections?: string[]; 10 } 11 12 export function SyncFormModal({ 13 sliceId, 14 collections = [], 15 externalCollections = [], 16 }: SyncFormModalProps) { ··· 95 type="submit" 96 variant="primary" 97 className="flex items-center justify-center" 98 - hx-post={`/api/slices/${sliceId}/sync/summary`} 99 hx-target="#modal-container" 100 hx-swap="innerHTML" 101 >
··· 5 6 interface SyncFormModalProps { 7 sliceId: string; 8 + handle: string; 9 collections?: string[]; 10 externalCollections?: string[]; 11 } 12 13 export function SyncFormModal({ 14 sliceId, 15 + handle, 16 collections = [], 17 externalCollections = [], 18 }: SyncFormModalProps) { ··· 97 type="submit" 98 variant="primary" 99 className="flex items-center justify-center" 100 + hx-post={`/profile/${handle}/slice/${sliceId}/sync/summary`} 101 hx-target="#modal-container" 102 hx-swap="innerHTML" 103 >
+3 -1
frontend/src/features/slices/sync/templates/fragments/SyncSummaryModal.tsx
··· 5 6 interface SyncSummaryModalProps { 7 sliceId: string; 8 summary: NetworkSlicesSliceGetSyncSummaryOutput; 9 collections: string[]; 10 externalCollections: string[]; ··· 13 14 export function SyncSummaryModal({ 15 sliceId, 16 summary, 17 collections, 18 externalCollections, ··· 144 145 {/* Actions */} 146 <form 147 - hx-post={`/api/slices/${sliceId}/sync`} 148 hx-target="#sync-result" 149 hx-swap="innerHTML" 150 className="space-y-4"
··· 5 6 interface SyncSummaryModalProps { 7 sliceId: string; 8 + handle: string; 9 summary: NetworkSlicesSliceGetSyncSummaryOutput; 10 collections: string[]; 11 externalCollections: string[]; ··· 14 15 export function SyncSummaryModal({ 16 sliceId, 17 + handle, 18 summary, 19 collections, 20 externalCollections, ··· 146 147 {/* Actions */} 148 <form 149 + hx-post={`/profile/${handle}/slice/${sliceId}/sync`} 150 hx-target="#sync-result" 151 hx-swap="innerHTML" 152 className="space-y-4"
+23 -52
frontend/src/features/slices/sync-logs/handlers.tsx
··· 1 import type { Route } from "@std/http/unstable-route"; 2 import { renderHTML } from "../../../utils/render.tsx"; 3 - import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 4 import { getSliceClient } from "../../../utils/client.ts"; 5 import { 6 requireSliceAccess, ··· 8 } from "../../../routes/slice-middleware.ts"; 9 import { extractSliceParams } from "../../../utils/slice-params.ts"; 10 import { SyncJobLogsPage } from "./templates/SyncJobLogsPage.tsx"; 11 - import { SyncJobLogs } from "./templates/SyncJobLogs.tsx"; 12 13 async function handleSyncJobLogsPage( 14 req: Request, 15 - params?: URLPatternResult, 16 ): Promise<Response> { 17 const authContext = await withAuth(req); 18 const sliceParams = extractSliceParams(params); ··· 25 const context = await withSliceAccess( 26 authContext, 27 sliceParams.handle, 28 - sliceParams.sliceId, 29 ); 30 const accessError = requireSliceAccess(context); 31 if (accessError) return accessError; 32 33 - return renderHTML( 34 - <SyncJobLogsPage 35 - slice={context.sliceContext!.slice!} 36 - sliceId={sliceParams.sliceId} 37 - jobId={jobId} 38 - currentUser={authContext.currentUser} 39 - />, 40 - ); 41 - } 42 - 43 - async function handleSyncJobLogs( 44 - req: Request, 45 - params?: URLPatternResult, 46 - ): Promise<Response> { 47 - const context = await withAuth(req); 48 - const authResponse = requireAuth(context); 49 - if (authResponse) return authResponse; 50 - 51 - const sliceId = params?.pathname.groups.id; 52 - const jobId = params?.pathname.groups.jobId; 53 - 54 - if (!sliceId || !jobId) { 55 - return renderHTML( 56 - <div className="p-8 text-center text-red-600"> 57 - Invalid slice ID or job ID 58 - </div>, 59 - { status: 400 }, 60 - ); 61 - } 62 63 try { 64 - const sliceClient = getSliceClient(context, sliceId); 65 const logsResponse = await sliceClient.network.slices.slice.getJobLogs({ 66 jobId, 67 }); 68 69 if (logsResponse.logs && Array.isArray(logsResponse.logs)) { 70 - return renderHTML(<SyncJobLogs logs={logsResponse.logs} />); 71 } 72 73 - return renderHTML( 74 - <div className="p-8 text-center text-gray-600">No logs available</div>, 75 - ); 76 - } catch (error) { 77 - console.error("Failed to get sync job logs:", error); 78 - const errorMessage = error instanceof Error ? error.message : String(error); 79 - return renderHTML( 80 - <div className="p-8 text-center text-red-600"> 81 - Failed to load logs: {errorMessage} 82 - </div>, 83 - ); 84 - } 85 } 86 87 export const syncLogsRoutes: Route[] = [ ··· 91 pathname: "/profile/:handle/slice/:rkey/sync/:jobId", 92 }), 93 handler: handleSyncJobLogsPage, 94 - }, 95 - { 96 - method: "GET", 97 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/:jobId" }), 98 - handler: handleSyncJobLogs, 99 }, 100 ];
··· 1 import type { Route } from "@std/http/unstable-route"; 2 import { renderHTML } from "../../../utils/render.tsx"; 3 + import { withAuth } from "../../../routes/middleware.ts"; 4 import { getSliceClient } from "../../../utils/client.ts"; 5 import { 6 requireSliceAccess, ··· 8 } from "../../../routes/slice-middleware.ts"; 9 import { extractSliceParams } from "../../../utils/slice-params.ts"; 10 import { SyncJobLogsPage } from "./templates/SyncJobLogsPage.tsx"; 11 + import type { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts"; 12 13 async function handleSyncJobLogsPage( 14 req: Request, 15 + params?: URLPatternResult 16 ): Promise<Response> { 17 const authContext = await withAuth(req); 18 const sliceParams = extractSliceParams(params); ··· 25 const context = await withSliceAccess( 26 authContext, 27 sliceParams.handle, 28 + sliceParams.sliceId 29 ); 30 const accessError = requireSliceAccess(context); 31 if (accessError) return accessError; 32 33 + // Fetch sync job logs 34 + let logs: NetworkSlicesSliceGetJobLogsLogEntry[] = []; 35 + let error: string | null = null; 36 37 try { 38 + const sliceClient = getSliceClient(authContext, sliceParams.sliceId); 39 const logsResponse = await sliceClient.network.slices.slice.getJobLogs({ 40 jobId, 41 }); 42 43 if (logsResponse.logs && Array.isArray(logsResponse.logs)) { 44 + logs = logsResponse.logs; 45 } 46 + } catch (err) { 47 + console.error("Failed to get sync job logs:", err); 48 + error = err instanceof Error ? err.message : String(err); 49 + } 50 51 + return renderHTML( 52 + <SyncJobLogsPage 53 + slice={context.sliceContext!.slice!} 54 + sliceId={sliceParams.sliceId} 55 + jobId={jobId} 56 + currentUser={authContext.currentUser} 57 + logs={logs} 58 + error={error} 59 + /> 60 + ); 61 } 62 63 export const syncLogsRoutes: Route[] = [ ··· 67 pathname: "/profile/:handle/slice/:rkey/sync/:jobId", 68 }), 69 handler: handleSyncJobLogsPage, 70 }, 71 ];
+17 -13
frontend/src/features/slices/sync-logs/templates/SyncJobLogsPage.tsx
··· 1 import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx"; 2 import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 3 - import type { NetworkSlicesSliceDefsSliceView } from "../../../../client.ts"; 4 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 5 6 interface SyncJobLogsPageProps { 7 slice: NetworkSlicesSliceDefsSliceView; 8 sliceId: string; 9 jobId: string; 10 currentUser?: AuthenticatedUser; 11 } 12 13 export function SyncJobLogsPage({ ··· 15 sliceId, 16 jobId, 17 currentUser, 18 }: SyncJobLogsPageProps) { 19 return ( 20 <SliceLogPage ··· 25 breadcrumbItems={[ 26 { label: slice.name, href: buildSliceUrlFromView(slice, sliceId) }, 27 { label: "Sync", href: buildSliceUrlFromView(slice, sliceId, "sync") }, 28 - { label: jobId.split("-")[0] + "..." } 29 ]} 30 headerActions={ 31 - <div className="text-sm text-zinc-500 font-mono"> 32 - Job: {jobId} 33 - </div> 34 } 35 > 36 - <div 37 - hx-get={`/api/slices/${sliceId}/sync/${jobId}`} 38 - hx-trigger="load" 39 - hx-swap="innerHTML" 40 - > 41 - <div className="p-8 text-center text-zinc-500"> 42 - Loading logs... 43 </div> 44 - </div> 45 </SliceLogPage> 46 ); 47 }
··· 1 import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx"; 2 import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 3 + import type { 4 + NetworkSlicesSliceDefsSliceView, 5 + NetworkSlicesSliceGetJobLogsLogEntry, 6 + } from "../../../../client.ts"; 7 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 8 + import { SyncJobLogs } from "./SyncJobLogs.tsx"; 9 10 interface SyncJobLogsPageProps { 11 slice: NetworkSlicesSliceDefsSliceView; 12 sliceId: string; 13 jobId: string; 14 currentUser?: AuthenticatedUser; 15 + logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 16 + error?: string | null; 17 } 18 19 export function SyncJobLogsPage({ ··· 21 sliceId, 22 jobId, 23 currentUser, 24 + logs, 25 + error, 26 }: SyncJobLogsPageProps) { 27 return ( 28 <SliceLogPage ··· 33 breadcrumbItems={[ 34 { label: slice.name, href: buildSliceUrlFromView(slice, sliceId) }, 35 { label: "Sync", href: buildSliceUrlFromView(slice, sliceId, "sync") }, 36 + { label: jobId.split("-")[0] + "..." }, 37 ]} 38 headerActions={ 39 + <div className="text-sm text-zinc-500 font-mono">Job: {jobId}</div> 40 } 41 > 42 + {error ? ( 43 + <div className="p-8 text-center text-red-600"> 44 + Failed to load logs: {error} 45 </div> 46 + ) : ( 47 + <SyncJobLogs logs={logs} /> 48 + )} 49 </SliceLogPage> 50 ); 51 }
+3 -3
frontend/src/routes/slice-middleware.ts
··· 1 - import { publicClient } from "../config.ts"; 2 import { buildAtUri } from "../utils/at-uri.ts"; 3 import { getSlice } from "../lib/api.ts"; 4 import type { AuthenticatedUser } from "./middleware.ts"; ··· 79 try { 80 const slice = await getSlice(publicClient, sliceUri); 81 82 - // User has access if they own the slice 83 const hasAccess = context.currentUser.isAuthenticated && 84 - context.currentUser.sub === profileDid; 85 86 return { 87 ...context,
··· 1 + import { publicClient, ADMIN_DID } from "../config.ts"; 2 import { buildAtUri } from "../utils/at-uri.ts"; 3 import { getSlice } from "../lib/api.ts"; 4 import type { AuthenticatedUser } from "./middleware.ts"; ··· 79 try { 80 const slice = await getSlice(publicClient, sliceUri); 81 82 + // User has access if they own the slice or are an admin 83 const hasAccess = context.currentUser.isAuthenticated && 84 + (context.currentUser.sub === profileDid || context.currentUser.sub === ADMIN_DID); 85 86 return { 87 ...context,
+62 -61
frontend/src/shared/fragments/LogViewer.tsx
··· 2 import { Text } from "./Text.tsx"; 3 import { Card } from "./Card.tsx"; 4 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../client.ts"; 5 6 interface LogViewerProps { 7 logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 8 emptyMessage?: string; 9 - formatTimestamp?: (timestamp: string) => string; 10 } 11 12 export function LogViewer({ 13 logs, 14 emptyMessage = "No logs available.", 15 - formatTimestamp = (timestamp) => new Date(timestamp).toLocaleString(), 16 }: LogViewerProps) { 17 if (logs.length === 0) { 18 return ( ··· 29 const infoCount = logs.filter((l) => l.level === "info").length; 30 31 return ( 32 - <Card> 33 - <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm"> 34 - <div className="flex items-center gap-4"> 35 - <Text as="span" size="sm"> 36 - Total: <strong>{logs.length}</strong> 37 - </Text> 38 - {errorCount > 0 && ( 39 - <Text as="span" size="sm" variant="error"> 40 - Errors: <strong>{errorCount}</strong> 41 </Text> 42 - )} 43 - {warnCount > 0 && ( 44 - <Text as="span" size="sm" variant="warning"> 45 - Warnings: <strong>{warnCount}</strong> 46 </Text> 47 - )} 48 - <Text 49 - as="span" 50 - size="sm" 51 - className="text-blue-600 dark:text-blue-400" 52 - > 53 - Info: <strong>{infoCount}</strong> 54 - </Text> 55 </div> 56 - </div> 57 58 - <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 59 - {logs.map((log) => ( 60 - <div 61 - key={log.id} 62 - className="p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 font-mono text-sm" 63 - > 64 - <div className="flex items-start gap-3"> 65 - <Text as="span" size="xs" variant="muted"> 66 - {formatTimestamp(log.createdAt)} 67 - </Text> 68 - <LogLevelBadge level={log.level} /> 69 - <div className="flex-1"> 70 - <Text as="div" size="sm"> 71 - {log.message} 72 - </Text> 73 - {log.metadata && Object.keys(log.metadata).length > 0 && ( 74 - <details className="mt-2"> 75 - <summary 76 - className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 77 - /* @ts-ignore - Hyperscript attribute */ 78 - _="on click toggle .hidden on next <pre/>" 79 - > 80 - <Text as="span" size="xs" variant="muted"> 81 - View metadata 82 - </Text> 83 - </summary> 84 - <pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-800 rounded text-xs overflow-x-auto break-words whitespace-pre-wrap hidden"> 85 - <Text as="span" size="xs"> 86 - {JSON.stringify(log.metadata, null, 2)} 87 - </Text> 88 - </pre> 89 - </details> 90 - )} 91 </div> 92 </div> 93 - </div> 94 - ))} 95 - </Card.Content> 96 - </Card> 97 ); 98 }
··· 2 import { Text } from "./Text.tsx"; 3 import { Card } from "./Card.tsx"; 4 import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../client.ts"; 5 + import { formatTimestamp } from "../../utils/time.ts"; 6 7 interface LogViewerProps { 8 logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 9 emptyMessage?: string; 10 } 11 12 export function LogViewer({ 13 logs, 14 emptyMessage = "No logs available.", 15 }: LogViewerProps) { 16 if (logs.length === 0) { 17 return ( ··· 28 const infoCount = logs.filter((l) => l.level === "info").length; 29 30 return ( 31 + <> 32 + <Card> 33 + <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm"> 34 + <div className="flex items-center gap-4"> 35 + <Text as="span" size="sm"> 36 + Total: <strong>{logs.length}</strong> 37 </Text> 38 + {errorCount > 0 && ( 39 + <Text as="span" size="sm" variant="error"> 40 + Errors: <strong>{errorCount}</strong> 41 + </Text> 42 + )} 43 + {warnCount > 0 && ( 44 + <Text as="span" size="sm" variant="warning"> 45 + Warnings: <strong>{warnCount}</strong> 46 + </Text> 47 + )} 48 + <Text 49 + as="span" 50 + size="sm" 51 + className="text-blue-600 dark:text-blue-400" 52 + > 53 + Info: <strong>{infoCount}</strong> 54 </Text> 55 + </div> 56 </div> 57 58 + <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 59 + {logs.map((log) => ( 60 + <div 61 + key={log.id} 62 + className="p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 font-mono text-sm" 63 + > 64 + <div className="flex items-start gap-3"> 65 + <span className="text-xs text-zinc-500 dark:text-zinc-400"> 66 + {formatTimestamp(log.createdAt)} 67 + </span> 68 + <LogLevelBadge level={log.level} /> 69 + <div className="flex-1"> 70 + <Text as="div" size="sm"> 71 + {log.message} 72 + </Text> 73 + {log.metadata && Object.keys(log.metadata).length > 0 && ( 74 + <details className="mt-2"> 75 + <summary 76 + className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 77 + /* @ts-ignore - Hyperscript attribute */ 78 + _="on click toggle .hidden on next <pre/>" 79 + > 80 + <Text as="span" size="xs" variant="muted"> 81 + View metadata 82 + </Text> 83 + </summary> 84 + <pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-800 rounded text-xs overflow-x-auto break-words whitespace-pre-wrap hidden"> 85 + <Text as="span" size="xs"> 86 + {JSON.stringify(log.metadata, null, 2)} 87 + </Text> 88 + </pre> 89 + </details> 90 + )} 91 + </div> 92 </div> 93 </div> 94 + ))} 95 + </Card.Content> 96 + </Card> 97 + </> 98 ); 99 }
+6 -3
frontend/src/utils/time.ts
··· 1 export function formatTimestamp(dateString: string): string { 2 const date = new Date(dateString); 3 - return date.toLocaleTimeString([], { 4 - hour: "2-digit", 5 minute: "2-digit", 6 second: "2-digit", 7 - fractionalSecondDigits: 3, 8 }); 9 } 10
··· 1 export function formatTimestamp(dateString: string): string { 2 const date = new Date(dateString); 3 + return date.toLocaleString([], { 4 + month: "numeric", 5 + day: "numeric", 6 + year: "numeric", 7 + hour: "numeric", 8 minute: "2-digit", 9 second: "2-digit", 10 + hour12: true, 11 }); 12 } 13
+1 -1
frontend.fly.toml
··· 23 force_https = true 24 auto_stop_machines = 'stop' 25 auto_start_machines = true 26 - min_machines_running = 0 27 processes = ['app'] 28 29 [[vm]]
··· 23 force_https = true 24 auto_stop_machines = 'stop' 25 auto_start_machines = true 26 + min_machines_running = 1 27 processes = ['app'] 28 29 [[vm]]
+42 -14
packages/cli/scripts/embed-templates.ts
··· 4 import { relative, join } from "@std/path"; 5 import { encodeBase64 } from "@std/encoding/base64"; 6 7 - const TEMPLATE_DIR = join(import.meta.dirname!, "..", "src", "templates", "deno-ssr"); 8 const OUTPUT_FILE = join(import.meta.dirname!, "..", "src", "templates", "embedded.ts"); 9 10 interface TemplateFile { 11 path: string; 12 content: string; 13 } 14 15 async function embedTemplates() { 16 - console.log("Embedding templates from:", TEMPLATE_DIR); 17 18 const templates: TemplateFile[] = []; 19 20 - // Walk through all files in the template directory 21 - for await (const entry of walk(TEMPLATE_DIR, { includeFiles: true, includeDirs: false })) { 22 - const relativePath = relative(TEMPLATE_DIR, entry.path); 23 - const content = await Deno.readFile(entry.path); 24 - const base64Content = encodeBase64(content); 25 26 - templates.push({ 27 - path: relativePath, 28 - content: base64Content, 29 - }); 30 31 - console.log(` Embedded: ${relativePath}`); 32 } 33 34 // Generate TypeScript file with embedded templates ··· 36 // Generated by scripts/embed-templates.ts 37 38 export interface EmbeddedTemplate { 39 path: string; 40 content: string; // Base64 encoded 41 } 42 43 export const EMBEDDED_TEMPLATES: EmbeddedTemplate[] = ${JSON.stringify(templates, null, 2)}; 44 45 - export function getTemplateContent(path: string): Uint8Array | undefined { 46 - const template = EMBEDDED_TEMPLATES.find(t => t.path === path); 47 if (!template) return undefined; 48 49 const binaryString = atob(template.content); ··· 57 export function getAllTemplates(): Map<string, Uint8Array> { 58 const result = new Map<string, Uint8Array>(); 59 for (const template of EMBEDDED_TEMPLATES) { 60 const binaryString = atob(template.content); 61 const bytes = new Uint8Array(binaryString.length); 62 for (let i = 0; i < binaryString.length; i++) {
··· 4 import { relative, join } from "@std/path"; 5 import { encodeBase64 } from "@std/encoding/base64"; 6 7 + const TEMPLATES_BASE_DIR = join(import.meta.dirname!, "..", "src", "templates"); 8 const OUTPUT_FILE = join(import.meta.dirname!, "..", "src", "templates", "embedded.ts"); 9 + 10 + const TEMPLATE_NAMES = ["deno-ssr"]; 11 12 interface TemplateFile { 13 + template: string; // Template name (e.g., "deno-ssr", "deno-graphql") 14 path: string; 15 content: string; 16 } 17 18 async function embedTemplates() { 19 + console.log("Embedding templates from:", TEMPLATES_BASE_DIR); 20 21 const templates: TemplateFile[] = []; 22 23 + // Walk through each template directory 24 + for (const templateName of TEMPLATE_NAMES) { 25 + const templateDir = join(TEMPLATES_BASE_DIR, templateName); 26 + console.log(`\nProcessing template: ${templateName}`); 27 28 + // Walk through all files in the template directory 29 + for await (const entry of walk(templateDir, { includeFiles: true, includeDirs: false })) { 30 + const relativePath = relative(templateDir, entry.path); 31 + const content = await Deno.readFile(entry.path); 32 + const base64Content = encodeBase64(content); 33 34 + templates.push({ 35 + template: templateName, 36 + path: relativePath, 37 + content: base64Content, 38 + }); 39 + 40 + console.log(` Embedded: ${relativePath}`); 41 + } 42 } 43 44 // Generate TypeScript file with embedded templates ··· 46 // Generated by scripts/embed-templates.ts 47 48 export interface EmbeddedTemplate { 49 + template: string; // Template name (e.g., "deno-ssr", "deno-graphql") 50 path: string; 51 content: string; // Base64 encoded 52 } 53 54 export const EMBEDDED_TEMPLATES: EmbeddedTemplate[] = ${JSON.stringify(templates, null, 2)}; 55 56 + export const AVAILABLE_TEMPLATES = ${JSON.stringify(TEMPLATE_NAMES)}; 57 + 58 + export function getTemplateContent(templateName: string, path: string): Uint8Array | undefined { 59 + const template = EMBEDDED_TEMPLATES.find(t => t.template === templateName && t.path === path); 60 if (!template) return undefined; 61 62 const binaryString = atob(template.content); ··· 70 export function getAllTemplates(): Map<string, Uint8Array> { 71 const result = new Map<string, Uint8Array>(); 72 for (const template of EMBEDDED_TEMPLATES) { 73 + const binaryString = atob(template.content); 74 + const bytes = new Uint8Array(binaryString.length); 75 + for (let i = 0; i < binaryString.length; i++) { 76 + bytes[i] = binaryString.charCodeAt(i); 77 + } 78 + result.set(template.path, bytes); 79 + } 80 + return result; 81 + } 82 + 83 + export function getTemplatesForName(templateName: string): Map<string, Uint8Array> { 84 + const result = new Map<string, Uint8Array>(); 85 + for (const template of EMBEDDED_TEMPLATES) { 86 + if (template.template !== templateName) continue; 87 + 88 const binaryString = atob(template.content); 89 const bytes = new Uint8Array(binaryString.length); 90 for (let i = 0; i < binaryString.length; i++) {
+31 -16
packages/cli/src/commands/init.ts
··· 3 import { ensureDir } from "@std/fs"; 4 import { cyan, green, bold, dim } from "@std/fmt/colors"; 5 import { logger } from "../utils/logger.ts"; 6 - import { getAllTemplates } from "../templates/embedded.ts"; 7 import { generateSliceName, generateDomain } from "../utils/name_generator.ts"; 8 import { createAuthenticatedClient } from "../utils/client.ts"; 9 import { ConfigManager } from "../auth/config.ts"; ··· 20 21 export async function initCommand(args: string[], _globalArgs: unknown) { 22 const parsed = parseArgs(args, { 23 - string: ["name"], 24 boolean: ["help"], 25 alias: { 26 n: "name", 27 h: "help", 28 }, 29 }); 30 ··· 33 return; 34 } 35 36 // Check if we're inside an existing project 37 const currentDir = Deno.cwd(); 38 const projectIndicators = ["deno.json", "slices.json", ".git"]; ··· 241 await ensureDir(targetDir); 242 243 // Extract embedded templates 244 - await extractEmbeddedTemplates(targetDir, projectName); 245 246 // Update .env.example with slice URI if we created one 247 if (sliceUri) { ··· 421 422 async function extractEmbeddedTemplates( 423 targetDir: string, 424 - projectName: string 425 ) { 426 try { 427 - const templates = getAllTemplates(); 428 429 if (templates.size === 0) { 430 - throw new Error("No embedded templates found"); 431 } 432 433 for (const [relativePath, content] of templates.entries()) { ··· 444 // Process template files for variable replacement 445 await processTemplateFiles(targetDir, projectName); 446 447 - logger.info("Template files extracted and processed"); 448 } catch (error) { 449 const err = error as Error; 450 logger.error("Failed to extract templates:", err.message); ··· 471 472 function showInitHelp() { 473 console.log(` 474 - Initialize a new Deno SSR project with OAuth authentication 475 476 USAGE: 477 - slices init <project-name> Create project with specified name 478 - slices init Create project with random generated name 479 - slices init --name <name> Create project with specified name 480 481 ARGUMENTS: 482 <project-name> Name of the project to create (optional) 483 484 OPTIONS: 485 - -n, --name <name> Project name 486 - -h, --help Show this help message 487 488 EXAMPLES: 489 - slices init my-app Creates "my-app" project and slice 490 - slices init Creates project with random name like "stellar-wave" 491 - slices init --name my-project 492 493 FEATURES: 494 โ€ข Automatically creates a matching slice (if authenticated)
··· 3 import { ensureDir } from "@std/fs"; 4 import { cyan, green, bold, dim } from "@std/fmt/colors"; 5 import { logger } from "../utils/logger.ts"; 6 + import { getTemplatesForName, AVAILABLE_TEMPLATES } from "../templates/embedded.ts"; 7 import { generateSliceName, generateDomain } from "../utils/name_generator.ts"; 8 import { createAuthenticatedClient } from "../utils/client.ts"; 9 import { ConfigManager } from "../auth/config.ts"; ··· 20 21 export async function initCommand(args: string[], _globalArgs: unknown) { 22 const parsed = parseArgs(args, { 23 + string: ["name", "template"], 24 boolean: ["help"], 25 alias: { 26 n: "name", 27 h: "help", 28 + t: "template", 29 + }, 30 + default: { 31 + template: "deno-ssr", 32 }, 33 }); 34 ··· 37 return; 38 } 39 40 + // Validate template name 41 + const templateName = parsed.template as string; 42 + if (!AVAILABLE_TEMPLATES.includes(templateName)) { 43 + logger.error(`Invalid template: ${templateName}`); 44 + logger.info(`Available templates: ${AVAILABLE_TEMPLATES.join(", ")}`); 45 + Deno.exit(1); 46 + } 47 + 48 // Check if we're inside an existing project 49 const currentDir = Deno.cwd(); 50 const projectIndicators = ["deno.json", "slices.json", ".git"]; ··· 253 await ensureDir(targetDir); 254 255 // Extract embedded templates 256 + await extractEmbeddedTemplates(targetDir, projectName, templateName); 257 258 // Update .env.example with slice URI if we created one 259 if (sliceUri) { ··· 433 434 async function extractEmbeddedTemplates( 435 targetDir: string, 436 + projectName: string, 437 + templateName: string 438 ) { 439 try { 440 + const templates = getTemplatesForName(templateName); 441 442 if (templates.size === 0) { 443 + throw new Error(`No templates found for: ${templateName}`); 444 } 445 446 for (const [relativePath, content] of templates.entries()) { ··· 457 // Process template files for variable replacement 458 await processTemplateFiles(targetDir, projectName); 459 460 + logger.info(`Template files extracted and processed (${templateName})`); 461 } catch (error) { 462 const err = error as Error; 463 logger.error("Failed to extract templates:", err.message); ··· 484 485 function showInitHelp() { 486 console.log(` 487 + Initialize a new Deno project with OAuth authentication 488 489 USAGE: 490 + slices init <project-name> Create project with specified name 491 + slices init Create project with random generated name 492 + slices init --name <name> Create project with specified name 493 + slices init --template <template-name> Specify template to use 494 495 ARGUMENTS: 496 <project-name> Name of the project to create (optional) 497 498 OPTIONS: 499 + -n, --name <name> Project name 500 + -t, --template <name> Template to use (default: deno-ssr) 501 + Available: ${AVAILABLE_TEMPLATES.join(", ")} 502 + -h, --help Show this help message 503 504 EXAMPLES: 505 + slices init my-app Creates "my-app" project with deno-ssr template 506 + slices init Creates project with random name 507 508 FEATURES: 509 โ€ข Automatically creates a matching slice (if authenticated)
+6 -6
packages/cli/src/templates/deno-ssr/src/config.ts
··· 32 clientSecret: OAUTH_CLIENT_SECRET, 33 authBaseUrl: OAUTH_AIP_BASE_URL, 34 redirectUri: OAUTH_REDIRECT_URI, 35 - scopes: ["atproto", "openid", "profile"], 36 }; 37 38 // Export config and storage for creating user-scoped clients ··· 61 ); 62 63 // Helper function to create user-scoped OAuth client 64 - export function createOAuthClient(userId: string): OAuthClient { 65 - return new OAuthClient(oauthConfig, oauthStorage, userId); 66 } 67 68 // Helper function to create authenticated AtProto client for a user 69 - export function createSessionClient(userId: string): AtProtoClient { 70 - const userOAuthClient = createOAuthClient(userId); 71 return new AtProtoClient(API_URL!, SLICE_URI!, userOAuthClient); 72 } 73 74 // Public client for unauthenticated requests 75 - export const publicClient = new AtProtoClient(API_URL, SLICE_URI);
··· 32 clientSecret: OAUTH_CLIENT_SECRET, 33 authBaseUrl: OAUTH_AIP_BASE_URL, 34 redirectUri: OAUTH_REDIRECT_URI, 35 + scopes: ["atproto", "openid", "profile", "transition:generic"], 36 }; 37 38 // Export config and storage for creating user-scoped clients ··· 61 ); 62 63 // Helper function to create user-scoped OAuth client 64 + export function createOAuthClient(sessionId: string): OAuthClient { 65 + return new OAuthClient(oauthConfig, oauthStorage, sessionId); 66 } 67 68 // Helper function to create authenticated AtProto client for a user 69 + export function createSessionClient(sessionId: string): AtProtoClient { 70 + const userOAuthClient = createOAuthClient(sessionId); 71 return new AtProtoClient(API_URL!, SLICE_URI!, userOAuthClient); 72 } 73 74 // Public client for unauthenticated requests 75 + export const publicClient = new AtProtoClient(API_URL, SLICE_URI);
+40 -3
packages/cli/src/templates/embedded.ts
··· 2 // Generated by scripts/embed-templates.ts 3 4 export interface EmbeddedTemplate { 5 path: string; 6 content: string; // Base64 encoded 7 } 8 9 export const EMBEDDED_TEMPLATES: EmbeddedTemplate[] = [ 10 { 11 "path": "deno.json", 12 "content": "ewogICJ0YXNrcyI6IHsKICAgICJzdGFydCI6ICJkZW5vIHJ1biAtQSAtLWVudi1maWxlPS5lbnYgc3JjL21haW4udHMiLAogICAgImRldiI6ICJkZW5vIHJ1biAtQSAtLWVudi1maWxlPS5lbnYgLS13YXRjaCBzcmMvbWFpbi50cyIKICB9LAogICJjb21waWxlck9wdGlvbnMiOiB7CiAgICAianN4IjogInByZWNvbXBpbGUiLAogICAgImpzeEltcG9ydFNvdXJjZSI6ICJwcmVhY3QiCiAgfSwKICAiaW1wb3J0cyI6IHsKICAgICJAc2xpY2VzL2NsaWVudCI6ICJqc3I6QHNsaWNlcy9jbGllbnRAXjAuMS4wLWFscGhhLjQiLAogICAgIkBzbGljZXMvb2F1dGgiOiAianNyOkBzbGljZXMvb2F1dGhAXjAuNi4wIiwKICAgICJAc2xpY2VzL3Nlc3Npb24iOiAianNyOkBzbGljZXMvc2Vzc2lvbkBeMC4zLjAiLAogICAgIkBzdGQvYXNzZXJ0IjogImpzcjpAc3RkL2Fzc2VydEBeMS4wLjE0IiwKICAgICJAc3RkL2ZtdCI6ICJqc3I6QHN0ZC9mbXRAXjEuMC44IiwKICAgICJwcmVhY3QiOiAibnBtOnByZWFjdEBeMTAuMjcuMSIsCiAgICAicHJlYWN0LXJlbmRlci10by1zdHJpbmciOiAibnBtOnByZWFjdC1yZW5kZXItdG8tc3RyaW5nQF42LjUuMTMiLAogICAgInR5cGVkLWh0bXgiOiAibnBtOnR5cGVkLWh0bXhAXjAuMy4xIiwKICAgICJAc3RkL2h0dHAiOiAianNyOkBzdGQvaHR0cEBeMS4wLjIwIiwKICAgICJjbHN4IjogIm5wbTpjbHN4QF4yLjEuMSIsCiAgICAidGFpbHdpbmQtbWVyZ2UiOiAibnBtOnRhaWx3aW5kLW1lcmdlQF4yLjUuNSIsCiAgICAibHVjaWRlLXByZWFjdCI6ICJucG06bHVjaWRlLXByZWFjdEBeMC41NDQuMCIKICB9LAogICJub2RlTW9kdWxlc0RpciI6ICJhdXRvIgp9Cg==" 13 }, 14 { 15 "path": "README.md", 16 "content": "IyB7e1BST0pFQ1RfTkFNRX19CgpBIERlbm8gU1NSIHdlYiBhcHBsaWNhdGlvbiB3aXRoIEFUIFByb3RvY29sIGludGVncmF0aW9uLCBidWlsdCB3aXRoIFByZWFjdCwKSFRNWCwgYW5kIE9BdXRoIGF1dGhlbnRpY2F0aW9uLgoKIyMgUXVpY2sgU3RhcnQKCmBgYGJhc2gKIyBTdGFydCB0aGUgZGV2ZWxvcG1lbnQgc2VydmVyCmRlbm8gdGFzayBkZXYKYGBgCgpWaXNpdCB5b3VyIGFwcCBhdCBodHRwOi8vbG9jYWxob3N0OjgwODAKCj4gKipOb3RlOioqIFlvdXIgc2xpY2UgYW5kIE9BdXRoIGNyZWRlbnRpYWxzIHdlcmUgYXV0b21hdGljYWxseSBjb25maWd1cmVkCj4gZHVyaW5nIHByb2plY3QgY3JlYXRpb24uIFRoZSBgLmVudmAgZmlsZSBpcyBhbHJlYWR5IHNldCB1cCB3aXRoIHlvdXIKPiBjcmVkZW50aWFscy4KCiMjIEZlYXR1cmVzCgotIPCflJAgKipPQXV0aCBBdXRoZW50aWNhdGlvbioqIHdpdGggUEtDRSBmbG93Ci0g4pqhICoqU2VydmVyLVNpZGUgUmVuZGVyaW5nKiogd2l0aCBQcmVhY3QKLSDwn46vICoqSW50ZXJhY3RpdmUgVUkqKiB3aXRoIEhUTVgKLSDwn46oICoqU3R5bGluZyoqIHdpdGggVGFpbHdpbmQgQ1NTCi0g8J+XhO+4jyAqKlNlc3Npb24gTWFuYWdlbWVudCoqIHdpdGggU1FMaXRlCi0g8J+UhCAqKkF1dG8gVG9rZW4gUmVmcmVzaCoqCi0g8J+Pl++4jyAqKkZlYXR1cmUtQmFzZWQgQXJjaGl0ZWN0dXJlKioKCiMjIERldmVsb3BtZW50CgpgYGBiYXNoCiMgU3RhcnQgZGV2ZWxvcG1lbnQgc2VydmVyIHdpdGggaG90IHJlbG9hZApkZW5vIHRhc2sgZGV2CgojIFN0YXJ0IHByb2R1Y3Rpb24gc2VydmVyCmRlbm8gdGFzayBzdGFydAoKIyBGb3JtYXQgY29kZQpkZW5vIGZtdAoKIyBDaGVjayB0eXBlcwpkZW5vIGNoZWNrIHNyYy8qKi8qLnRzIHNyYy8qKi8qLnRzeApgYGAKCiMjIFByb2plY3QgU3RydWN0dXJlCgpgYGAKc2xpY2VzLmpzb24gICAgICAgICAgICAgICMgU2xpY2VzIGNvbmZpZ3VyYXRpb24gZmlsZQpsZXhpY29ucy8gICAgICAgICAgICAgICAgIyBBVCBQcm90b2NvbCBsZXhpY29uIGRlZmluaXRpb25zCnNyYy8K4pSc4pSA4pSAIG1haW4udHMgICAgICAgICAgICAgICMgU2VydmVyIGVudHJ5IHBvaW50CuKUnOKUgOKUgCBjb25maWcudHMgICAgICAgICAgICAjIE9BdXRoICYgc2Vzc2lvbiBjb25maWd1cmF0aW9uCuKUnOKUgOKUgCBnZW5lcmF0ZWRfY2xpZW50LnRzICAjIEdlbmVyYXRlZCBUeXBlU2NyaXB0IGNsaWVudCBmcm9tIGxleGljb25zCuKUnOKUgOKUgCByb3V0ZXMvICAgICAgICAgICAgICAjIFJvdXRlIGRlZmluaXRpb25zCuKUnOKUgOKUgCBmZWF0dXJlcy8gICAgICAgICAgICAjIEZlYXR1cmUgbW9kdWxlcwrilIIgICDilJTilIDilIAgYXV0aC8gICAgICAgICAgICMgQXV0aGVudGljYXRpb24K4pSc4pSA4pSAIHNoYXJlZC9mcmFnbWVudHMvICAgICMgUmV1c2FibGUgVUkgY29tcG9uZW50cwrilJTilIDilIAgdXRpbHMvICAgICAgICAgICAgICAjIFV0aWxpdHkgZnVuY3Rpb25zCmBgYAoKIyMgT0F1dGggU2V0dXAKCllvdXIgT0F1dGggYXBwbGljYXRpb24gd2FzIGF1dG9tYXRpY2FsbHkgY3JlYXRlZCBkdXJpbmcgcHJvamVjdCBpbml0aWFsaXphdGlvbgp3aXRoOgoKLSAqKkNsaWVudCBJRCAmIFNlY3JldCoqOiBBbHJlYWR5IGNvbmZpZ3VyZWQgaW4gYC5lbnZgCi0gKipSZWRpcmVjdCBVUkkqKjogYGh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9vYXV0aC9jYWxsYmFja2AKLSAqKlNsaWNlKio6IEF1dG9tYXRpY2FsbHkgY3JlYXRlZCBhbmQgbGlua2VkCgpUbyBtYW5hZ2UgeW91ciBPQXV0aCBjbGllbnRzIG9yIGNyZWF0ZSBhZGRpdGlvbmFsIG9uZXM6CgoxLiBWaXNpdCBbU2xpY2VzIE5ldHdvcmtdKGh0dHBzOi8vc2xpY2VzLm5ldHdvcmspCjIuIFVzZSB0aGUgYHNsaWNlcyBsb2dpbmAgQ0xJIGNvbW1hbmQKCiMjIERvY3VtZW50YXRpb24KCi0gYENMQVVERS5tZGAgLSBBcmNoaXRlY3R1cmUgZ3VpZGUgZm9yIEFJIGFzc2lzdGFuY2UKLSBGZWF0dXJlIGRpcmVjdG9yaWVzIGNvbnRhaW4gaGFuZGxlcnMgYW5kIHRlbXBsYXRlcwotIENvbXBvbmVudHMgdXNlIFByZWFjdCB3aXRoIHNlcnZlci1zaWRlIHJlbmRlcmluZwotIEhUTVggcHJvdmlkZXMgaW50ZXJhY3RpdmUgYmVoYXZpb3Igd2l0aG91dCBwYWdlIHJlbG9hZHMKCiMjIExpY2Vuc2UKCk1JVAo=" 17 }, 18 { 19 "path": ".gitignore", 20 "content": "LmVudioKbm9kZV9tb2R1bGVzCiouZGIqCg==" 21 }, 22 { 23 "path": ".env.example", 24 "content": "IyBPQXV0aCBDb25maWd1cmF0aW9uIChyZXF1aXJlZCkKT0FVVEhfQ0xJRU5UX0lEPXlvdXJfb2F1dGhfY2xpZW50X2lkCk9BVVRIX0NMSUVOVF9TRUNSRVQ9eW91cl9vYXV0aF9jbGllbnRfc2VjcmV0Ck9BVVRIX1JFRElSRUNUX1VSST1odHRwOi8vbG9jYWxob3N0OjgwODAvb2F1dGgvY2FsbGJhY2sKT0FVVEhfQUlQX0JBU0VfVVJMPWh0dHBzOi8vYXV0aC5zbGljZXMubmV0d29yawoKIyBBUEkgQ29uZmlndXJhdGlvbiAocmVxdWlyZWQpCkFQSV9VUkw9aHR0cHM6Ly9hcGkuc2xpY2VzLm5ldHdvcmsKU0xJQ0VfVVJJPWF0Oi8vZGlkOnBsYzpiY2dsdHpxYXp3NXRiNmsyZzN0dGVuYmovbmV0d29yay5zbGljZXMuc2xpY2UvM2x6Ynp1bWNtdm8yegoKIyBEYXRhYmFzZSAob3B0aW9uYWwsIGRlZmF1bHRzIHRvIHNsaWNlcy5kYikKREFUQUJBU0VfVVJMPXNsaWNlcy5kYgoKIyBFbnZpcm9ubWVudCAob3B0aW9uYWwsIGFmZmVjdHMgY29va2llIHNlY3VyaXR5KQpERU5PX0VOVj1kZXZlbG9wbWVudAoKIyBTZXJ2ZXIgKG9wdGlvbmFsLCBkZWZhdWx0cyB0byA4MDgwKQpQT1JUPTgwODA=" 25 }, 26 { 27 "path": "CLAUDE.md", 28 "content": "IyBDTEFVREUubWQKClRoaXMgZmlsZSBwcm92aWRlcyBndWlkYW5jZSB0byBDbGF1ZGUgQ29kZSAoY2xhdWRlLmFpL2NvZGUpIHdoZW4gd29ya2luZyB3aXRoCmNvZGUgaW4gdGhpcyByZXBvc2l0b3J5LgoKIyMgRGV2ZWxvcG1lbnQgQ29tbWFuZHMKCmBgYGJhc2gKIyBTdGFydCBkZXZlbG9wbWVudCBzZXJ2ZXIgd2l0aCBob3QgcmVsb2FkCmRlbm8gdGFzayBkZXYKCiMgU3RhcnQgcHJvZHVjdGlvbiBzZXJ2ZXIKZGVubyB0YXNrIHN0YXJ0CgojIEZvcm1hdCBjb2RlCmRlbm8gZm10CgojIENoZWNrIHR5cGVzCmRlbm8gY2hlY2sgc3JjLyoqLyoudHMgc3JjLyoqLyoudHN4CmBgYAoKIyMgQXJjaGl0ZWN0dXJlIE92ZXJ2aWV3CgpUaGlzIGlzIGEgRGVuby1iYXNlZCB3ZWIgYXBwbGljYXRpb24gYnVpbHQgd2l0aCB0aGUgU2xpY2VzIENMSS4gSXQgcHJvdmlkZXMKc2VydmVyLXNpZGUgcmVuZGVyaW5nIHdpdGggUHJlYWN0LCBPQXV0aCBhdXRoZW50aWNhdGlvbiwgYW5kIEFUIFByb3RvY29sCmludGVncmF0aW9uIGZvciBidWlsZGluZyBhcHBsaWNhdGlvbnMgb24gdGhlIGRlY2VudHJhbGl6ZWQgd2ViLgoKIyMjIFRlY2hub2xvZ3kgU3RhY2sKCi0gKipSdW50aW1lKio6IERlbm8gd2l0aCBUeXBlU2NyaXB0Ci0gKipGcm9udGVuZCoqOiBQcmVhY3Qgd2l0aCBzZXJ2ZXItc2lkZSByZW5kZXJpbmcKLSAqKlN0eWxpbmcqKjogVGFpbHdpbmQgQ1NTICh2aWEgQ0ROKQotICoqSW50ZXJhY3Rpdml0eSoqOiBIVE1YICsgSHlwZXJzY3JpcHQKLSAqKlJvdXRpbmcqKjogRGVubydzIHN0YW5kYXJkIEhUVFAgcm91dGluZwotICoqQXV0aGVudGljYXRpb24qKjogT0F1dGggd2l0aCBQS0NFIGZsb3cgdXNpbmcgYEBzbGljZXMvb2F1dGhgCi0gKipTZXNzaW9ucyoqOiBTUUxpdGUtYmFzZWQgd2l0aCBgQHNsaWNlcy9zZXNzaW9uYAotICoqRGF0YWJhc2UqKjogU1FMaXRlIHZpYSBPQXV0aCBhbmQgc2Vzc2lvbiBsaWJyYXJpZXMKCiMjIyBDb3JlIEFyY2hpdGVjdHVyZSBQYXR0ZXJucwoKIyMjIyBGZWF0dXJlLUJhc2VkIE9yZ2FuaXphdGlvbgoKVGhlIGNvZGViYXNlIGlzIG9yZ2FuaXplZCBieSBmZWF0dXJlcyByYXRoZXIgdGhhbiB0ZWNobmljYWwgbGF5ZXJzOgoKYGBgCnNyYy8K4pSc4pSA4pSAIGZlYXR1cmVzLyAgICAgICAgICAgIyBGZWF0dXJlIG1vZHVsZXMK4pSCICAg4pSU4pSA4pSAIGF1dGgvICAgICAgICAgICMgQXV0aGVudGljYXRpb24gKGxvZ2luL2xvZ291dCkK4pSc4pSA4pSAIHNoYXJlZC8gICAgICAgICAgICAjIFNoYXJlZCBVSSBjb21wb25lbnRzCuKUnOKUgOKUgCByb3V0ZXMvICAgICAgICAgICAgIyBSb3V0ZSBkZWZpbml0aW9ucyBhbmQgbWlkZGxld2FyZQrilJzilIDilIAgdXRpbHMvICAgICAgICAgICAgICMgVXRpbGl0eSBmdW5jdGlvbnMK4pSU4pSA4pSAIGNvbmZpZy50cyAgICAgICAgICAjIENvcmUgY29uZmlndXJhdGlvbgpgYGAKCiMjIyMgSGFuZGxlciBQYXR0ZXJuCgpFYWNoIGZlYXR1cmUgZm9sbG93cyBhIGNvbnNpc3RlbnQgcGF0dGVybjoKCi0gYGhhbmRsZXJzLnRzeGAgLSBSb3V0ZSBoYW5kbGVycyB0aGF0IHJldHVybiBSZXNwb25zZSBvYmplY3RzCi0gYHRlbXBsYXRlcy9gIC0gUHJlYWN0IGNvbXBvbmVudHMgZm9yIHJlbmRlcmluZwotIGB0ZW1wbGF0ZXMvZnJhZ21lbnRzL2AgLSBSZXVzYWJsZSBVSSBjb21wb25lbnRzCgojIyMjIEF1dGhlbnRpY2F0aW9uICYgU2Vzc2lvbnMKCi0gT0F1dGggaW50ZWdyYXRpb24gd2l0aCBBVCBQcm90b2NvbCB1c2luZyBgQHNsaWNlcy9vYXV0aGAKLSBQS0NFIGZsb3cgZm9yIHNlY3VyZSBhdXRoZW50aWNhdGlvbgotIFNlc3Npb24gbWFuYWdlbWVudCB3aXRoIGBAc2xpY2VzL3Nlc3Npb25gCi0gU1FMaXRlIHN0b3JhZ2UgZm9yIE9BdXRoIHN0YXRlIGFuZCBzZXNzaW9ucwotIEF1dG9tYXRpYyB0b2tlbiByZWZyZXNoIGNhcGFiaWxpdGllcwoKIyMjIEtleSBDb21wb25lbnRzCgojIyMjIFJvdXRlIFN5c3RlbQoKLSBBbGwgcm91dGVzIGRlZmluZWQgaW4gYHNyYy9yb3V0ZXMvbW9kLnRzYAotIEZlYXR1cmUgcm91dGVzIGV4cG9ydGVkIGZyb20gYHNyYy9mZWF0dXJlcy8qL2hhbmRsZXJzLnRzeGAKLSBNaWRkbGV3YXJlIGluIGBzcmMvcm91dGVzL21pZGRsZXdhcmUudHNgIGhhbmRsZXMgYXV0aCBzdGF0ZQoKIyMjIyBPQXV0aCBJbnRlZ3JhdGlvbgoKLSBgc3JjL2NvbmZpZy50c2AgLSBPQXV0aCBjbGllbnQgYW5kIHNlc3Npb24gc3RvcmUgc2V0dXAKLSBFbnZpcm9ubWVudCB2YXJpYWJsZXMgcmVxdWlyZWQ6IGBPQVVUSF9DTElFTlRfSURgLCBgT0FVVEhfQ0xJRU5UX1NFQ1JFVGAsCiAgYE9BVVRIX1JFRElSRUNUX1VSSWAsIGBPQVVUSF9BSVBfQkFTRV9VUkxgLCBgQVBJX1VSTGAsIGBTTElDRV9VUklgCi0gUEtDRSBmbG93IGltcGxlbWVudGF0aW9uIGluIGF1dGggaGFuZGxlcnMKLSBTUUxpdGUgc3RvcmFnZSBmb3IgT0F1dGggc3RhdGUgYW5kIHRva2VucwoKIyMjIyBSZW5kZXJpbmcgU3lzdGVtCgotIGBzcmMvdXRpbHMvcmVuZGVyLnRzeGAgLSBVbmlmaWVkIEhUTUwgcmVuZGVyaW5nIHdpdGggcHJvcGVyIGhlYWRlcnMKLSBTZXJ2ZXItc2lkZSByZW5kZXJpbmcgd2l0aCBQcmVhY3QKLSBIVE1YIGZvciBkeW5hbWljIGludGVyYWN0aW9ucyB3aXRob3V0IHBhZ2UgcmVsb2FkcwotIFNoYXJlZCBgTGF5b3V0YCBjb21wb25lbnQgaW4gYHNyYy9zaGFyZWQvZnJhZ21lbnRzL0xheW91dC50c3hgCgojIyMgRGV2ZWxvcG1lbnQgR3VpZGVsaW5lcwoKIyMjIyBDb21wb25lbnQgQ29udmVudGlvbnMKCi0gVXNlIGAudHN4YCBleHRlbnNpb24gZm9yIGNvbXBvbmVudHMgd2l0aCBKU1gKLSBQcmVhY3QgY29tcG9uZW50cyBmb3IgYWxsIFVJIHJlbmRlcmluZwotIEhUTVggYXR0cmlidXRlcyBmb3IgaW50ZXJhY3RpdmUgYmVoYXZpb3IKLSBUYWlsd2luZCBjbGFzc2VzIGZvciBzdHlsaW5nCgojIyMjIEZlYXR1cmUgRGV2ZWxvcG1lbnQKCldoZW4gYWRkaW5nIG5ldyBmZWF0dXJlczoKCjEuIENyZWF0ZSBmZWF0dXJlIGRpcmVjdG9yeSB1bmRlciBgc3JjL2ZlYXR1cmVzL2AKMi4gQWRkIGBoYW5kbGVycy50c3hgIHdpdGggcm91dGUgZGVmaW5pdGlvbnMKMy4gQ3JlYXRlIGB0ZW1wbGF0ZXMvYCBkaXJlY3Rvcnkgd2l0aCBQcmVhY3QgY29tcG9uZW50cwo0LiBFeHBvcnQgcm91dGVzIGZyb20gZmVhdHVyZSBhbmQgYWRkIHRvIGBzcmMvcm91dGVzL21vZC50c2AKNS4gRm9sbG93IGV4aXN0aW5nIGF1dGhlbnRpY2F0aW9uIHBhdHRlcm5zIHVzaW5nIGF1dGggbWlkZGxld2FyZQoKIyMjIyBFbnZpcm9ubWVudCBTZXR1cAoKVGhlIGFwcGxpY2F0aW9uIHJlcXVpcmVzIGEgYC5lbnZgIGZpbGUgd2l0aCBPQXV0aCBhbmQgQVBJIGNvbmZpZ3VyYXRpb24uCkNvcHkgYC5lbnYuZXhhbXBsZWAgYW5kIGZpbGwgaW4geW91ciB2YWx1ZXMuIE1pc3NpbmcgZW52aXJvbm1lbnQgdmFyaWFibGVzCndpbGwgY2F1c2Ugc3RhcnR1cCBmYWlsdXJlcyB3aXRoIGRlc2NyaXB0aXZlIGVycm9yIG1lc3NhZ2VzLgoKIyMjIFJlcXVlc3QvUmVzcG9uc2UgRmxvdwoKMS4gUmVxdWVzdCBoaXRzIG1haW4gc2VydmVyIGluIGBzcmMvbWFpbi50c2AKMi4gUm91dGVzIHByb2Nlc3NlZCB0aHJvdWdoIGBzcmMvcm91dGVzL21vZC50c2AKMy4gQXV0aGVudGljYXRpb24gbWlkZGxld2FyZSBhcHBsaWVzIHNlc3Npb24gc3RhdGUKNC4gRmVhdHVyZSBoYW5kbGVycyBwcm9jZXNzIHJlcXVlc3RzIGFuZCByZXR1cm4gcmVuZGVyZWQgSFRNTAo1LiBIVE1YIGhhbmRsZXMgcGFydGlhbCBwYWdlIHVwZGF0ZXMgb24gY2xpZW50LXNpZGUgaW50ZXJhY3Rpb25zCgojIyMgT0F1dGggRmxvdwoKMS4gVXNlciBpbml0aWF0ZXMgbG9naW4gd2l0aCBoYW5kbGUvaWRlbnRpZmllcgoyLiBPQXV0aCBjbGllbnQgZ2VuZXJhdGVzIFBLQ0UgY2hhbGxlbmdlIGFuZCByZWRpcmVjdHMgdG8gYXV0aCBzZXJ2ZXIKMy4gVXNlciBhdXRoZW50aWNhdGVzIGFuZCBpcyByZWRpcmVjdGVkIGJhY2sgd2l0aCBhdXRob3JpemF0aW9uIGNvZGUKNC4gQ2xpZW50IGV4Y2hhbmdlcyBjb2RlIGZvciB0b2tlbnMgdXNpbmcgUEtDRSB2ZXJpZmllcgo1LiBTZXNzaW9uIGNyZWF0ZWQgd2l0aCBhdXRvbWF0aWMgdG9rZW4gcmVmcmVzaAo2LiBQcm90ZWN0ZWQgcm91dGVzIGFjY2VzcyB1c2VyIGRhdGEgdGhyb3VnaCBhdXRoZW50aWNhdGVkIGNsaWVudAoKIyMjIEFkZGluZyBOZXcgRmVhdHVyZXMKClRvIGFkZCBhIG5ldyBmZWF0dXJlOgoKMS4gQ3JlYXRlIGBzcmMvZmVhdHVyZXMvZmVhdHVyZS1uYW1lL2AKMi4gQWRkIGBoYW5kbGVycy50c3hgIHdpdGggcm91dGUgaGFuZGxlcnMKMy4gQ3JlYXRlIGB0ZW1wbGF0ZXMvYCBkaXJlY3RvcnkgZm9yIFVJIGNvbXBvbmVudHMKNC4gRXhwb3J0IHJvdXRlcyBhbmQgYWRkIHRvIG1haW4gcm91dGVyCjUuIFVzZSBleGlzdGluZyBwYXR0ZXJucyBmb3IgYXV0aGVudGljYXRpb24gYW5kIHJlbmRlcmluZw==" 29 }, 30 { 31 "path": "src/main.ts", 32 "content": "aW1wb3J0IHsgcm91dGUgfSBmcm9tICJAc3RkL2h0dHAvdW5zdGFibGUtcm91dGUiOwppbXBvcnQgeyBhbGxSb3V0ZXMgfSBmcm9tICIuL3JvdXRlcy9tb2QudHMiOwppbXBvcnQgeyBjcmVhdGVMb2dnaW5nSGFuZGxlciB9IGZyb20gIi4vdXRpbHMvbG9nZ2luZy50cyI7CgpmdW5jdGlvbiBkZWZhdWx0SGFuZGxlcihyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgcmV0dXJuIFByb21pc2UucmVzb2x2ZShSZXNwb25zZS5yZWRpcmVjdChuZXcgVVJMKCIvIiwgcmVxLnVybCksIDMwMikpOwp9Cgpjb25zdCBoYW5kbGVyID0gY3JlYXRlTG9nZ2luZ0hhbmRsZXIocm91dGUoYWxsUm91dGVzLCBkZWZhdWx0SGFuZGxlcikpOwoKRGVuby5zZXJ2ZSgKICB7CiAgICBwb3J0OiBwYXJzZUludChEZW5vLmVudi5nZXQoIlBPUlQiKSB8fCAiODA4MCIpLAogICAgaG9zdG5hbWU6ICIwLjAuMC4wIiwKICAgIG9uTGlzdGVuOiAoeyBwb3J0LCBob3N0bmFtZSB9KSA9PgogICAgICBjb25zb2xlLmxvZyhg8J+agCBTZXJ2ZXIgcnVubmluZyBvbiBodHRwOi8vJHtob3N0bmFtZX06JHtwb3J0fWApLAogIH0sCiAgaGFuZGxlciwKKTs=" 33 }, 34 { 35 "path": "src/features/auth/templates/LoginPage.tsx", 36 "content": "aW1wb3J0IHsgTGF5b3V0IH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9MYXlvdXQudHN4IjsKaW1wb3J0IHsgQnV0dG9uIH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9CdXR0b24udHN4IjsKaW1wb3J0IHsgSW5wdXQgfSBmcm9tICIuLi8uLi8uLi9zaGFyZWQvZnJhZ21lbnRzL0lucHV0LnRzeCI7CgppbnRlcmZhY2UgTG9naW5QYWdlUHJvcHMgewogIGVycm9yPzogc3RyaW5nOwp9CgpleHBvcnQgZnVuY3Rpb24gTG9naW5QYWdlKHsgZXJyb3IgfTogTG9naW5QYWdlUHJvcHMpIHsKICByZXR1cm4gKAogICAgPExheW91dCB0aXRsZT0iTG9naW4iPgogICAgICA8ZGl2IGNsYXNzTmFtZT0ibWluLWgtc2NyZWVuIGZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktY2VudGVyIGJnLWdyYXktNTAiPgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJtYXgtdy1tZCB3LWZ1bGwgc3BhY2UteS04Ij4KICAgICAgICAgIDxkaXY+CiAgICAgICAgICAgIDxoMiBjbGFzc05hbWU9Im10LTYgdGV4dC1jZW50ZXIgdGV4dC0zeGwgZm9udC1leHRyYWJvbGQgdGV4dC1ncmF5LTkwMCI+CiAgICAgICAgICAgICAgU2lnbiBpbiB0byB5b3VyIGFjY291bnQKICAgICAgICAgICAgPC9oMj4KICAgICAgICAgICAgPHAgY2xhc3NOYW1lPSJtdC0yIHRleHQtY2VudGVyIHRleHQtc20gdGV4dC1ncmF5LTYwMCI+CiAgICAgICAgICAgICAgVXNlIHlvdXIgQVQgUHJvdG9jb2wgaGFuZGxlIG9yIERJRAogICAgICAgICAgICA8L3A+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICB7ZXJyb3IgJiYgKAogICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0iYmctcmVkLTUwIGJvcmRlciBib3JkZXItcmVkLTIwMCB0ZXh0LXJlZC03MDAgcHgtNCBweS0zIHJvdW5kZWQiPgogICAgICAgICAgICAgIHtlcnJvciA9PT0gIk9BdXRoIGluaXRpYWxpemF0aW9uIGZhaWxlZCIgJiYgIkZhaWxlZCB0byBzdGFydCBhdXRoZW50aWNhdGlvbiJ9CiAgICAgICAgICAgICAge2Vycm9yID09PSAiSW52YWxpZCBPQXV0aCBjYWxsYmFjayIgJiYgIkF1dGhlbnRpY2F0aW9uIGNhbGxiYWNrIGZhaWxlZCJ9CiAgICAgICAgICAgICAge2Vycm9yID09PSAiQXV0aGVudGljYXRpb24gZmFpbGVkIiAmJiAiQXV0aGVudGljYXRpb24gZmFpbGVkIn0KICAgICAgICAgICAgICB7ZXJyb3IgPT09ICJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24iICYmICJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24ifQogICAgICAgICAgICAgIHshWyJPQXV0aCBpbml0aWFsaXphdGlvbiBmYWlsZWQiLCAiSW52YWxpZCBPQXV0aCBjYWxsYmFjayIsICJBdXRoZW50aWNhdGlvbiBmYWlsZWQiLCAiRmFpbGVkIHRvIGNyZWF0ZSBzZXNzaW9uIl0uaW5jbHVkZXMoZXJyb3IpICYmIGVycm9yfQogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICl9CgogICAgICAgICAgPGZvcm0gY2xhc3NOYW1lPSJtdC04IHNwYWNlLXktNiIgYWN0aW9uPSIvb2F1dGgvYXV0aG9yaXplIiBtZXRob2Q9InBvc3QiPgogICAgICAgICAgICA8ZGl2PgogICAgICAgICAgICAgIDxsYWJlbCBodG1sRm9yPSJsb2dpbkhpbnQiIGNsYXNzTmFtZT0iYmxvY2sgdGV4dC1zbSBmb250LW1lZGl1bSB0ZXh0LWdyYXktNzAwIj4KICAgICAgICAgICAgICAgIEhhbmRsZSBvciBESUQKICAgICAgICAgICAgICA8L2xhYmVsPgogICAgICAgICAgICAgIDxJbnB1dAogICAgICAgICAgICAgICAgaWQ9ImxvZ2luSGludCIKICAgICAgICAgICAgICAgIG5hbWU9ImxvZ2luSGludCIKICAgICAgICAgICAgICAgIHR5cGU9InRleHQiCiAgICAgICAgICAgICAgICByZXF1aXJlZAogICAgICAgICAgICAgICAgcGxhY2Vob2xkZXI9ImFsaWNlLmJza3kuc29jaWFsIG9yIGRpZDpwbGM6Li4uIgogICAgICAgICAgICAgICAgY2xhc3NOYW1lPSJtdC0xIgogICAgICAgICAgICAgIC8+CiAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgPEJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzTmFtZT0idy1mdWxsIj4KICAgICAgICAgICAgICBTaWduIGluCiAgICAgICAgICAgIDwvQnV0dG9uPgogICAgICAgICAgPC9mb3JtPgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvTGF5b3V0PgogICk7Cn0=" 37 }, 38 { 39 "path": "src/features/auth/handlers.tsx", 40 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IHdpdGhBdXRoIH0gZnJvbSAiLi4vLi4vcm91dGVzL21pZGRsZXdhcmUudHMiOwppbXBvcnQgeyBPQXV0aENsaWVudCB9IGZyb20gIkBzbGljZXMvb2F1dGgiOwppbXBvcnQgewogIGNyZWF0ZU9BdXRoQ2xpZW50LAogIGNyZWF0ZVNlc3Npb25DbGllbnQsCiAgb2F1dGhDb25maWcsCiAgb2F1dGhTdG9yYWdlLAogIG9hdXRoU2Vzc2lvbnMsCiAgc2Vzc2lvblN0b3JlLAp9IGZyb20gIi4uLy4uL2NvbmZpZy50cyI7CmltcG9ydCB7IHJlbmRlckhUTUwgfSBmcm9tICIuLi8uLi91dGlscy9yZW5kZXIudHN4IjsKaW1wb3J0IHsgTG9naW5QYWdlIH0gZnJvbSAiLi90ZW1wbGF0ZXMvTG9naW5QYWdlLnRzeCI7Cgphc3luYyBmdW5jdGlvbiBoYW5kbGVMb2dpblBhZ2UocmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxSZXNwb25zZT4gewogIGNvbnN0IGNvbnRleHQgPSBhd2FpdCB3aXRoQXV0aChyZXEpOwogIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybCk7CgogIC8vIFJlZGlyZWN0IGlmIGFscmVhZHkgbG9nZ2VkIGluCiAgaWYgKGNvbnRleHQuY3VycmVudFVzZXIpIHsKICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdChuZXcgVVJMKCIvZGFzaGJvYXJkIiwgcmVxLnVybCksIDMwMik7CiAgfQoKICBjb25zdCBlcnJvciA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJlcnJvciIpOwogIHJldHVybiByZW5kZXJIVE1MKDxMb2dpblBhZ2UgZXJyb3I9e2Vycm9yIHx8IHVuZGVmaW5lZH0gLz4pOwp9Cgphc3luYyBmdW5jdGlvbiBoYW5kbGVPQXV0aEF1dGhvcml6ZShyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgdHJ5IHsKICAgIGNvbnN0IGZvcm1EYXRhID0gYXdhaXQgcmVxLmZvcm1EYXRhKCk7CiAgICBjb25zdCBsb2dpbkhpbnQgPSBmb3JtRGF0YS5nZXQoImxvZ2luSGludCIpIGFzIHN0cmluZzsKCiAgICBpZiAoIWxvZ2luSGludCkgewogICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKCJNaXNzaW5nIGxvZ2luIGhpbnQiLCB7IHN0YXR1czogNDAwIH0pOwogICAgfQoKICAgIGNvbnN0IHRlbXBPQXV0aENsaWVudCA9IG5ldyBPQXV0aENsaWVudCgKICAgICAgb2F1dGhDb25maWcsCiAgICAgIG9hdXRoU3RvcmFnZSwKICAgICAgbG9naW5IaW50CiAgICApOwogICAgY29uc3QgYXV0aFJlc3VsdCA9IGF3YWl0IHRlbXBPQXV0aENsaWVudC5hdXRob3JpemUoewogICAgICBsb2dpbkhpbnQsCiAgICB9KTsKCiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QoYXV0aFJlc3VsdC5hdXRob3JpemF0aW9uVXJsLCAzMDIpOwogIH0gY2F0Y2ggKGVycm9yKSB7CiAgICBjb25zb2xlLmVycm9yKCJPQXV0aCBhdXRob3JpemUgZXJyb3I6IiwgZXJyb3IpOwoKICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdCgKICAgICAgbmV3IFVSTCgKICAgICAgICAiL2xvZ2luP2Vycm9yPSIgKwogICAgICAgICAgZW5jb2RlVVJJQ29tcG9uZW50KCJQbGVhc2UgY2hlY2sgeW91ciBoYW5kbGUgYW5kIHRyeSBhZ2Fpbi4iKSwKICAgICAgICByZXEudXJsCiAgICAgICksCiAgICAgIDMwMgogICAgKTsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIGhhbmRsZU9BdXRoQ2FsbGJhY2socmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxSZXNwb25zZT4gewogIHRyeSB7CiAgICBjb25zdCB1cmwgPSBuZXcgVVJMKHJlcS51cmwpOwogICAgY29uc3QgY29kZSA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJjb2RlIik7CiAgICBjb25zdCBzdGF0ZSA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJzdGF0ZSIpOwoKICAgIGlmICghY29kZSB8fCAhc3RhdGUpIHsKICAgICAgcmV0dXJuIFJlc3BvbnNlLnJlZGlyZWN0KAogICAgICAgIG5ldyBVUkwoCiAgICAgICAgICAiL2xvZ2luP2Vycm9yPSIgKyBlbmNvZGVVUklDb21wb25lbnQoIkludmFsaWQgT0F1dGggY2FsbGJhY2siKSwKICAgICAgICAgIHJlcS51cmwKICAgICAgICApLAogICAgICAgIDMwMgogICAgICApOwogICAgfQoKICAgIGNvbnN0IHRlbXBPQXV0aENsaWVudCA9IG5ldyBPQXV0aENsaWVudChvYXV0aENvbmZpZywgb2F1dGhTdG9yYWdlLCAidGVtcCIpOwogICAgY29uc3QgdG9rZW5zID0gYXdhaXQgdGVtcE9BdXRoQ2xpZW50LmhhbmRsZUNhbGxiYWNrKHsgY29kZSwgc3RhdGUgfSk7CiAgICBjb25zdCBzZXNzaW9uSWQgPSBhd2FpdCBvYXV0aFNlc3Npb25zLmNyZWF0ZU9BdXRoU2Vzc2lvbih0b2tlbnMpOwoKICAgIGlmICghc2Vzc2lvbklkKSB7CiAgICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdCgKICAgICAgICBuZXcgVVJMKAogICAgICAgICAgIi9sb2dpbj9lcnJvcj0iICsgZW5jb2RlVVJJQ29tcG9uZW50KCJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24iKSwKICAgICAgICAgIHJlcS51cmwKICAgICAgICApLAogICAgICAgIDMwMgogICAgICApOwogICAgfQoKICAgIGNvbnN0IHNlc3Npb25Db29raWUgPSBzZXNzaW9uU3RvcmUuY3JlYXRlU2Vzc2lvbkNvb2tpZShzZXNzaW9uSWQpOwoKICAgIGxldCB1c2VySW5mbzsKICAgIHRyeSB7CiAgICAgIGNvbnN0IHNlc3Npb25PQXV0aENsaWVudCA9IGNyZWF0ZU9BdXRoQ2xpZW50KHNlc3Npb25JZCk7CiAgICAgIHVzZXJJbmZvID0gYXdhaXQgc2Vzc2lvbk9BdXRoQ2xpZW50LmdldFVzZXJJbmZvKCk7CiAgICB9IGNhdGNoIChlcnJvcikgewogICAgICBjb25zb2xlLmVycm9yKCJGYWlsZWQgdG8gZ2V0IHVzZXIgaW5mbzoiLCBlcnJvcik7CiAgICB9CgogICAgaWYgKHVzZXJJbmZvPy5zdWIpIHsKICAgICAgdHJ5IHsKICAgICAgICBjb25zdCB1c2VyQ2xpZW50ID0gY3JlYXRlU2Vzc2lvbkNsaWVudChzZXNzaW9uSWQpOwogICAgICAgIGF3YWl0IHVzZXJDbGllbnQuc3luY1VzZXJDb2xsZWN0aW9ucygpOwogICAgICAgIGNvbnNvbGUubG9nKCJTeW5jZWQgQmx1ZXNreSBwcm9maWxlIGZvciIsIHVzZXJJbmZvLnN1Yik7CiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc29sZS5lcnJvcigiRXJyb3Igc3luY2luZyBCbHVlc2t5IHByb2ZpbGU6IiwgZXJyb3IpOwogICAgICB9CiAgICB9CgogICAgcmV0dXJuIG5ldyBSZXNwb25zZShudWxsLCB7CiAgICAgIHN0YXR1czogMzAyLAogICAgICBoZWFkZXJzOiB7CiAgICAgICAgTG9jYXRpb246IG5ldyBVUkwoIi9kYXNoYm9hcmQiLCByZXEudXJsKS50b1N0cmluZygpLAogICAgICAgICJTZXQtQ29va2llIjogc2Vzc2lvbkNvb2tpZSwKICAgICAgfSwKICAgIH0pOwogIH0gY2F0Y2ggKGVycm9yKSB7CiAgICBjb25zb2xlLmVycm9yKCJPQXV0aCBjYWxsYmFjayBlcnJvcjoiLCBlcnJvcik7CiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QoCiAgICAgIG5ldyBVUkwoCiAgICAgICAgIi9sb2dpbj9lcnJvcj0iICsgZW5jb2RlVVJJQ29tcG9uZW50KCJBdXRoZW50aWNhdGlvbiBmYWlsZWQiKSwKICAgICAgICByZXEudXJsCiAgICAgICksCiAgICAgIDMwMgogICAgKTsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIGhhbmRsZUxvZ291dChyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgY29uc3Qgc2Vzc2lvbiA9IGF3YWl0IHNlc3Npb25TdG9yZS5nZXRTZXNzaW9uRnJvbVJlcXVlc3QocmVxKTsKCiAgaWYgKHNlc3Npb24pIHsKICAgIGF3YWl0IG9hdXRoU2Vzc2lvbnMubG9nb3V0KHNlc3Npb24uc2Vzc2lvbklkKTsKICB9CgogIGNvbnN0IGNsZWFyQ29va2llID0gc2Vzc2lvblN0b3JlLmNyZWF0ZUxvZ291dENvb2tpZSgpOwoKICByZXR1cm4gbmV3IFJlc3BvbnNlKG51bGwsIHsKICAgIHN0YXR1czogMzAyLAogICAgaGVhZGVyczogewogICAgICBMb2NhdGlvbjogbmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCkudG9TdHJpbmcoKSwKICAgICAgIlNldC1Db29raWUiOiBjbGVhckNvb2tpZSwKICAgIH0sCiAgfSk7Cn0KCmV4cG9ydCBjb25zdCBhdXRoUm91dGVzOiBSb3V0ZVtdID0gWwogIHsKICAgIG1ldGhvZDogIkdFVCIsCiAgICBwYXR0ZXJuOiBuZXcgVVJMUGF0dGVybih7IHBhdGhuYW1lOiAiL2xvZ2luIiB9KSwKICAgIGhhbmRsZXI6IGhhbmRsZUxvZ2luUGFnZSwKICB9LAogIHsKICAgIG1ldGhvZDogIlBPU1QiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi9vYXV0aC9hdXRob3JpemUiIH0pLAogICAgaGFuZGxlcjogaGFuZGxlT0F1dGhBdXRob3JpemUsCiAgfSwKICB7CiAgICBtZXRob2Q6ICJHRVQiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi9vYXV0aC9jYWxsYmFjayIgfSksCiAgICBoYW5kbGVyOiBoYW5kbGVPQXV0aENhbGxiYWNrLAogIH0sCiAgewogICAgbWV0aG9kOiAiUE9TVCIsCiAgICBwYXR0ZXJuOiBuZXcgVVJMUGF0dGVybih7IHBhdGhuYW1lOiAiL2xvZ291dCIgfSksCiAgICBoYW5kbGVyOiBoYW5kbGVMb2dvdXQsCiAgfSwKXTsK" 41 }, 42 { 43 "path": "src/features/dashboard/templates/DashboardPage.tsx", 44 "content": "aW1wb3J0IHsgTGF5b3V0IH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9MYXlvdXQudHN4IjsKaW1wb3J0IHsgQnV0dG9uIH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9CdXR0b24udHN4IjsKaW1wb3J0IHR5cGUgeyBBcHBCc2t5QWN0b3JQcm9maWxlIH0gZnJvbSAiLi4vLi4vLi4vZ2VuZXJhdGVkX2NsaWVudC50cyI7CgppbnRlcmZhY2UgRGFzaGJvYXJkUGFnZVByb3BzIHsKICBjdXJyZW50VXNlcjogewogICAgbmFtZT86IHN0cmluZzsKICAgIHN1Yjogc3RyaW5nOwogIH07CiAgcHJvZmlsZT86IEFwcEJza3lBY3RvclByb2ZpbGU7CiAgYXZhdGFyVXJsPzogc3RyaW5nOwp9CgpleHBvcnQgZnVuY3Rpb24gRGFzaGJvYXJkUGFnZSh7CiAgY3VycmVudFVzZXIsCiAgcHJvZmlsZSwKICBhdmF0YXJVcmwsCn06IERhc2hib2FyZFBhZ2VQcm9wcykgewogIHJldHVybiAoCiAgICA8TGF5b3V0IHRpdGxlPSJEYXNoYm9hcmQiPgogICAgICA8ZGl2IGNsYXNzTmFtZT0ibWluLWgtc2NyZWVuIGJnLWdyYXktNTAgcC04Ij4KICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ibWF4LXctMnhsIG14LWF1dG8iPgogICAgICAgICAgPGRpdiBjbGFzc05hbWU9ImJnLXdoaXRlIHJvdW5kZWQtbGcgc2hhZG93IHAtNiI+CiAgICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGp1c3RpZnktYmV0d2VlbiBpdGVtcy1jZW50ZXIgbWItNiI+CiAgICAgICAgICAgICAgPGgxIGNsYXNzTmFtZT0idGV4dC0yeGwgZm9udC1ib2xkIj5EYXNoYm9hcmQ8L2gxPgogICAgICAgICAgICAgIDxmb3JtIG1ldGhvZD0icG9zdCIgYWN0aW9uPSIvbG9nb3V0Ij4KICAgICAgICAgICAgICAgIDxCdXR0b24gdHlwZT0ic3VibWl0IiB2YXJpYW50PSJzZWNvbmRhcnkiPgogICAgICAgICAgICAgICAgICBMb2dvdXQKICAgICAgICAgICAgICAgIDwvQnV0dG9uPgogICAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ibWItNiI+CiAgICAgICAgICAgICAge2F2YXRhclVybCAmJiAoCiAgICAgICAgICAgICAgICA8aW1nCiAgICAgICAgICAgICAgICAgIHNyYz17YXZhdGFyVXJsfQogICAgICAgICAgICAgICAgICBhbHQ9IlByb2ZpbGUiCiAgICAgICAgICAgICAgICAgIGNsYXNzTmFtZT0idy0yMCBoLTIwIHJvdW5kZWQtZnVsbCBtYi00IgogICAgICAgICAgICAgICAgLz4KICAgICAgICAgICAgICApfQogICAgICAgICAgICAgIDxoMiBjbGFzc05hbWU9InRleHQteGwgZm9udC1zZW1pYm9sZCBtYi0yIj4KICAgICAgICAgICAgICAgIHtwcm9maWxlPy5kaXNwbGF5TmFtZSB8fCBjdXJyZW50VXNlci5uYW1lIHx8IGN1cnJlbnRVc2VyLnN1Yn0KICAgICAgICAgICAgICA8L2gyPgogICAgICAgICAgICAgIHtjdXJyZW50VXNlci5uYW1lICYmICgKICAgICAgICAgICAgICAgIDxwIGNsYXNzTmFtZT0idGV4dC1ncmF5LTYwMCBtYi0yIj5Ae2N1cnJlbnRVc2VyLm5hbWV9PC9wPgogICAgICAgICAgICAgICl9CiAgICAgICAgICAgICAge3Byb2ZpbGU/LmRlc2NyaXB0aW9uICYmICgKICAgICAgICAgICAgICAgIDxwIGNsYXNzTmFtZT0idGV4dC1ncmF5LTcwMCBtdC0yIj57cHJvZmlsZS5kZXNjcmlwdGlvbn08L3A+CiAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L0xheW91dD4KICApOwp9Cg==" 45 }, 46 { 47 "path": "src/features/dashboard/handlers.tsx", 48 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IHdpdGhBdXRoIH0gZnJvbSAiLi4vLi4vcm91dGVzL21pZGRsZXdhcmUudHMiOwppbXBvcnQgeyByZW5kZXJIVE1MIH0gZnJvbSAiLi4vLi4vdXRpbHMvcmVuZGVyLnRzeCI7CmltcG9ydCB7IERhc2hib2FyZFBhZ2UgfSBmcm9tICIuL3RlbXBsYXRlcy9EYXNoYm9hcmRQYWdlLnRzeCI7CmltcG9ydCB7IHB1YmxpY0NsaWVudCB9IGZyb20gIi4uLy4uL2NvbmZpZy50cyI7CmltcG9ydCB7IHJlY29yZEJsb2JUb0NkblVybCB9IGZyb20gIkBzbGljZXMvY2xpZW50IjsKaW1wb3J0IHsgQXBwQnNreUFjdG9yUHJvZmlsZSB9IGZyb20gIi4uLy4uL2dlbmVyYXRlZF9jbGllbnQudHMiOwoKYXN5bmMgZnVuY3Rpb24gaGFuZGxlRGFzaGJvYXJkKHJlcTogUmVxdWVzdCk6IFByb21pc2U8UmVzcG9uc2U+IHsKICBjb25zdCBjb250ZXh0ID0gYXdhaXQgd2l0aEF1dGgocmVxKTsKCiAgaWYgKCFjb250ZXh0LmN1cnJlbnRVc2VyKSB7CiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMik7CiAgfQoKICBsZXQgcHJvZmlsZTogQXBwQnNreUFjdG9yUHJvZmlsZSB8IHVuZGVmaW5lZDsKICBsZXQgYXZhdGFyVXJsOiBzdHJpbmcgfCB1bmRlZmluZWQ7CiAgdHJ5IHsKICAgIGNvbnN0IHByb2ZpbGVSZXN1bHQgPSBhd2FpdCBwdWJsaWNDbGllbnQuYXBwLmJza3kuYWN0b3IucHJvZmlsZS5nZXRSZWNvcmQoewogICAgICB1cmk6IGBhdDovLyR7Y29udGV4dC5jdXJyZW50VXNlci5zdWJ9L2FwcC5ic2t5LmFjdG9yLnByb2ZpbGUvc2VsZmAsCiAgICB9KTsKCiAgICBpZiAocHJvZmlsZVJlc3VsdCkgewogICAgICBwcm9maWxlID0gcHJvZmlsZVJlc3VsdC52YWx1ZTsKCiAgICAgIGlmIChwcm9maWxlLmF2YXRhcikgewogICAgICAgIGF2YXRhclVybCA9IHJlY29yZEJsb2JUb0NkblVybChwcm9maWxlUmVzdWx0LCBwcm9maWxlLmF2YXRhciwgImF2YXRhciIpOwogICAgICB9CiAgICB9CiAgfSBjYXRjaCAoZXJyb3IpIHsKICAgIGNvbnNvbGUuZXJyb3IoIkVycm9yIGZldGNoaW5nIHByb2ZpbGU6IiwgZXJyb3IpOwogIH0KCiAgcmV0dXJuIHJlbmRlckhUTUwoCiAgICA8RGFzaGJvYXJkUGFnZQogICAgICBjdXJyZW50VXNlcj17Y29udGV4dC5jdXJyZW50VXNlcn0KICAgICAgcHJvZmlsZT17cHJvZmlsZX0KICAgICAgYXZhdGFyVXJsPXthdmF0YXJVcmx9CiAgICAvPgogICk7Cn0KCmV4cG9ydCBjb25zdCBkYXNoYm9hcmRSb3V0ZXM6IFJvdXRlW10gPSBbCiAgewogICAgbWV0aG9kOiAiR0VUIiwKICAgIHBhdHRlcm46IG5ldyBVUkxQYXR0ZXJuKHsgcGF0aG5hbWU6ICIvZGFzaGJvYXJkIiB9KSwKICAgIGhhbmRsZXI6IGhhbmRsZURhc2hib2FyZCwKICB9LApdOwo=" 49 }, 50 { 51 "path": "src/utils/cn.ts", 52 "content": "aW1wb3J0IHsgdHlwZSBDbGFzc1ZhbHVlLCBjbHN4IH0gZnJvbSAiY2xzeCI7CmltcG9ydCB7IHR3TWVyZ2UgfSBmcm9tICJ0YWlsd2luZC1tZXJnZSI7CgpleHBvcnQgZnVuY3Rpb24gY24oLi4uaW5wdXRzOiBDbGFzc1ZhbHVlW10pOiBzdHJpbmcgewogIHJldHVybiB0d01lcmdlKGNsc3goaW5wdXRzKSk7Cn0=" 53 }, 54 { 55 "path": "src/utils/logging.ts", 56 "content": "aW1wb3J0IHsgY3lhbiwgZ3JlZW4sIHJlZCwgeWVsbG93LCBib2xkLCBkaW0gfSBmcm9tICJAc3RkL2ZtdC9jb2xvcnMiOwoKZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUxvZ2dpbmdIYW5kbGVyKAogIGhhbmRsZXI6IChyZXE6IFJlcXVlc3QpID0+IFJlc3BvbnNlIHwgUHJvbWlzZTxSZXNwb25zZT4KKSB7CiAgcmV0dXJuIGFzeW5jIChyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiA9PiB7CiAgICBjb25zdCBzdGFydCA9IERhdGUubm93KCk7CiAgICBjb25zdCBtZXRob2QgPSByZXEubWV0aG9kOwogICAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKTsKCiAgICB0cnkgewogICAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IFByb21pc2UucmVzb2x2ZShoYW5kbGVyKHJlcSkpOwogICAgICBjb25zdCBkdXJhdGlvbiA9IERhdGUubm93KCkgLSBzdGFydDsKCiAgICAgIGNvbnN0IG1ldGhvZENvbG9yID0gY3lhbihib2xkKG1ldGhvZCkpOwogICAgICBjb25zdCBzdGF0dXNDb2xvciA9CiAgICAgICAgcmVzcG9uc2Uuc3RhdHVzID49IDIwMCAmJiByZXNwb25zZS5zdGF0dXMgPCAzMDAKICAgICAgICAgID8gZ3JlZW4oU3RyaW5nKHJlc3BvbnNlLnN0YXR1cykpCiAgICAgICAgICA6IHJlc3BvbnNlLnN0YXR1cyA+PSAzMDAgJiYgcmVzcG9uc2Uuc3RhdHVzIDwgNDAwCiAgICAgICAgICA/IHllbGxvdyhTdHJpbmcocmVzcG9uc2Uuc3RhdHVzKSkKICAgICAgICAgIDogcmVzcG9uc2Uuc3RhdHVzID49IDQwMAogICAgICAgICAgPyByZWQoU3RyaW5nKHJlc3BvbnNlLnN0YXR1cykpCiAgICAgICAgICA6IFN0cmluZyhyZXNwb25zZS5zdGF0dXMpOwogICAgICBjb25zdCBkdXJhdGlvblRleHQgPSBkaW0oYCgke2R1cmF0aW9ufW1zKWApOwoKICAgICAgY29uc29sZS5sb2coCiAgICAgICAgYCR7bWV0aG9kQ29sb3J9ICR7dXJsLnBhdGhuYW1lfSAtICR7c3RhdHVzQ29sb3J9ICR7ZHVyYXRpb25UZXh0fWAKICAgICAgKTsKICAgICAgcmV0dXJuIHJlc3BvbnNlOwogICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgY29uc3QgZHVyYXRpb24gPSBEYXRlLm5vdygpIC0gc3RhcnQ7CiAgICAgIGNvbnN0IG1ldGhvZENvbG9yID0gY3lhbihib2xkKG1ldGhvZCkpOwogICAgICBjb25zdCBlcnJvclRleHQgPSByZWQoYm9sZCgiRVJST1IiKSk7CiAgICAgIGNvbnN0IGR1cmF0aW9uVGV4dCA9IGRpbShgKCR7ZHVyYXRpb259bXMpYCk7CgogICAgICBjb25zb2xlLmVycm9yKAogICAgICAgIGAke21ldGhvZENvbG9yfSAke3VybC5wYXRobmFtZX0gLSAke2Vycm9yVGV4dH0gJHtkdXJhdGlvblRleHR9OmAsCiAgICAgICAgZXJyb3IKICAgICAgKTsKICAgICAgdGhyb3cgZXJyb3I7CiAgICB9CiAgfTsKfQo=" 57 }, 58 { 59 "path": "src/utils/render.tsx", 60 "content": "aW1wb3J0IHsgcmVuZGVyVG9TdHJpbmcgfSBmcm9tICJwcmVhY3QtcmVuZGVyLXRvLXN0cmluZyI7CmltcG9ydCB7IFZOb2RlIH0gZnJvbSAicHJlYWN0IjsKCmV4cG9ydCBmdW5jdGlvbiByZW5kZXJIVE1MKGVsZW1lbnQ6IFZOb2RlKTogUmVzcG9uc2UgewogIGNvbnN0IGh0bWwgPSByZW5kZXJUb1N0cmluZyhlbGVtZW50KTsKCiAgcmV0dXJuIG5ldyBSZXNwb25zZShodG1sLCB7CiAgICBoZWFkZXJzOiB7CiAgICAgICJDb250ZW50LVR5cGUiOiAidGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04IiwKICAgIH0sCiAgfSk7Cn0=" 61 }, 62 { 63 "path": "src/shared/fragments/Layout.tsx", 64 "content": "aW1wb3J0IHsgQ29tcG9uZW50Q2hpbGRyZW4gfSBmcm9tICJwcmVhY3QiOwoKaW50ZXJmYWNlIExheW91dFByb3BzIHsKICB0aXRsZT86IHN0cmluZzsKICBjaGlsZHJlbjogQ29tcG9uZW50Q2hpbGRyZW47Cn0KCmV4cG9ydCBmdW5jdGlvbiBMYXlvdXQoeyB0aXRsZSA9ICJBcHAiLCBjaGlsZHJlbiB9OiBMYXlvdXRQcm9wcykgewogIHJldHVybiAoCiAgICA8aHRtbCBsYW5nPSJlbiI+CiAgICAgIDxoZWFkPgogICAgICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wIiAvPgogICAgICAgIDx0aXRsZT57dGl0bGV9PC90aXRsZT4KICAgICAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4udGFpbHdpbmRjc3MuY29tIj48L3NjcmlwdD4KICAgICAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly91bnBrZy5jb20vaHRteC5vcmdAMS45LjEwIj48L3NjcmlwdD4KICAgICAgPC9oZWFkPgogICAgICA8Ym9keT4KICAgICAgICB7Y2hpbGRyZW59CiAgICAgIDwvYm9keT4KICAgIDwvaHRtbD4KICApOwp9" 65 }, 66 { 67 "path": "src/shared/fragments/Button.tsx", 68 "content": "aW1wb3J0IHsgQ29tcG9uZW50Q2hpbGRyZW4sIEpTWCB9IGZyb20gInByZWFjdCI7CmltcG9ydCB7IGNuIH0gZnJvbSAiLi4vLi4vdXRpbHMvY24udHMiOwoKaW50ZXJmYWNlIEJ1dHRvblByb3BzIGV4dGVuZHMgT21pdDxKU1guSW50cmluc2ljRWxlbWVudHNbJ2J1dHRvbiddLCAic2l6ZSI+IHsKICBjaGlsZHJlbjogQ29tcG9uZW50Q2hpbGRyZW47CiAgdmFyaWFudD86ICJwcmltYXJ5IiB8ICJzZWNvbmRhcnkiIHwgImRhbmdlciI7CiAgc2l6ZT86ICJzbSIgfCAibWQiIHwgImxnIjsKfQoKZXhwb3J0IGZ1bmN0aW9uIEJ1dHRvbih7CiAgY2hpbGRyZW4sCiAgdHlwZSA9ICJidXR0b24iLAogIHZhcmlhbnQgPSAicHJpbWFyeSIsCiAgc2l6ZSA9ICJtZCIsCiAgY2xhc3NOYW1lLAogIGRpc2FibGVkLAogIC4uLnByb3BzCn06IEJ1dHRvblByb3BzKSB7CiAgY29uc3QgYmFzZUNsYXNzZXMgPQogICAgImlubGluZS1mbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWNlbnRlciBmb250LW1lZGl1bSByb3VuZGVkLW1kIGZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpyaW5nLTIgZm9jdXM6cmluZy1vZmZzZXQtMiBkaXNhYmxlZDpvcGFjaXR5LTUwIGRpc2FibGVkOmN1cnNvci1ub3QtYWxsb3dlZCI7CgogIGNvbnN0IHZhcmlhbnRDbGFzc2VzID0gewogICAgcHJpbWFyeTogImJnLWJsdWUtNjAwIGhvdmVyOmJnLWJsdWUtNzAwIHRleHQtd2hpdGUgZm9jdXM6cmluZy1ibHVlLTUwMCIsCiAgICBzZWNvbmRhcnk6CiAgICAgICJiZy1ncmF5LTIwMCBob3ZlcjpiZy1ncmF5LTMwMCB0ZXh0LWdyYXktOTAwIGZvY3VzOnJpbmctZ3JheS01MDAiLAogICAgZGFuZ2VyOiAiYmctcmVkLTYwMCBob3ZlcjpiZy1yZWQtNzAwIHRleHQtd2hpdGUgZm9jdXM6cmluZy1yZWQtNTAwIiwKICB9OwoKICBjb25zdCBzaXplQ2xhc3NlcyA9IHsKICAgIHNtOiAicHgtMyBweS0xLjUgdGV4dC1zbSIsCiAgICBtZDogInB4LTQgcHktMiB0ZXh0LXNtIiwKICAgIGxnOiAicHgtNiBweS0zIHRleHQtYmFzZSIsCiAgfTsKCiAgcmV0dXJuICgKICAgIDxidXR0b24KICAgICAgdHlwZT17dHlwZX0KICAgICAgZGlzYWJsZWQ9e2Rpc2FibGVkfQogICAgICBjbGFzc05hbWU9e2NuKAogICAgICAgIGJhc2VDbGFzc2VzLAogICAgICAgIHZhcmlhbnRDbGFzc2VzW3ZhcmlhbnRdLAogICAgICAgIHNpemVDbGFzc2VzW3NpemVdLAogICAgICAgIGNsYXNzTmFtZQogICAgICApfQogICAgICB7Li4ucHJvcHN9CiAgICA+CiAgICAgIHtjaGlsZHJlbn0KICAgIDwvYnV0dG9uPgogICk7Cn0K" 69 }, 70 { 71 "path": "src/shared/fragments/Input.tsx", 72 "content": "aW1wb3J0IHsgSlNYIH0gZnJvbSAicHJlYWN0IjsKaW1wb3J0IHsgY24gfSBmcm9tICIuLi8uLi91dGlscy9jbi50cyI7Cgp0eXBlIElucHV0UHJvcHMgPSBKU1guSW50cmluc2ljRWxlbWVudHNbJ2lucHV0J107CgpleHBvcnQgZnVuY3Rpb24gSW5wdXQoewogIHR5cGUgPSAidGV4dCIsCiAgY2xhc3NOYW1lLAogIC4uLnByb3BzCn06IElucHV0UHJvcHMpIHsKICByZXR1cm4gKAogICAgPGlucHV0CiAgICAgIHR5cGU9e3R5cGV9CiAgICAgIGNsYXNzTmFtZT17Y24oCiAgICAgICAgImJsb2NrIHctZnVsbCBweC0zIHB5LTIgYm9yZGVyIGJvcmRlci1ncmF5LTMwMCByb3VuZGVkLW1kIHNoYWRvdy1zbSIsCiAgICAgICAgImZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpyaW5nLWJsdWUtNTAwIGZvY3VzOmJvcmRlci1ibHVlLTUwMCIsCiAgICAgICAgImRpc2FibGVkOmJnLWdyYXktNTAgZGlzYWJsZWQ6dGV4dC1ncmF5LTUwMCIsCiAgICAgICAgY2xhc3NOYW1lCiAgICAgICl9CiAgICAgIHsuLi5wcm9wc30KICAgIC8+CiAgKTsKfQ==" 73 }, 74 { 75 "path": "src/config.ts", 76 - "content": "aW1wb3J0IHsgT0F1dGhDbGllbnQsIFNRTGl0ZU9BdXRoU3RvcmFnZSB9IGZyb20gIkBzbGljZXMvb2F1dGgiOwppbXBvcnQgeyBTZXNzaW9uU3RvcmUsIFNRTGl0ZUFkYXB0ZXIsIHdpdGhPQXV0aFNlc3Npb24gfSBmcm9tICJAc2xpY2VzL3Nlc3Npb24iOwppbXBvcnQgeyBBdFByb3RvQ2xpZW50IH0gZnJvbSAiLi9nZW5lcmF0ZWRfY2xpZW50LnRzIjsKCmNvbnN0IE9BVVRIX0NMSUVOVF9JRCA9IERlbm8uZW52LmdldCgiT0FVVEhfQ0xJRU5UX0lEIik7CmNvbnN0IE9BVVRIX0NMSUVOVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoIk9BVVRIX0NMSUVOVF9TRUNSRVQiKTsKY29uc3QgT0FVVEhfUkVESVJFQ1RfVVJJID0gRGVuby5lbnYuZ2V0KCJPQVVUSF9SRURJUkVDVF9VUkkiKTsKY29uc3QgT0FVVEhfQUlQX0JBU0VfVVJMID0gRGVuby5lbnYuZ2V0KCJPQVVUSF9BSVBfQkFTRV9VUkwiKTsKY29uc3QgQVBJX1VSTCA9IERlbm8uZW52LmdldCgiQVBJX1VSTCIpOwpleHBvcnQgY29uc3QgU0xJQ0VfVVJJID0gRGVuby5lbnYuZ2V0KCJTTElDRV9VUkkiKTsKCmlmICgKICAhT0FVVEhfQ0xJRU5UX0lEIHx8CiAgIU9BVVRIX0NMSUVOVF9TRUNSRVQgfHwKICAhT0FVVEhfUkVESVJFQ1RfVVJJIHx8CiAgIU9BVVRIX0FJUF9CQVNFX1VSTCB8fAogICFBUElfVVJMIHx8CiAgIVNMSUNFX1VSSQopIHsKICB0aHJvdyBuZXcgRXJyb3IoCiAgICAiTWlzc2luZyBPQXV0aCBjb25maWd1cmF0aW9uLiBQbGVhc2UgZW5zdXJlIC5lbnYgZmlsZSBjb250YWluczpcbiIgKwogICAgICAiT0FVVEhfQ0xJRU5UX0lELCBPQVVUSF9DTElFTlRfU0VDUkVULCBPQVVUSF9SRURJUkVDVF9VUkksIE9BVVRIX0FJUF9CQVNFX1VSTCwgQVBJX1VSTCwgU0xJQ0VfVVJJIgogICk7Cn0KCmNvbnN0IERBVEFCQVNFX1VSTCA9IERlbm8uZW52LmdldCgiREFUQUJBU0VfVVJMIikgfHwgInNsaWNlcy5kYiI7CgovLyBPQXV0aCBzZXR1cApjb25zdCBvYXV0aFN0b3JhZ2UgPSBuZXcgU1FMaXRlT0F1dGhTdG9yYWdlKERBVEFCQVNFX1VSTCk7CmNvbnN0IG9hdXRoQ29uZmlnID0gewogIGNsaWVudElkOiBPQVVUSF9DTElFTlRfSUQsCiAgY2xpZW50U2VjcmV0OiBPQVVUSF9DTElFTlRfU0VDUkVULAogIGF1dGhCYXNlVXJsOiBPQVVUSF9BSVBfQkFTRV9VUkwsCiAgcmVkaXJlY3RVcmk6IE9BVVRIX1JFRElSRUNUX1VSSSwKICBzY29wZXM6IFsiYXRwcm90byIsICJvcGVuaWQiLCAicHJvZmlsZSJdLAp9OwoKLy8gRXhwb3J0IGNvbmZpZyBhbmQgc3RvcmFnZSBmb3IgY3JlYXRpbmcgdXNlci1zY29wZWQgY2xpZW50cwpleHBvcnQgeyBvYXV0aENvbmZpZywgb2F1dGhTdG9yYWdlIH07CgovLyBTZXNzaW9uIHNldHVwIChzaGFyZWQgZGF0YWJhc2UpCmV4cG9ydCBjb25zdCBzZXNzaW9uU3RvcmUgPSBuZXcgU2Vzc2lvblN0b3JlKHsKICBhZGFwdGVyOiBuZXcgU1FMaXRlQWRhcHRlcihEQVRBQkFTRV9VUkwpLAogIGNvb2tpZU5hbWU6ICJ7e1BST0pFQ1RfTkFNRX19LXNlc3Npb24iLAogIGNvb2tpZU9wdGlvbnM6IHsKICAgIGh0dHBPbmx5OiB0cnVlLAogICAgc2VjdXJlOiBEZW5vLmVudi5nZXQoIkRFTk9fRU5WIikgPT09ICJwcm9kdWN0aW9uIiwKICAgIHNhbWVTaXRlOiAibGF4IiwKICAgIHBhdGg6ICIvIiwKICB9LAp9KTsKCi8vIE9BdXRoICsgU2Vzc2lvbiBpbnRlZ3JhdGlvbgpleHBvcnQgY29uc3Qgb2F1dGhTZXNzaW9ucyA9IHdpdGhPQXV0aFNlc3Npb24oCiAgc2Vzc2lvblN0b3JlLAogIG9hdXRoQ29uZmlnLAogIG9hdXRoU3RvcmFnZSwKICB7CiAgICBhdXRvUmVmcmVzaDogdHJ1ZSwKICB9Cik7CgovLyBIZWxwZXIgZnVuY3Rpb24gdG8gY3JlYXRlIHVzZXItc2NvcGVkIE9BdXRoIGNsaWVudApleHBvcnQgZnVuY3Rpb24gY3JlYXRlT0F1dGhDbGllbnQodXNlcklkOiBzdHJpbmcpOiBPQXV0aENsaWVudCB7CiAgcmV0dXJuIG5ldyBPQXV0aENsaWVudChvYXV0aENvbmZpZywgb2F1dGhTdG9yYWdlLCB1c2VySWQpOwp9CgovLyBIZWxwZXIgZnVuY3Rpb24gdG8gY3JlYXRlIGF1dGhlbnRpY2F0ZWQgQXRQcm90byBjbGllbnQgZm9yIGEgdXNlcgpleHBvcnQgZnVuY3Rpb24gY3JlYXRlU2Vzc2lvbkNsaWVudCh1c2VySWQ6IHN0cmluZyk6IEF0UHJvdG9DbGllbnQgewogIGNvbnN0IHVzZXJPQXV0aENsaWVudCA9IGNyZWF0ZU9BdXRoQ2xpZW50KHVzZXJJZCk7CiAgcmV0dXJuIG5ldyBBdFByb3RvQ2xpZW50KEFQSV9VUkwhLCBTTElDRV9VUkkhLCB1c2VyT0F1dGhDbGllbnQpOwp9CgovLyBQdWJsaWMgY2xpZW50IGZvciB1bmF1dGhlbnRpY2F0ZWQgcmVxdWVzdHMKZXhwb3J0IGNvbnN0IHB1YmxpY0NsaWVudCA9IG5ldyBBdFByb3RvQ2xpZW50KEFQSV9VUkwsIFNMSUNFX1VSSSk7" 77 }, 78 { 79 "path": "src/routes/middleware.ts", 80 "content": "aW1wb3J0IHsgc2Vzc2lvblN0b3JlLCBjcmVhdGVPQXV0aENsaWVudCB9IGZyb20gIi4uL2NvbmZpZy50cyI7CgpleHBvcnQgaW50ZXJmYWNlIEF1dGhDb250ZXh0IHsKICBjdXJyZW50VXNlcjogewogICAgc3ViOiBzdHJpbmc7CiAgICBuYW1lPzogc3RyaW5nOwogICAgZW1haWw/OiBzdHJpbmc7CiAgfSB8IG51bGw7CiAgc2Vzc2lvbklkOiBzdHJpbmcgfCBudWxsOwp9CgpleHBvcnQgYXN5bmMgZnVuY3Rpb24gd2l0aEF1dGgocmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxBdXRoQ29udGV4dD4gewogIGNvbnN0IHNlc3Npb24gPSBhd2FpdCBzZXNzaW9uU3RvcmUuZ2V0U2Vzc2lvbkZyb21SZXF1ZXN0KHJlcSk7CgogIGlmICghc2Vzc2lvbikgewogICAgcmV0dXJuIHsgY3VycmVudFVzZXI6IG51bGwsIHNlc3Npb25JZDogbnVsbCB9OwogIH0KCiAgdHJ5IHsKICAgIGNvbnN0IHNlc3Npb25PQXV0aENsaWVudCA9IGNyZWF0ZU9BdXRoQ2xpZW50KHNlc3Npb24uc2Vzc2lvbklkKTsKICAgIGNvbnN0IHVzZXJJbmZvID0gYXdhaXQgc2Vzc2lvbk9BdXRoQ2xpZW50LmdldFVzZXJJbmZvKCk7CiAgICByZXR1cm4gewogICAgICBjdXJyZW50VXNlcjogdXNlckluZm8gfHwgbnVsbCwKICAgICAgc2Vzc2lvbklkOiBzZXNzaW9uLnNlc3Npb25JZCwKICAgIH07CiAgfSBjYXRjaCB7CiAgICByZXR1cm4geyBjdXJyZW50VXNlcjogbnVsbCwgc2Vzc2lvbklkOiBzZXNzaW9uLnNlc3Npb25JZCB9OwogIH0KfQoKZXhwb3J0IGZ1bmN0aW9uIHJlcXVpcmVBdXRoKAogIGhhbmRsZXI6IChyZXE6IFJlcXVlc3QsIGNvbnRleHQ6IEF1dGhDb250ZXh0KSA9PiBQcm9taXNlPFJlc3BvbnNlPgopIHsKICByZXR1cm4gYXN5bmMgKHJlcTogUmVxdWVzdCk6IFByb21pc2U8UmVzcG9uc2U+ID0+IHsKICAgIGNvbnN0IGNvbnRleHQgPSBhd2FpdCB3aXRoQXV0aChyZXEpOwoKICAgIGlmICghY29udGV4dC5jdXJyZW50VXNlcikgewogICAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMik7CiAgICB9CgogICAgcmV0dXJuIGhhbmRsZXIocmVxLCBjb250ZXh0KTsKICB9Owp9Cg==" 81 }, 82 { 83 "path": "src/routes/mod.ts", 84 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IGF1dGhSb3V0ZXMgfSBmcm9tICIuLi9mZWF0dXJlcy9hdXRoL2hhbmRsZXJzLnRzeCI7CmltcG9ydCB7IGRhc2hib2FyZFJvdXRlcyB9IGZyb20gIi4uL2ZlYXR1cmVzL2Rhc2hib2FyZC9oYW5kbGVycy50c3giOwoKZXhwb3J0IGNvbnN0IGFsbFJvdXRlczogUm91dGVbXSA9IFsKICAvLyBSb290IHJlZGlyZWN0IHRvIGxvZ2luIGZvciBub3cKICB7CiAgICBtZXRob2Q6ICJHRVQiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi8iIH0pLAogICAgaGFuZGxlcjogKHJlcSkgPT4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMiksCiAgfSwKCiAgLy8gQXV0aCByb3V0ZXMKICAuLi5hdXRoUm91dGVzLAoKICAvLyBEYXNoYm9hcmQgcm91dGVzCiAgLi4uZGFzaGJvYXJkUm91dGVzLApdOw==" 85 } 86 ]; 87 88 - export function getTemplateContent(path: string): Uint8Array | undefined { 89 - const template = EMBEDDED_TEMPLATES.find(t => t.path === path); 90 if (!template) return undefined; 91 92 const binaryString = atob(template.content); ··· 109 } 110 return result; 111 }
··· 2 // Generated by scripts/embed-templates.ts 3 4 export interface EmbeddedTemplate { 5 + template: string; // Template name (e.g., "deno-ssr", "deno-graphql") 6 path: string; 7 content: string; // Base64 encoded 8 } 9 10 export const EMBEDDED_TEMPLATES: EmbeddedTemplate[] = [ 11 { 12 + "template": "deno-ssr", 13 "path": "deno.json", 14 "content": "ewogICJ0YXNrcyI6IHsKICAgICJzdGFydCI6ICJkZW5vIHJ1biAtQSAtLWVudi1maWxlPS5lbnYgc3JjL21haW4udHMiLAogICAgImRldiI6ICJkZW5vIHJ1biAtQSAtLWVudi1maWxlPS5lbnYgLS13YXRjaCBzcmMvbWFpbi50cyIKICB9LAogICJjb21waWxlck9wdGlvbnMiOiB7CiAgICAianN4IjogInByZWNvbXBpbGUiLAogICAgImpzeEltcG9ydFNvdXJjZSI6ICJwcmVhY3QiCiAgfSwKICAiaW1wb3J0cyI6IHsKICAgICJAc2xpY2VzL2NsaWVudCI6ICJqc3I6QHNsaWNlcy9jbGllbnRAXjAuMS4wLWFscGhhLjQiLAogICAgIkBzbGljZXMvb2F1dGgiOiAianNyOkBzbGljZXMvb2F1dGhAXjAuNi4wIiwKICAgICJAc2xpY2VzL3Nlc3Npb24iOiAianNyOkBzbGljZXMvc2Vzc2lvbkBeMC4zLjAiLAogICAgIkBzdGQvYXNzZXJ0IjogImpzcjpAc3RkL2Fzc2VydEBeMS4wLjE0IiwKICAgICJAc3RkL2ZtdCI6ICJqc3I6QHN0ZC9mbXRAXjEuMC44IiwKICAgICJwcmVhY3QiOiAibnBtOnByZWFjdEBeMTAuMjcuMSIsCiAgICAicHJlYWN0LXJlbmRlci10by1zdHJpbmciOiAibnBtOnByZWFjdC1yZW5kZXItdG8tc3RyaW5nQF42LjUuMTMiLAogICAgInR5cGVkLWh0bXgiOiAibnBtOnR5cGVkLWh0bXhAXjAuMy4xIiwKICAgICJAc3RkL2h0dHAiOiAianNyOkBzdGQvaHR0cEBeMS4wLjIwIiwKICAgICJjbHN4IjogIm5wbTpjbHN4QF4yLjEuMSIsCiAgICAidGFpbHdpbmQtbWVyZ2UiOiAibnBtOnRhaWx3aW5kLW1lcmdlQF4yLjUuNSIsCiAgICAibHVjaWRlLXByZWFjdCI6ICJucG06bHVjaWRlLXByZWFjdEBeMC41NDQuMCIKICB9LAogICJub2RlTW9kdWxlc0RpciI6ICJhdXRvIgp9Cg==" 15 }, 16 { 17 + "template": "deno-ssr", 18 "path": "README.md", 19 "content": "IyB7e1BST0pFQ1RfTkFNRX19CgpBIERlbm8gU1NSIHdlYiBhcHBsaWNhdGlvbiB3aXRoIEFUIFByb3RvY29sIGludGVncmF0aW9uLCBidWlsdCB3aXRoIFByZWFjdCwKSFRNWCwgYW5kIE9BdXRoIGF1dGhlbnRpY2F0aW9uLgoKIyMgUXVpY2sgU3RhcnQKCmBgYGJhc2gKIyBTdGFydCB0aGUgZGV2ZWxvcG1lbnQgc2VydmVyCmRlbm8gdGFzayBkZXYKYGBgCgpWaXNpdCB5b3VyIGFwcCBhdCBodHRwOi8vbG9jYWxob3N0OjgwODAKCj4gKipOb3RlOioqIFlvdXIgc2xpY2UgYW5kIE9BdXRoIGNyZWRlbnRpYWxzIHdlcmUgYXV0b21hdGljYWxseSBjb25maWd1cmVkCj4gZHVyaW5nIHByb2plY3QgY3JlYXRpb24uIFRoZSBgLmVudmAgZmlsZSBpcyBhbHJlYWR5IHNldCB1cCB3aXRoIHlvdXIKPiBjcmVkZW50aWFscy4KCiMjIEZlYXR1cmVzCgotIPCflJAgKipPQXV0aCBBdXRoZW50aWNhdGlvbioqIHdpdGggUEtDRSBmbG93Ci0g4pqhICoqU2VydmVyLVNpZGUgUmVuZGVyaW5nKiogd2l0aCBQcmVhY3QKLSDwn46vICoqSW50ZXJhY3RpdmUgVUkqKiB3aXRoIEhUTVgKLSDwn46oICoqU3R5bGluZyoqIHdpdGggVGFpbHdpbmQgQ1NTCi0g8J+XhO+4jyAqKlNlc3Npb24gTWFuYWdlbWVudCoqIHdpdGggU1FMaXRlCi0g8J+UhCAqKkF1dG8gVG9rZW4gUmVmcmVzaCoqCi0g8J+Pl++4jyAqKkZlYXR1cmUtQmFzZWQgQXJjaGl0ZWN0dXJlKioKCiMjIERldmVsb3BtZW50CgpgYGBiYXNoCiMgU3RhcnQgZGV2ZWxvcG1lbnQgc2VydmVyIHdpdGggaG90IHJlbG9hZApkZW5vIHRhc2sgZGV2CgojIFN0YXJ0IHByb2R1Y3Rpb24gc2VydmVyCmRlbm8gdGFzayBzdGFydAoKIyBGb3JtYXQgY29kZQpkZW5vIGZtdAoKIyBDaGVjayB0eXBlcwpkZW5vIGNoZWNrIHNyYy8qKi8qLnRzIHNyYy8qKi8qLnRzeApgYGAKCiMjIFByb2plY3QgU3RydWN0dXJlCgpgYGAKc2xpY2VzLmpzb24gICAgICAgICAgICAgICMgU2xpY2VzIGNvbmZpZ3VyYXRpb24gZmlsZQpsZXhpY29ucy8gICAgICAgICAgICAgICAgIyBBVCBQcm90b2NvbCBsZXhpY29uIGRlZmluaXRpb25zCnNyYy8K4pSc4pSA4pSAIG1haW4udHMgICAgICAgICAgICAgICMgU2VydmVyIGVudHJ5IHBvaW50CuKUnOKUgOKUgCBjb25maWcudHMgICAgICAgICAgICAjIE9BdXRoICYgc2Vzc2lvbiBjb25maWd1cmF0aW9uCuKUnOKUgOKUgCBnZW5lcmF0ZWRfY2xpZW50LnRzICAjIEdlbmVyYXRlZCBUeXBlU2NyaXB0IGNsaWVudCBmcm9tIGxleGljb25zCuKUnOKUgOKUgCByb3V0ZXMvICAgICAgICAgICAgICAjIFJvdXRlIGRlZmluaXRpb25zCuKUnOKUgOKUgCBmZWF0dXJlcy8gICAgICAgICAgICAjIEZlYXR1cmUgbW9kdWxlcwrilIIgICDilJTilIDilIAgYXV0aC8gICAgICAgICAgICMgQXV0aGVudGljYXRpb24K4pSc4pSA4pSAIHNoYXJlZC9mcmFnbWVudHMvICAgICMgUmV1c2FibGUgVUkgY29tcG9uZW50cwrilJTilIDilIAgdXRpbHMvICAgICAgICAgICAgICAjIFV0aWxpdHkgZnVuY3Rpb25zCmBgYAoKIyMgT0F1dGggU2V0dXAKCllvdXIgT0F1dGggYXBwbGljYXRpb24gd2FzIGF1dG9tYXRpY2FsbHkgY3JlYXRlZCBkdXJpbmcgcHJvamVjdCBpbml0aWFsaXphdGlvbgp3aXRoOgoKLSAqKkNsaWVudCBJRCAmIFNlY3JldCoqOiBBbHJlYWR5IGNvbmZpZ3VyZWQgaW4gYC5lbnZgCi0gKipSZWRpcmVjdCBVUkkqKjogYGh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9vYXV0aC9jYWxsYmFja2AKLSAqKlNsaWNlKio6IEF1dG9tYXRpY2FsbHkgY3JlYXRlZCBhbmQgbGlua2VkCgpUbyBtYW5hZ2UgeW91ciBPQXV0aCBjbGllbnRzIG9yIGNyZWF0ZSBhZGRpdGlvbmFsIG9uZXM6CgoxLiBWaXNpdCBbU2xpY2VzIE5ldHdvcmtdKGh0dHBzOi8vc2xpY2VzLm5ldHdvcmspCjIuIFVzZSB0aGUgYHNsaWNlcyBsb2dpbmAgQ0xJIGNvbW1hbmQKCiMjIERvY3VtZW50YXRpb24KCi0gYENMQVVERS5tZGAgLSBBcmNoaXRlY3R1cmUgZ3VpZGUgZm9yIEFJIGFzc2lzdGFuY2UKLSBGZWF0dXJlIGRpcmVjdG9yaWVzIGNvbnRhaW4gaGFuZGxlcnMgYW5kIHRlbXBsYXRlcwotIENvbXBvbmVudHMgdXNlIFByZWFjdCB3aXRoIHNlcnZlci1zaWRlIHJlbmRlcmluZwotIEhUTVggcHJvdmlkZXMgaW50ZXJhY3RpdmUgYmVoYXZpb3Igd2l0aG91dCBwYWdlIHJlbG9hZHMKCiMjIExpY2Vuc2UKCk1JVAo=" 20 }, 21 { 22 + "template": "deno-ssr", 23 "path": ".gitignore", 24 "content": "LmVudioKbm9kZV9tb2R1bGVzCiouZGIqCg==" 25 }, 26 { 27 + "template": "deno-ssr", 28 "path": ".env.example", 29 "content": "IyBPQXV0aCBDb25maWd1cmF0aW9uIChyZXF1aXJlZCkKT0FVVEhfQ0xJRU5UX0lEPXlvdXJfb2F1dGhfY2xpZW50X2lkCk9BVVRIX0NMSUVOVF9TRUNSRVQ9eW91cl9vYXV0aF9jbGllbnRfc2VjcmV0Ck9BVVRIX1JFRElSRUNUX1VSST1odHRwOi8vbG9jYWxob3N0OjgwODAvb2F1dGgvY2FsbGJhY2sKT0FVVEhfQUlQX0JBU0VfVVJMPWh0dHBzOi8vYXV0aC5zbGljZXMubmV0d29yawoKIyBBUEkgQ29uZmlndXJhdGlvbiAocmVxdWlyZWQpCkFQSV9VUkw9aHR0cHM6Ly9hcGkuc2xpY2VzLm5ldHdvcmsKU0xJQ0VfVVJJPWF0Oi8vZGlkOnBsYzpiY2dsdHpxYXp3NXRiNmsyZzN0dGVuYmovbmV0d29yay5zbGljZXMuc2xpY2UvM2x6Ynp1bWNtdm8yegoKIyBEYXRhYmFzZSAob3B0aW9uYWwsIGRlZmF1bHRzIHRvIHNsaWNlcy5kYikKREFUQUJBU0VfVVJMPXNsaWNlcy5kYgoKIyBFbnZpcm9ubWVudCAob3B0aW9uYWwsIGFmZmVjdHMgY29va2llIHNlY3VyaXR5KQpERU5PX0VOVj1kZXZlbG9wbWVudAoKIyBTZXJ2ZXIgKG9wdGlvbmFsLCBkZWZhdWx0cyB0byA4MDgwKQpQT1JUPTgwODA=" 30 }, 31 { 32 + "template": "deno-ssr", 33 "path": "CLAUDE.md", 34 "content": "IyBDTEFVREUubWQKClRoaXMgZmlsZSBwcm92aWRlcyBndWlkYW5jZSB0byBDbGF1ZGUgQ29kZSAoY2xhdWRlLmFpL2NvZGUpIHdoZW4gd29ya2luZyB3aXRoCmNvZGUgaW4gdGhpcyByZXBvc2l0b3J5LgoKIyMgRGV2ZWxvcG1lbnQgQ29tbWFuZHMKCmBgYGJhc2gKIyBTdGFydCBkZXZlbG9wbWVudCBzZXJ2ZXIgd2l0aCBob3QgcmVsb2FkCmRlbm8gdGFzayBkZXYKCiMgU3RhcnQgcHJvZHVjdGlvbiBzZXJ2ZXIKZGVubyB0YXNrIHN0YXJ0CgojIEZvcm1hdCBjb2RlCmRlbm8gZm10CgojIENoZWNrIHR5cGVzCmRlbm8gY2hlY2sgc3JjLyoqLyoudHMgc3JjLyoqLyoudHN4CmBgYAoKIyMgQXJjaGl0ZWN0dXJlIE92ZXJ2aWV3CgpUaGlzIGlzIGEgRGVuby1iYXNlZCB3ZWIgYXBwbGljYXRpb24gYnVpbHQgd2l0aCB0aGUgU2xpY2VzIENMSS4gSXQgcHJvdmlkZXMKc2VydmVyLXNpZGUgcmVuZGVyaW5nIHdpdGggUHJlYWN0LCBPQXV0aCBhdXRoZW50aWNhdGlvbiwgYW5kIEFUIFByb3RvY29sCmludGVncmF0aW9uIGZvciBidWlsZGluZyBhcHBsaWNhdGlvbnMgb24gdGhlIGRlY2VudHJhbGl6ZWQgd2ViLgoKIyMjIFRlY2hub2xvZ3kgU3RhY2sKCi0gKipSdW50aW1lKio6IERlbm8gd2l0aCBUeXBlU2NyaXB0Ci0gKipGcm9udGVuZCoqOiBQcmVhY3Qgd2l0aCBzZXJ2ZXItc2lkZSByZW5kZXJpbmcKLSAqKlN0eWxpbmcqKjogVGFpbHdpbmQgQ1NTICh2aWEgQ0ROKQotICoqSW50ZXJhY3Rpdml0eSoqOiBIVE1YICsgSHlwZXJzY3JpcHQKLSAqKlJvdXRpbmcqKjogRGVubydzIHN0YW5kYXJkIEhUVFAgcm91dGluZwotICoqQXV0aGVudGljYXRpb24qKjogT0F1dGggd2l0aCBQS0NFIGZsb3cgdXNpbmcgYEBzbGljZXMvb2F1dGhgCi0gKipTZXNzaW9ucyoqOiBTUUxpdGUtYmFzZWQgd2l0aCBgQHNsaWNlcy9zZXNzaW9uYAotICoqRGF0YWJhc2UqKjogU1FMaXRlIHZpYSBPQXV0aCBhbmQgc2Vzc2lvbiBsaWJyYXJpZXMKCiMjIyBDb3JlIEFyY2hpdGVjdHVyZSBQYXR0ZXJucwoKIyMjIyBGZWF0dXJlLUJhc2VkIE9yZ2FuaXphdGlvbgoKVGhlIGNvZGViYXNlIGlzIG9yZ2FuaXplZCBieSBmZWF0dXJlcyByYXRoZXIgdGhhbiB0ZWNobmljYWwgbGF5ZXJzOgoKYGBgCnNyYy8K4pSc4pSA4pSAIGZlYXR1cmVzLyAgICAgICAgICAgIyBGZWF0dXJlIG1vZHVsZXMK4pSCICAg4pSU4pSA4pSAIGF1dGgvICAgICAgICAgICMgQXV0aGVudGljYXRpb24gKGxvZ2luL2xvZ291dCkK4pSc4pSA4pSAIHNoYXJlZC8gICAgICAgICAgICAjIFNoYXJlZCBVSSBjb21wb25lbnRzCuKUnOKUgOKUgCByb3V0ZXMvICAgICAgICAgICAgIyBSb3V0ZSBkZWZpbml0aW9ucyBhbmQgbWlkZGxld2FyZQrilJzilIDilIAgdXRpbHMvICAgICAgICAgICAgICMgVXRpbGl0eSBmdW5jdGlvbnMK4pSU4pSA4pSAIGNvbmZpZy50cyAgICAgICAgICAjIENvcmUgY29uZmlndXJhdGlvbgpgYGAKCiMjIyMgSGFuZGxlciBQYXR0ZXJuCgpFYWNoIGZlYXR1cmUgZm9sbG93cyBhIGNvbnNpc3RlbnQgcGF0dGVybjoKCi0gYGhhbmRsZXJzLnRzeGAgLSBSb3V0ZSBoYW5kbGVycyB0aGF0IHJldHVybiBSZXNwb25zZSBvYmplY3RzCi0gYHRlbXBsYXRlcy9gIC0gUHJlYWN0IGNvbXBvbmVudHMgZm9yIHJlbmRlcmluZwotIGB0ZW1wbGF0ZXMvZnJhZ21lbnRzL2AgLSBSZXVzYWJsZSBVSSBjb21wb25lbnRzCgojIyMjIEF1dGhlbnRpY2F0aW9uICYgU2Vzc2lvbnMKCi0gT0F1dGggaW50ZWdyYXRpb24gd2l0aCBBVCBQcm90b2NvbCB1c2luZyBgQHNsaWNlcy9vYXV0aGAKLSBQS0NFIGZsb3cgZm9yIHNlY3VyZSBhdXRoZW50aWNhdGlvbgotIFNlc3Npb24gbWFuYWdlbWVudCB3aXRoIGBAc2xpY2VzL3Nlc3Npb25gCi0gU1FMaXRlIHN0b3JhZ2UgZm9yIE9BdXRoIHN0YXRlIGFuZCBzZXNzaW9ucwotIEF1dG9tYXRpYyB0b2tlbiByZWZyZXNoIGNhcGFiaWxpdGllcwoKIyMjIEtleSBDb21wb25lbnRzCgojIyMjIFJvdXRlIFN5c3RlbQoKLSBBbGwgcm91dGVzIGRlZmluZWQgaW4gYHNyYy9yb3V0ZXMvbW9kLnRzYAotIEZlYXR1cmUgcm91dGVzIGV4cG9ydGVkIGZyb20gYHNyYy9mZWF0dXJlcy8qL2hhbmRsZXJzLnRzeGAKLSBNaWRkbGV3YXJlIGluIGBzcmMvcm91dGVzL21pZGRsZXdhcmUudHNgIGhhbmRsZXMgYXV0aCBzdGF0ZQoKIyMjIyBPQXV0aCBJbnRlZ3JhdGlvbgoKLSBgc3JjL2NvbmZpZy50c2AgLSBPQXV0aCBjbGllbnQgYW5kIHNlc3Npb24gc3RvcmUgc2V0dXAKLSBFbnZpcm9ubWVudCB2YXJpYWJsZXMgcmVxdWlyZWQ6IGBPQVVUSF9DTElFTlRfSURgLCBgT0FVVEhfQ0xJRU5UX1NFQ1JFVGAsCiAgYE9BVVRIX1JFRElSRUNUX1VSSWAsIGBPQVVUSF9BSVBfQkFTRV9VUkxgLCBgQVBJX1VSTGAsIGBTTElDRV9VUklgCi0gUEtDRSBmbG93IGltcGxlbWVudGF0aW9uIGluIGF1dGggaGFuZGxlcnMKLSBTUUxpdGUgc3RvcmFnZSBmb3IgT0F1dGggc3RhdGUgYW5kIHRva2VucwoKIyMjIyBSZW5kZXJpbmcgU3lzdGVtCgotIGBzcmMvdXRpbHMvcmVuZGVyLnRzeGAgLSBVbmlmaWVkIEhUTUwgcmVuZGVyaW5nIHdpdGggcHJvcGVyIGhlYWRlcnMKLSBTZXJ2ZXItc2lkZSByZW5kZXJpbmcgd2l0aCBQcmVhY3QKLSBIVE1YIGZvciBkeW5hbWljIGludGVyYWN0aW9ucyB3aXRob3V0IHBhZ2UgcmVsb2FkcwotIFNoYXJlZCBgTGF5b3V0YCBjb21wb25lbnQgaW4gYHNyYy9zaGFyZWQvZnJhZ21lbnRzL0xheW91dC50c3hgCgojIyMgRGV2ZWxvcG1lbnQgR3VpZGVsaW5lcwoKIyMjIyBDb21wb25lbnQgQ29udmVudGlvbnMKCi0gVXNlIGAudHN4YCBleHRlbnNpb24gZm9yIGNvbXBvbmVudHMgd2l0aCBKU1gKLSBQcmVhY3QgY29tcG9uZW50cyBmb3IgYWxsIFVJIHJlbmRlcmluZwotIEhUTVggYXR0cmlidXRlcyBmb3IgaW50ZXJhY3RpdmUgYmVoYXZpb3IKLSBUYWlsd2luZCBjbGFzc2VzIGZvciBzdHlsaW5nCgojIyMjIEZlYXR1cmUgRGV2ZWxvcG1lbnQKCldoZW4gYWRkaW5nIG5ldyBmZWF0dXJlczoKCjEuIENyZWF0ZSBmZWF0dXJlIGRpcmVjdG9yeSB1bmRlciBgc3JjL2ZlYXR1cmVzL2AKMi4gQWRkIGBoYW5kbGVycy50c3hgIHdpdGggcm91dGUgZGVmaW5pdGlvbnMKMy4gQ3JlYXRlIGB0ZW1wbGF0ZXMvYCBkaXJlY3Rvcnkgd2l0aCBQcmVhY3QgY29tcG9uZW50cwo0LiBFeHBvcnQgcm91dGVzIGZyb20gZmVhdHVyZSBhbmQgYWRkIHRvIGBzcmMvcm91dGVzL21vZC50c2AKNS4gRm9sbG93IGV4aXN0aW5nIGF1dGhlbnRpY2F0aW9uIHBhdHRlcm5zIHVzaW5nIGF1dGggbWlkZGxld2FyZQoKIyMjIyBFbnZpcm9ubWVudCBTZXR1cAoKVGhlIGFwcGxpY2F0aW9uIHJlcXVpcmVzIGEgYC5lbnZgIGZpbGUgd2l0aCBPQXV0aCBhbmQgQVBJIGNvbmZpZ3VyYXRpb24uCkNvcHkgYC5lbnYuZXhhbXBsZWAgYW5kIGZpbGwgaW4geW91ciB2YWx1ZXMuIE1pc3NpbmcgZW52aXJvbm1lbnQgdmFyaWFibGVzCndpbGwgY2F1c2Ugc3RhcnR1cCBmYWlsdXJlcyB3aXRoIGRlc2NyaXB0aXZlIGVycm9yIG1lc3NhZ2VzLgoKIyMjIFJlcXVlc3QvUmVzcG9uc2UgRmxvdwoKMS4gUmVxdWVzdCBoaXRzIG1haW4gc2VydmVyIGluIGBzcmMvbWFpbi50c2AKMi4gUm91dGVzIHByb2Nlc3NlZCB0aHJvdWdoIGBzcmMvcm91dGVzL21vZC50c2AKMy4gQXV0aGVudGljYXRpb24gbWlkZGxld2FyZSBhcHBsaWVzIHNlc3Npb24gc3RhdGUKNC4gRmVhdHVyZSBoYW5kbGVycyBwcm9jZXNzIHJlcXVlc3RzIGFuZCByZXR1cm4gcmVuZGVyZWQgSFRNTAo1LiBIVE1YIGhhbmRsZXMgcGFydGlhbCBwYWdlIHVwZGF0ZXMgb24gY2xpZW50LXNpZGUgaW50ZXJhY3Rpb25zCgojIyMgT0F1dGggRmxvdwoKMS4gVXNlciBpbml0aWF0ZXMgbG9naW4gd2l0aCBoYW5kbGUvaWRlbnRpZmllcgoyLiBPQXV0aCBjbGllbnQgZ2VuZXJhdGVzIFBLQ0UgY2hhbGxlbmdlIGFuZCByZWRpcmVjdHMgdG8gYXV0aCBzZXJ2ZXIKMy4gVXNlciBhdXRoZW50aWNhdGVzIGFuZCBpcyByZWRpcmVjdGVkIGJhY2sgd2l0aCBhdXRob3JpemF0aW9uIGNvZGUKNC4gQ2xpZW50IGV4Y2hhbmdlcyBjb2RlIGZvciB0b2tlbnMgdXNpbmcgUEtDRSB2ZXJpZmllcgo1LiBTZXNzaW9uIGNyZWF0ZWQgd2l0aCBhdXRvbWF0aWMgdG9rZW4gcmVmcmVzaAo2LiBQcm90ZWN0ZWQgcm91dGVzIGFjY2VzcyB1c2VyIGRhdGEgdGhyb3VnaCBhdXRoZW50aWNhdGVkIGNsaWVudAoKIyMjIEFkZGluZyBOZXcgRmVhdHVyZXMKClRvIGFkZCBhIG5ldyBmZWF0dXJlOgoKMS4gQ3JlYXRlIGBzcmMvZmVhdHVyZXMvZmVhdHVyZS1uYW1lL2AKMi4gQWRkIGBoYW5kbGVycy50c3hgIHdpdGggcm91dGUgaGFuZGxlcnMKMy4gQ3JlYXRlIGB0ZW1wbGF0ZXMvYCBkaXJlY3RvcnkgZm9yIFVJIGNvbXBvbmVudHMKNC4gRXhwb3J0IHJvdXRlcyBhbmQgYWRkIHRvIG1haW4gcm91dGVyCjUuIFVzZSBleGlzdGluZyBwYXR0ZXJucyBmb3IgYXV0aGVudGljYXRpb24gYW5kIHJlbmRlcmluZw==" 35 }, 36 { 37 + "template": "deno-ssr", 38 "path": "src/main.ts", 39 "content": "aW1wb3J0IHsgcm91dGUgfSBmcm9tICJAc3RkL2h0dHAvdW5zdGFibGUtcm91dGUiOwppbXBvcnQgeyBhbGxSb3V0ZXMgfSBmcm9tICIuL3JvdXRlcy9tb2QudHMiOwppbXBvcnQgeyBjcmVhdGVMb2dnaW5nSGFuZGxlciB9IGZyb20gIi4vdXRpbHMvbG9nZ2luZy50cyI7CgpmdW5jdGlvbiBkZWZhdWx0SGFuZGxlcihyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgcmV0dXJuIFByb21pc2UucmVzb2x2ZShSZXNwb25zZS5yZWRpcmVjdChuZXcgVVJMKCIvIiwgcmVxLnVybCksIDMwMikpOwp9Cgpjb25zdCBoYW5kbGVyID0gY3JlYXRlTG9nZ2luZ0hhbmRsZXIocm91dGUoYWxsUm91dGVzLCBkZWZhdWx0SGFuZGxlcikpOwoKRGVuby5zZXJ2ZSgKICB7CiAgICBwb3J0OiBwYXJzZUludChEZW5vLmVudi5nZXQoIlBPUlQiKSB8fCAiODA4MCIpLAogICAgaG9zdG5hbWU6ICIwLjAuMC4wIiwKICAgIG9uTGlzdGVuOiAoeyBwb3J0LCBob3N0bmFtZSB9KSA9PgogICAgICBjb25zb2xlLmxvZyhg8J+agCBTZXJ2ZXIgcnVubmluZyBvbiBodHRwOi8vJHtob3N0bmFtZX06JHtwb3J0fWApLAogIH0sCiAgaGFuZGxlciwKKTs=" 40 }, 41 { 42 + "template": "deno-ssr", 43 "path": "src/features/auth/templates/LoginPage.tsx", 44 "content": "aW1wb3J0IHsgTGF5b3V0IH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9MYXlvdXQudHN4IjsKaW1wb3J0IHsgQnV0dG9uIH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9CdXR0b24udHN4IjsKaW1wb3J0IHsgSW5wdXQgfSBmcm9tICIuLi8uLi8uLi9zaGFyZWQvZnJhZ21lbnRzL0lucHV0LnRzeCI7CgppbnRlcmZhY2UgTG9naW5QYWdlUHJvcHMgewogIGVycm9yPzogc3RyaW5nOwp9CgpleHBvcnQgZnVuY3Rpb24gTG9naW5QYWdlKHsgZXJyb3IgfTogTG9naW5QYWdlUHJvcHMpIHsKICByZXR1cm4gKAogICAgPExheW91dCB0aXRsZT0iTG9naW4iPgogICAgICA8ZGl2IGNsYXNzTmFtZT0ibWluLWgtc2NyZWVuIGZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktY2VudGVyIGJnLWdyYXktNTAiPgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJtYXgtdy1tZCB3LWZ1bGwgc3BhY2UteS04Ij4KICAgICAgICAgIDxkaXY+CiAgICAgICAgICAgIDxoMiBjbGFzc05hbWU9Im10LTYgdGV4dC1jZW50ZXIgdGV4dC0zeGwgZm9udC1leHRyYWJvbGQgdGV4dC1ncmF5LTkwMCI+CiAgICAgICAgICAgICAgU2lnbiBpbiB0byB5b3VyIGFjY291bnQKICAgICAgICAgICAgPC9oMj4KICAgICAgICAgICAgPHAgY2xhc3NOYW1lPSJtdC0yIHRleHQtY2VudGVyIHRleHQtc20gdGV4dC1ncmF5LTYwMCI+CiAgICAgICAgICAgICAgVXNlIHlvdXIgQVQgUHJvdG9jb2wgaGFuZGxlIG9yIERJRAogICAgICAgICAgICA8L3A+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICB7ZXJyb3IgJiYgKAogICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0iYmctcmVkLTUwIGJvcmRlciBib3JkZXItcmVkLTIwMCB0ZXh0LXJlZC03MDAgcHgtNCBweS0zIHJvdW5kZWQiPgogICAgICAgICAgICAgIHtlcnJvciA9PT0gIk9BdXRoIGluaXRpYWxpemF0aW9uIGZhaWxlZCIgJiYgIkZhaWxlZCB0byBzdGFydCBhdXRoZW50aWNhdGlvbiJ9CiAgICAgICAgICAgICAge2Vycm9yID09PSAiSW52YWxpZCBPQXV0aCBjYWxsYmFjayIgJiYgIkF1dGhlbnRpY2F0aW9uIGNhbGxiYWNrIGZhaWxlZCJ9CiAgICAgICAgICAgICAge2Vycm9yID09PSAiQXV0aGVudGljYXRpb24gZmFpbGVkIiAmJiAiQXV0aGVudGljYXRpb24gZmFpbGVkIn0KICAgICAgICAgICAgICB7ZXJyb3IgPT09ICJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24iICYmICJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24ifQogICAgICAgICAgICAgIHshWyJPQXV0aCBpbml0aWFsaXphdGlvbiBmYWlsZWQiLCAiSW52YWxpZCBPQXV0aCBjYWxsYmFjayIsICJBdXRoZW50aWNhdGlvbiBmYWlsZWQiLCAiRmFpbGVkIHRvIGNyZWF0ZSBzZXNzaW9uIl0uaW5jbHVkZXMoZXJyb3IpICYmIGVycm9yfQogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICl9CgogICAgICAgICAgPGZvcm0gY2xhc3NOYW1lPSJtdC04IHNwYWNlLXktNiIgYWN0aW9uPSIvb2F1dGgvYXV0aG9yaXplIiBtZXRob2Q9InBvc3QiPgogICAgICAgICAgICA8ZGl2PgogICAgICAgICAgICAgIDxsYWJlbCBodG1sRm9yPSJsb2dpbkhpbnQiIGNsYXNzTmFtZT0iYmxvY2sgdGV4dC1zbSBmb250LW1lZGl1bSB0ZXh0LWdyYXktNzAwIj4KICAgICAgICAgICAgICAgIEhhbmRsZSBvciBESUQKICAgICAgICAgICAgICA8L2xhYmVsPgogICAgICAgICAgICAgIDxJbnB1dAogICAgICAgICAgICAgICAgaWQ9ImxvZ2luSGludCIKICAgICAgICAgICAgICAgIG5hbWU9ImxvZ2luSGludCIKICAgICAgICAgICAgICAgIHR5cGU9InRleHQiCiAgICAgICAgICAgICAgICByZXF1aXJlZAogICAgICAgICAgICAgICAgcGxhY2Vob2xkZXI9ImFsaWNlLmJza3kuc29jaWFsIG9yIGRpZDpwbGM6Li4uIgogICAgICAgICAgICAgICAgY2xhc3NOYW1lPSJtdC0xIgogICAgICAgICAgICAgIC8+CiAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgPEJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzTmFtZT0idy1mdWxsIj4KICAgICAgICAgICAgICBTaWduIGluCiAgICAgICAgICAgIDwvQnV0dG9uPgogICAgICAgICAgPC9mb3JtPgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvTGF5b3V0PgogICk7Cn0=" 45 }, 46 { 47 + "template": "deno-ssr", 48 "path": "src/features/auth/handlers.tsx", 49 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IHdpdGhBdXRoIH0gZnJvbSAiLi4vLi4vcm91dGVzL21pZGRsZXdhcmUudHMiOwppbXBvcnQgeyBPQXV0aENsaWVudCB9IGZyb20gIkBzbGljZXMvb2F1dGgiOwppbXBvcnQgewogIGNyZWF0ZU9BdXRoQ2xpZW50LAogIGNyZWF0ZVNlc3Npb25DbGllbnQsCiAgb2F1dGhDb25maWcsCiAgb2F1dGhTdG9yYWdlLAogIG9hdXRoU2Vzc2lvbnMsCiAgc2Vzc2lvblN0b3JlLAp9IGZyb20gIi4uLy4uL2NvbmZpZy50cyI7CmltcG9ydCB7IHJlbmRlckhUTUwgfSBmcm9tICIuLi8uLi91dGlscy9yZW5kZXIudHN4IjsKaW1wb3J0IHsgTG9naW5QYWdlIH0gZnJvbSAiLi90ZW1wbGF0ZXMvTG9naW5QYWdlLnRzeCI7Cgphc3luYyBmdW5jdGlvbiBoYW5kbGVMb2dpblBhZ2UocmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxSZXNwb25zZT4gewogIGNvbnN0IGNvbnRleHQgPSBhd2FpdCB3aXRoQXV0aChyZXEpOwogIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybCk7CgogIC8vIFJlZGlyZWN0IGlmIGFscmVhZHkgbG9nZ2VkIGluCiAgaWYgKGNvbnRleHQuY3VycmVudFVzZXIpIHsKICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdChuZXcgVVJMKCIvZGFzaGJvYXJkIiwgcmVxLnVybCksIDMwMik7CiAgfQoKICBjb25zdCBlcnJvciA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJlcnJvciIpOwogIHJldHVybiByZW5kZXJIVE1MKDxMb2dpblBhZ2UgZXJyb3I9e2Vycm9yIHx8IHVuZGVmaW5lZH0gLz4pOwp9Cgphc3luYyBmdW5jdGlvbiBoYW5kbGVPQXV0aEF1dGhvcml6ZShyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgdHJ5IHsKICAgIGNvbnN0IGZvcm1EYXRhID0gYXdhaXQgcmVxLmZvcm1EYXRhKCk7CiAgICBjb25zdCBsb2dpbkhpbnQgPSBmb3JtRGF0YS5nZXQoImxvZ2luSGludCIpIGFzIHN0cmluZzsKCiAgICBpZiAoIWxvZ2luSGludCkgewogICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKCJNaXNzaW5nIGxvZ2luIGhpbnQiLCB7IHN0YXR1czogNDAwIH0pOwogICAgfQoKICAgIGNvbnN0IHRlbXBPQXV0aENsaWVudCA9IG5ldyBPQXV0aENsaWVudCgKICAgICAgb2F1dGhDb25maWcsCiAgICAgIG9hdXRoU3RvcmFnZSwKICAgICAgbG9naW5IaW50CiAgICApOwogICAgY29uc3QgYXV0aFJlc3VsdCA9IGF3YWl0IHRlbXBPQXV0aENsaWVudC5hdXRob3JpemUoewogICAgICBsb2dpbkhpbnQsCiAgICB9KTsKCiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QoYXV0aFJlc3VsdC5hdXRob3JpemF0aW9uVXJsLCAzMDIpOwogIH0gY2F0Y2ggKGVycm9yKSB7CiAgICBjb25zb2xlLmVycm9yKCJPQXV0aCBhdXRob3JpemUgZXJyb3I6IiwgZXJyb3IpOwoKICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdCgKICAgICAgbmV3IFVSTCgKICAgICAgICAiL2xvZ2luP2Vycm9yPSIgKwogICAgICAgICAgZW5jb2RlVVJJQ29tcG9uZW50KCJQbGVhc2UgY2hlY2sgeW91ciBoYW5kbGUgYW5kIHRyeSBhZ2Fpbi4iKSwKICAgICAgICByZXEudXJsCiAgICAgICksCiAgICAgIDMwMgogICAgKTsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIGhhbmRsZU9BdXRoQ2FsbGJhY2socmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxSZXNwb25zZT4gewogIHRyeSB7CiAgICBjb25zdCB1cmwgPSBuZXcgVVJMKHJlcS51cmwpOwogICAgY29uc3QgY29kZSA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJjb2RlIik7CiAgICBjb25zdCBzdGF0ZSA9IHVybC5zZWFyY2hQYXJhbXMuZ2V0KCJzdGF0ZSIpOwoKICAgIGlmICghY29kZSB8fCAhc3RhdGUpIHsKICAgICAgcmV0dXJuIFJlc3BvbnNlLnJlZGlyZWN0KAogICAgICAgIG5ldyBVUkwoCiAgICAgICAgICAiL2xvZ2luP2Vycm9yPSIgKyBlbmNvZGVVUklDb21wb25lbnQoIkludmFsaWQgT0F1dGggY2FsbGJhY2siKSwKICAgICAgICAgIHJlcS51cmwKICAgICAgICApLAogICAgICAgIDMwMgogICAgICApOwogICAgfQoKICAgIGNvbnN0IHRlbXBPQXV0aENsaWVudCA9IG5ldyBPQXV0aENsaWVudChvYXV0aENvbmZpZywgb2F1dGhTdG9yYWdlLCAidGVtcCIpOwogICAgY29uc3QgdG9rZW5zID0gYXdhaXQgdGVtcE9BdXRoQ2xpZW50LmhhbmRsZUNhbGxiYWNrKHsgY29kZSwgc3RhdGUgfSk7CiAgICBjb25zdCBzZXNzaW9uSWQgPSBhd2FpdCBvYXV0aFNlc3Npb25zLmNyZWF0ZU9BdXRoU2Vzc2lvbih0b2tlbnMpOwoKICAgIGlmICghc2Vzc2lvbklkKSB7CiAgICAgIHJldHVybiBSZXNwb25zZS5yZWRpcmVjdCgKICAgICAgICBuZXcgVVJMKAogICAgICAgICAgIi9sb2dpbj9lcnJvcj0iICsgZW5jb2RlVVJJQ29tcG9uZW50KCJGYWlsZWQgdG8gY3JlYXRlIHNlc3Npb24iKSwKICAgICAgICAgIHJlcS51cmwKICAgICAgICApLAogICAgICAgIDMwMgogICAgICApOwogICAgfQoKICAgIGNvbnN0IHNlc3Npb25Db29raWUgPSBzZXNzaW9uU3RvcmUuY3JlYXRlU2Vzc2lvbkNvb2tpZShzZXNzaW9uSWQpOwoKICAgIGxldCB1c2VySW5mbzsKICAgIHRyeSB7CiAgICAgIGNvbnN0IHNlc3Npb25PQXV0aENsaWVudCA9IGNyZWF0ZU9BdXRoQ2xpZW50KHNlc3Npb25JZCk7CiAgICAgIHVzZXJJbmZvID0gYXdhaXQgc2Vzc2lvbk9BdXRoQ2xpZW50LmdldFVzZXJJbmZvKCk7CiAgICB9IGNhdGNoIChlcnJvcikgewogICAgICBjb25zb2xlLmVycm9yKCJGYWlsZWQgdG8gZ2V0IHVzZXIgaW5mbzoiLCBlcnJvcik7CiAgICB9CgogICAgaWYgKHVzZXJJbmZvPy5zdWIpIHsKICAgICAgdHJ5IHsKICAgICAgICBjb25zdCB1c2VyQ2xpZW50ID0gY3JlYXRlU2Vzc2lvbkNsaWVudChzZXNzaW9uSWQpOwogICAgICAgIGF3YWl0IHVzZXJDbGllbnQuc3luY1VzZXJDb2xsZWN0aW9ucygpOwogICAgICAgIGNvbnNvbGUubG9nKCJTeW5jZWQgQmx1ZXNreSBwcm9maWxlIGZvciIsIHVzZXJJbmZvLnN1Yik7CiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc29sZS5lcnJvcigiRXJyb3Igc3luY2luZyBCbHVlc2t5IHByb2ZpbGU6IiwgZXJyb3IpOwogICAgICB9CiAgICB9CgogICAgcmV0dXJuIG5ldyBSZXNwb25zZShudWxsLCB7CiAgICAgIHN0YXR1czogMzAyLAogICAgICBoZWFkZXJzOiB7CiAgICAgICAgTG9jYXRpb246IG5ldyBVUkwoIi9kYXNoYm9hcmQiLCByZXEudXJsKS50b1N0cmluZygpLAogICAgICAgICJTZXQtQ29va2llIjogc2Vzc2lvbkNvb2tpZSwKICAgICAgfSwKICAgIH0pOwogIH0gY2F0Y2ggKGVycm9yKSB7CiAgICBjb25zb2xlLmVycm9yKCJPQXV0aCBjYWxsYmFjayBlcnJvcjoiLCBlcnJvcik7CiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QoCiAgICAgIG5ldyBVUkwoCiAgICAgICAgIi9sb2dpbj9lcnJvcj0iICsgZW5jb2RlVVJJQ29tcG9uZW50KCJBdXRoZW50aWNhdGlvbiBmYWlsZWQiKSwKICAgICAgICByZXEudXJsCiAgICAgICksCiAgICAgIDMwMgogICAgKTsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIGhhbmRsZUxvZ291dChyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiB7CiAgY29uc3Qgc2Vzc2lvbiA9IGF3YWl0IHNlc3Npb25TdG9yZS5nZXRTZXNzaW9uRnJvbVJlcXVlc3QocmVxKTsKCiAgaWYgKHNlc3Npb24pIHsKICAgIGF3YWl0IG9hdXRoU2Vzc2lvbnMubG9nb3V0KHNlc3Npb24uc2Vzc2lvbklkKTsKICB9CgogIGNvbnN0IGNsZWFyQ29va2llID0gc2Vzc2lvblN0b3JlLmNyZWF0ZUxvZ291dENvb2tpZSgpOwoKICByZXR1cm4gbmV3IFJlc3BvbnNlKG51bGwsIHsKICAgIHN0YXR1czogMzAyLAogICAgaGVhZGVyczogewogICAgICBMb2NhdGlvbjogbmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCkudG9TdHJpbmcoKSwKICAgICAgIlNldC1Db29raWUiOiBjbGVhckNvb2tpZSwKICAgIH0sCiAgfSk7Cn0KCmV4cG9ydCBjb25zdCBhdXRoUm91dGVzOiBSb3V0ZVtdID0gWwogIHsKICAgIG1ldGhvZDogIkdFVCIsCiAgICBwYXR0ZXJuOiBuZXcgVVJMUGF0dGVybih7IHBhdGhuYW1lOiAiL2xvZ2luIiB9KSwKICAgIGhhbmRsZXI6IGhhbmRsZUxvZ2luUGFnZSwKICB9LAogIHsKICAgIG1ldGhvZDogIlBPU1QiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi9vYXV0aC9hdXRob3JpemUiIH0pLAogICAgaGFuZGxlcjogaGFuZGxlT0F1dGhBdXRob3JpemUsCiAgfSwKICB7CiAgICBtZXRob2Q6ICJHRVQiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi9vYXV0aC9jYWxsYmFjayIgfSksCiAgICBoYW5kbGVyOiBoYW5kbGVPQXV0aENhbGxiYWNrLAogIH0sCiAgewogICAgbWV0aG9kOiAiUE9TVCIsCiAgICBwYXR0ZXJuOiBuZXcgVVJMUGF0dGVybih7IHBhdGhuYW1lOiAiL2xvZ291dCIgfSksCiAgICBoYW5kbGVyOiBoYW5kbGVMb2dvdXQsCiAgfSwKXTsK" 50 }, 51 { 52 + "template": "deno-ssr", 53 "path": "src/features/dashboard/templates/DashboardPage.tsx", 54 "content": "aW1wb3J0IHsgTGF5b3V0IH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9MYXlvdXQudHN4IjsKaW1wb3J0IHsgQnV0dG9uIH0gZnJvbSAiLi4vLi4vLi4vc2hhcmVkL2ZyYWdtZW50cy9CdXR0b24udHN4IjsKaW1wb3J0IHR5cGUgeyBBcHBCc2t5QWN0b3JQcm9maWxlIH0gZnJvbSAiLi4vLi4vLi4vZ2VuZXJhdGVkX2NsaWVudC50cyI7CgppbnRlcmZhY2UgRGFzaGJvYXJkUGFnZVByb3BzIHsKICBjdXJyZW50VXNlcjogewogICAgbmFtZT86IHN0cmluZzsKICAgIHN1Yjogc3RyaW5nOwogIH07CiAgcHJvZmlsZT86IEFwcEJza3lBY3RvclByb2ZpbGU7CiAgYXZhdGFyVXJsPzogc3RyaW5nOwp9CgpleHBvcnQgZnVuY3Rpb24gRGFzaGJvYXJkUGFnZSh7CiAgY3VycmVudFVzZXIsCiAgcHJvZmlsZSwKICBhdmF0YXJVcmwsCn06IERhc2hib2FyZFBhZ2VQcm9wcykgewogIHJldHVybiAoCiAgICA8TGF5b3V0IHRpdGxlPSJEYXNoYm9hcmQiPgogICAgICA8ZGl2IGNsYXNzTmFtZT0ibWluLWgtc2NyZWVuIGJnLWdyYXktNTAgcC04Ij4KICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ibWF4LXctMnhsIG14LWF1dG8iPgogICAgICAgICAgPGRpdiBjbGFzc05hbWU9ImJnLXdoaXRlIHJvdW5kZWQtbGcgc2hhZG93IHAtNiI+CiAgICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGp1c3RpZnktYmV0d2VlbiBpdGVtcy1jZW50ZXIgbWItNiI+CiAgICAgICAgICAgICAgPGgxIGNsYXNzTmFtZT0idGV4dC0yeGwgZm9udC1ib2xkIj5EYXNoYm9hcmQ8L2gxPgogICAgICAgICAgICAgIDxmb3JtIG1ldGhvZD0icG9zdCIgYWN0aW9uPSIvbG9nb3V0Ij4KICAgICAgICAgICAgICAgIDxCdXR0b24gdHlwZT0ic3VibWl0IiB2YXJpYW50PSJzZWNvbmRhcnkiPgogICAgICAgICAgICAgICAgICBMb2dvdXQKICAgICAgICAgICAgICAgIDwvQnV0dG9uPgogICAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ibWItNiI+CiAgICAgICAgICAgICAge2F2YXRhclVybCAmJiAoCiAgICAgICAgICAgICAgICA8aW1nCiAgICAgICAgICAgICAgICAgIHNyYz17YXZhdGFyVXJsfQogICAgICAgICAgICAgICAgICBhbHQ9IlByb2ZpbGUiCiAgICAgICAgICAgICAgICAgIGNsYXNzTmFtZT0idy0yMCBoLTIwIHJvdW5kZWQtZnVsbCBtYi00IgogICAgICAgICAgICAgICAgLz4KICAgICAgICAgICAgICApfQogICAgICAgICAgICAgIDxoMiBjbGFzc05hbWU9InRleHQteGwgZm9udC1zZW1pYm9sZCBtYi0yIj4KICAgICAgICAgICAgICAgIHtwcm9maWxlPy5kaXNwbGF5TmFtZSB8fCBjdXJyZW50VXNlci5uYW1lIHx8IGN1cnJlbnRVc2VyLnN1Yn0KICAgICAgICAgICAgICA8L2gyPgogICAgICAgICAgICAgIHtjdXJyZW50VXNlci5uYW1lICYmICgKICAgICAgICAgICAgICAgIDxwIGNsYXNzTmFtZT0idGV4dC1ncmF5LTYwMCBtYi0yIj5Ae2N1cnJlbnRVc2VyLm5hbWV9PC9wPgogICAgICAgICAgICAgICl9CiAgICAgICAgICAgICAge3Byb2ZpbGU/LmRlc2NyaXB0aW9uICYmICgKICAgICAgICAgICAgICAgIDxwIGNsYXNzTmFtZT0idGV4dC1ncmF5LTcwMCBtdC0yIj57cHJvZmlsZS5kZXNjcmlwdGlvbn08L3A+CiAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L0xheW91dD4KICApOwp9Cg==" 55 }, 56 { 57 + "template": "deno-ssr", 58 "path": "src/features/dashboard/handlers.tsx", 59 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IHdpdGhBdXRoIH0gZnJvbSAiLi4vLi4vcm91dGVzL21pZGRsZXdhcmUudHMiOwppbXBvcnQgeyByZW5kZXJIVE1MIH0gZnJvbSAiLi4vLi4vdXRpbHMvcmVuZGVyLnRzeCI7CmltcG9ydCB7IERhc2hib2FyZFBhZ2UgfSBmcm9tICIuL3RlbXBsYXRlcy9EYXNoYm9hcmRQYWdlLnRzeCI7CmltcG9ydCB7IHB1YmxpY0NsaWVudCB9IGZyb20gIi4uLy4uL2NvbmZpZy50cyI7CmltcG9ydCB7IHJlY29yZEJsb2JUb0NkblVybCB9IGZyb20gIkBzbGljZXMvY2xpZW50IjsKaW1wb3J0IHsgQXBwQnNreUFjdG9yUHJvZmlsZSB9IGZyb20gIi4uLy4uL2dlbmVyYXRlZF9jbGllbnQudHMiOwoKYXN5bmMgZnVuY3Rpb24gaGFuZGxlRGFzaGJvYXJkKHJlcTogUmVxdWVzdCk6IFByb21pc2U8UmVzcG9uc2U+IHsKICBjb25zdCBjb250ZXh0ID0gYXdhaXQgd2l0aEF1dGgocmVxKTsKCiAgaWYgKCFjb250ZXh0LmN1cnJlbnRVc2VyKSB7CiAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMik7CiAgfQoKICBsZXQgcHJvZmlsZTogQXBwQnNreUFjdG9yUHJvZmlsZSB8IHVuZGVmaW5lZDsKICBsZXQgYXZhdGFyVXJsOiBzdHJpbmcgfCB1bmRlZmluZWQ7CiAgdHJ5IHsKICAgIGNvbnN0IHByb2ZpbGVSZXN1bHQgPSBhd2FpdCBwdWJsaWNDbGllbnQuYXBwLmJza3kuYWN0b3IucHJvZmlsZS5nZXRSZWNvcmQoewogICAgICB1cmk6IGBhdDovLyR7Y29udGV4dC5jdXJyZW50VXNlci5zdWJ9L2FwcC5ic2t5LmFjdG9yLnByb2ZpbGUvc2VsZmAsCiAgICB9KTsKCiAgICBpZiAocHJvZmlsZVJlc3VsdCkgewogICAgICBwcm9maWxlID0gcHJvZmlsZVJlc3VsdC52YWx1ZTsKCiAgICAgIGlmIChwcm9maWxlLmF2YXRhcikgewogICAgICAgIGF2YXRhclVybCA9IHJlY29yZEJsb2JUb0NkblVybChwcm9maWxlUmVzdWx0LCBwcm9maWxlLmF2YXRhciwgImF2YXRhciIpOwogICAgICB9CiAgICB9CiAgfSBjYXRjaCAoZXJyb3IpIHsKICAgIGNvbnNvbGUuZXJyb3IoIkVycm9yIGZldGNoaW5nIHByb2ZpbGU6IiwgZXJyb3IpOwogIH0KCiAgcmV0dXJuIHJlbmRlckhUTUwoCiAgICA8RGFzaGJvYXJkUGFnZQogICAgICBjdXJyZW50VXNlcj17Y29udGV4dC5jdXJyZW50VXNlcn0KICAgICAgcHJvZmlsZT17cHJvZmlsZX0KICAgICAgYXZhdGFyVXJsPXthdmF0YXJVcmx9CiAgICAvPgogICk7Cn0KCmV4cG9ydCBjb25zdCBkYXNoYm9hcmRSb3V0ZXM6IFJvdXRlW10gPSBbCiAgewogICAgbWV0aG9kOiAiR0VUIiwKICAgIHBhdHRlcm46IG5ldyBVUkxQYXR0ZXJuKHsgcGF0aG5hbWU6ICIvZGFzaGJvYXJkIiB9KSwKICAgIGhhbmRsZXI6IGhhbmRsZURhc2hib2FyZCwKICB9LApdOwo=" 60 }, 61 { 62 + "template": "deno-ssr", 63 "path": "src/utils/cn.ts", 64 "content": "aW1wb3J0IHsgdHlwZSBDbGFzc1ZhbHVlLCBjbHN4IH0gZnJvbSAiY2xzeCI7CmltcG9ydCB7IHR3TWVyZ2UgfSBmcm9tICJ0YWlsd2luZC1tZXJnZSI7CgpleHBvcnQgZnVuY3Rpb24gY24oLi4uaW5wdXRzOiBDbGFzc1ZhbHVlW10pOiBzdHJpbmcgewogIHJldHVybiB0d01lcmdlKGNsc3goaW5wdXRzKSk7Cn0=" 65 }, 66 { 67 + "template": "deno-ssr", 68 "path": "src/utils/logging.ts", 69 "content": "aW1wb3J0IHsgY3lhbiwgZ3JlZW4sIHJlZCwgeWVsbG93LCBib2xkLCBkaW0gfSBmcm9tICJAc3RkL2ZtdC9jb2xvcnMiOwoKZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUxvZ2dpbmdIYW5kbGVyKAogIGhhbmRsZXI6IChyZXE6IFJlcXVlc3QpID0+IFJlc3BvbnNlIHwgUHJvbWlzZTxSZXNwb25zZT4KKSB7CiAgcmV0dXJuIGFzeW5jIChyZXE6IFJlcXVlc3QpOiBQcm9taXNlPFJlc3BvbnNlPiA9PiB7CiAgICBjb25zdCBzdGFydCA9IERhdGUubm93KCk7CiAgICBjb25zdCBtZXRob2QgPSByZXEubWV0aG9kOwogICAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKTsKCiAgICB0cnkgewogICAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IFByb21pc2UucmVzb2x2ZShoYW5kbGVyKHJlcSkpOwogICAgICBjb25zdCBkdXJhdGlvbiA9IERhdGUubm93KCkgLSBzdGFydDsKCiAgICAgIGNvbnN0IG1ldGhvZENvbG9yID0gY3lhbihib2xkKG1ldGhvZCkpOwogICAgICBjb25zdCBzdGF0dXNDb2xvciA9CiAgICAgICAgcmVzcG9uc2Uuc3RhdHVzID49IDIwMCAmJiByZXNwb25zZS5zdGF0dXMgPCAzMDAKICAgICAgICAgID8gZ3JlZW4oU3RyaW5nKHJlc3BvbnNlLnN0YXR1cykpCiAgICAgICAgICA6IHJlc3BvbnNlLnN0YXR1cyA+PSAzMDAgJiYgcmVzcG9uc2Uuc3RhdHVzIDwgNDAwCiAgICAgICAgICA/IHllbGxvdyhTdHJpbmcocmVzcG9uc2Uuc3RhdHVzKSkKICAgICAgICAgIDogcmVzcG9uc2Uuc3RhdHVzID49IDQwMAogICAgICAgICAgPyByZWQoU3RyaW5nKHJlc3BvbnNlLnN0YXR1cykpCiAgICAgICAgICA6IFN0cmluZyhyZXNwb25zZS5zdGF0dXMpOwogICAgICBjb25zdCBkdXJhdGlvblRleHQgPSBkaW0oYCgke2R1cmF0aW9ufW1zKWApOwoKICAgICAgY29uc29sZS5sb2coCiAgICAgICAgYCR7bWV0aG9kQ29sb3J9ICR7dXJsLnBhdGhuYW1lfSAtICR7c3RhdHVzQ29sb3J9ICR7ZHVyYXRpb25UZXh0fWAKICAgICAgKTsKICAgICAgcmV0dXJuIHJlc3BvbnNlOwogICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgY29uc3QgZHVyYXRpb24gPSBEYXRlLm5vdygpIC0gc3RhcnQ7CiAgICAgIGNvbnN0IG1ldGhvZENvbG9yID0gY3lhbihib2xkKG1ldGhvZCkpOwogICAgICBjb25zdCBlcnJvclRleHQgPSByZWQoYm9sZCgiRVJST1IiKSk7CiAgICAgIGNvbnN0IGR1cmF0aW9uVGV4dCA9IGRpbShgKCR7ZHVyYXRpb259bXMpYCk7CgogICAgICBjb25zb2xlLmVycm9yKAogICAgICAgIGAke21ldGhvZENvbG9yfSAke3VybC5wYXRobmFtZX0gLSAke2Vycm9yVGV4dH0gJHtkdXJhdGlvblRleHR9OmAsCiAgICAgICAgZXJyb3IKICAgICAgKTsKICAgICAgdGhyb3cgZXJyb3I7CiAgICB9CiAgfTsKfQo=" 70 }, 71 { 72 + "template": "deno-ssr", 73 "path": "src/utils/render.tsx", 74 "content": "aW1wb3J0IHsgcmVuZGVyVG9TdHJpbmcgfSBmcm9tICJwcmVhY3QtcmVuZGVyLXRvLXN0cmluZyI7CmltcG9ydCB7IFZOb2RlIH0gZnJvbSAicHJlYWN0IjsKCmV4cG9ydCBmdW5jdGlvbiByZW5kZXJIVE1MKGVsZW1lbnQ6IFZOb2RlKTogUmVzcG9uc2UgewogIGNvbnN0IGh0bWwgPSByZW5kZXJUb1N0cmluZyhlbGVtZW50KTsKCiAgcmV0dXJuIG5ldyBSZXNwb25zZShodG1sLCB7CiAgICBoZWFkZXJzOiB7CiAgICAgICJDb250ZW50LVR5cGUiOiAidGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04IiwKICAgIH0sCiAgfSk7Cn0=" 75 }, 76 { 77 + "template": "deno-ssr", 78 "path": "src/shared/fragments/Layout.tsx", 79 "content": "aW1wb3J0IHsgQ29tcG9uZW50Q2hpbGRyZW4gfSBmcm9tICJwcmVhY3QiOwoKaW50ZXJmYWNlIExheW91dFByb3BzIHsKICB0aXRsZT86IHN0cmluZzsKICBjaGlsZHJlbjogQ29tcG9uZW50Q2hpbGRyZW47Cn0KCmV4cG9ydCBmdW5jdGlvbiBMYXlvdXQoeyB0aXRsZSA9ICJBcHAiLCBjaGlsZHJlbiB9OiBMYXlvdXRQcm9wcykgewogIHJldHVybiAoCiAgICA8aHRtbCBsYW5nPSJlbiI+CiAgICAgIDxoZWFkPgogICAgICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wIiAvPgogICAgICAgIDx0aXRsZT57dGl0bGV9PC90aXRsZT4KICAgICAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4udGFpbHdpbmRjc3MuY29tIj48L3NjcmlwdD4KICAgICAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly91bnBrZy5jb20vaHRteC5vcmdAMS45LjEwIj48L3NjcmlwdD4KICAgICAgPC9oZWFkPgogICAgICA8Ym9keT4KICAgICAgICB7Y2hpbGRyZW59CiAgICAgIDwvYm9keT4KICAgIDwvaHRtbD4KICApOwp9" 80 }, 81 { 82 + "template": "deno-ssr", 83 "path": "src/shared/fragments/Button.tsx", 84 "content": "aW1wb3J0IHsgQ29tcG9uZW50Q2hpbGRyZW4sIEpTWCB9IGZyb20gInByZWFjdCI7CmltcG9ydCB7IGNuIH0gZnJvbSAiLi4vLi4vdXRpbHMvY24udHMiOwoKaW50ZXJmYWNlIEJ1dHRvblByb3BzIGV4dGVuZHMgT21pdDxKU1guSW50cmluc2ljRWxlbWVudHNbJ2J1dHRvbiddLCAic2l6ZSI+IHsKICBjaGlsZHJlbjogQ29tcG9uZW50Q2hpbGRyZW47CiAgdmFyaWFudD86ICJwcmltYXJ5IiB8ICJzZWNvbmRhcnkiIHwgImRhbmdlciI7CiAgc2l6ZT86ICJzbSIgfCAibWQiIHwgImxnIjsKfQoKZXhwb3J0IGZ1bmN0aW9uIEJ1dHRvbih7CiAgY2hpbGRyZW4sCiAgdHlwZSA9ICJidXR0b24iLAogIHZhcmlhbnQgPSAicHJpbWFyeSIsCiAgc2l6ZSA9ICJtZCIsCiAgY2xhc3NOYW1lLAogIGRpc2FibGVkLAogIC4uLnByb3BzCn06IEJ1dHRvblByb3BzKSB7CiAgY29uc3QgYmFzZUNsYXNzZXMgPQogICAgImlubGluZS1mbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWNlbnRlciBmb250LW1lZGl1bSByb3VuZGVkLW1kIGZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpyaW5nLTIgZm9jdXM6cmluZy1vZmZzZXQtMiBkaXNhYmxlZDpvcGFjaXR5LTUwIGRpc2FibGVkOmN1cnNvci1ub3QtYWxsb3dlZCI7CgogIGNvbnN0IHZhcmlhbnRDbGFzc2VzID0gewogICAgcHJpbWFyeTogImJnLWJsdWUtNjAwIGhvdmVyOmJnLWJsdWUtNzAwIHRleHQtd2hpdGUgZm9jdXM6cmluZy1ibHVlLTUwMCIsCiAgICBzZWNvbmRhcnk6CiAgICAgICJiZy1ncmF5LTIwMCBob3ZlcjpiZy1ncmF5LTMwMCB0ZXh0LWdyYXktOTAwIGZvY3VzOnJpbmctZ3JheS01MDAiLAogICAgZGFuZ2VyOiAiYmctcmVkLTYwMCBob3ZlcjpiZy1yZWQtNzAwIHRleHQtd2hpdGUgZm9jdXM6cmluZy1yZWQtNTAwIiwKICB9OwoKICBjb25zdCBzaXplQ2xhc3NlcyA9IHsKICAgIHNtOiAicHgtMyBweS0xLjUgdGV4dC1zbSIsCiAgICBtZDogInB4LTQgcHktMiB0ZXh0LXNtIiwKICAgIGxnOiAicHgtNiBweS0zIHRleHQtYmFzZSIsCiAgfTsKCiAgcmV0dXJuICgKICAgIDxidXR0b24KICAgICAgdHlwZT17dHlwZX0KICAgICAgZGlzYWJsZWQ9e2Rpc2FibGVkfQogICAgICBjbGFzc05hbWU9e2NuKAogICAgICAgIGJhc2VDbGFzc2VzLAogICAgICAgIHZhcmlhbnRDbGFzc2VzW3ZhcmlhbnRdLAogICAgICAgIHNpemVDbGFzc2VzW3NpemVdLAogICAgICAgIGNsYXNzTmFtZQogICAgICApfQogICAgICB7Li4ucHJvcHN9CiAgICA+CiAgICAgIHtjaGlsZHJlbn0KICAgIDwvYnV0dG9uPgogICk7Cn0K" 85 }, 86 { 87 + "template": "deno-ssr", 88 "path": "src/shared/fragments/Input.tsx", 89 "content": "aW1wb3J0IHsgSlNYIH0gZnJvbSAicHJlYWN0IjsKaW1wb3J0IHsgY24gfSBmcm9tICIuLi8uLi91dGlscy9jbi50cyI7Cgp0eXBlIElucHV0UHJvcHMgPSBKU1guSW50cmluc2ljRWxlbWVudHNbJ2lucHV0J107CgpleHBvcnQgZnVuY3Rpb24gSW5wdXQoewogIHR5cGUgPSAidGV4dCIsCiAgY2xhc3NOYW1lLAogIC4uLnByb3BzCn06IElucHV0UHJvcHMpIHsKICByZXR1cm4gKAogICAgPGlucHV0CiAgICAgIHR5cGU9e3R5cGV9CiAgICAgIGNsYXNzTmFtZT17Y24oCiAgICAgICAgImJsb2NrIHctZnVsbCBweC0zIHB5LTIgYm9yZGVyIGJvcmRlci1ncmF5LTMwMCByb3VuZGVkLW1kIHNoYWRvdy1zbSIsCiAgICAgICAgImZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpyaW5nLWJsdWUtNTAwIGZvY3VzOmJvcmRlci1ibHVlLTUwMCIsCiAgICAgICAgImRpc2FibGVkOmJnLWdyYXktNTAgZGlzYWJsZWQ6dGV4dC1ncmF5LTUwMCIsCiAgICAgICAgY2xhc3NOYW1lCiAgICAgICl9CiAgICAgIHsuLi5wcm9wc30KICAgIC8+CiAgKTsKfQ==" 90 }, 91 { 92 + "template": "deno-ssr", 93 "path": "src/config.ts", 94 + "content": "aW1wb3J0IHsgT0F1dGhDbGllbnQsIFNRTGl0ZU9BdXRoU3RvcmFnZSB9IGZyb20gIkBzbGljZXMvb2F1dGgiOwppbXBvcnQgeyBTZXNzaW9uU3RvcmUsIFNRTGl0ZUFkYXB0ZXIsIHdpdGhPQXV0aFNlc3Npb24gfSBmcm9tICJAc2xpY2VzL3Nlc3Npb24iOwppbXBvcnQgeyBBdFByb3RvQ2xpZW50IH0gZnJvbSAiLi9nZW5lcmF0ZWRfY2xpZW50LnRzIjsKCmNvbnN0IE9BVVRIX0NMSUVOVF9JRCA9IERlbm8uZW52LmdldCgiT0FVVEhfQ0xJRU5UX0lEIik7CmNvbnN0IE9BVVRIX0NMSUVOVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoIk9BVVRIX0NMSUVOVF9TRUNSRVQiKTsKY29uc3QgT0FVVEhfUkVESVJFQ1RfVVJJID0gRGVuby5lbnYuZ2V0KCJPQVVUSF9SRURJUkVDVF9VUkkiKTsKY29uc3QgT0FVVEhfQUlQX0JBU0VfVVJMID0gRGVuby5lbnYuZ2V0KCJPQVVUSF9BSVBfQkFTRV9VUkwiKTsKY29uc3QgQVBJX1VSTCA9IERlbm8uZW52LmdldCgiQVBJX1VSTCIpOwpleHBvcnQgY29uc3QgU0xJQ0VfVVJJID0gRGVuby5lbnYuZ2V0KCJTTElDRV9VUkkiKTsKCmlmICgKICAhT0FVVEhfQ0xJRU5UX0lEIHx8CiAgIU9BVVRIX0NMSUVOVF9TRUNSRVQgfHwKICAhT0FVVEhfUkVESVJFQ1RfVVJJIHx8CiAgIU9BVVRIX0FJUF9CQVNFX1VSTCB8fAogICFBUElfVVJMIHx8CiAgIVNMSUNFX1VSSQopIHsKICB0aHJvdyBuZXcgRXJyb3IoCiAgICAiTWlzc2luZyBPQXV0aCBjb25maWd1cmF0aW9uLiBQbGVhc2UgZW5zdXJlIC5lbnYgZmlsZSBjb250YWluczpcbiIgKwogICAgICAiT0FVVEhfQ0xJRU5UX0lELCBPQVVUSF9DTElFTlRfU0VDUkVULCBPQVVUSF9SRURJUkVDVF9VUkksIE9BVVRIX0FJUF9CQVNFX1VSTCwgQVBJX1VSTCwgU0xJQ0VfVVJJIgogICk7Cn0KCmNvbnN0IERBVEFCQVNFX1VSTCA9IERlbm8uZW52LmdldCgiREFUQUJBU0VfVVJMIikgfHwgInNsaWNlcy5kYiI7CgovLyBPQXV0aCBzZXR1cApjb25zdCBvYXV0aFN0b3JhZ2UgPSBuZXcgU1FMaXRlT0F1dGhTdG9yYWdlKERBVEFCQVNFX1VSTCk7CmNvbnN0IG9hdXRoQ29uZmlnID0gewogIGNsaWVudElkOiBPQVVUSF9DTElFTlRfSUQsCiAgY2xpZW50U2VjcmV0OiBPQVVUSF9DTElFTlRfU0VDUkVULAogIGF1dGhCYXNlVXJsOiBPQVVUSF9BSVBfQkFTRV9VUkwsCiAgcmVkaXJlY3RVcmk6IE9BVVRIX1JFRElSRUNUX1VSSSwKICBzY29wZXM6IFsiYXRwcm90byIsICJvcGVuaWQiLCAicHJvZmlsZSIsICJ0cmFuc2l0aW9uOmdlbmVyaWMiXSwKfTsKCi8vIEV4cG9ydCBjb25maWcgYW5kIHN0b3JhZ2UgZm9yIGNyZWF0aW5nIHVzZXItc2NvcGVkIGNsaWVudHMKZXhwb3J0IHsgb2F1dGhDb25maWcsIG9hdXRoU3RvcmFnZSB9OwoKLy8gU2Vzc2lvbiBzZXR1cCAoc2hhcmVkIGRhdGFiYXNlKQpleHBvcnQgY29uc3Qgc2Vzc2lvblN0b3JlID0gbmV3IFNlc3Npb25TdG9yZSh7CiAgYWRhcHRlcjogbmV3IFNRTGl0ZUFkYXB0ZXIoREFUQUJBU0VfVVJMKSwKICBjb29raWVOYW1lOiAie3tQUk9KRUNUX05BTUV9fS1zZXNzaW9uIiwKICBjb29raWVPcHRpb25zOiB7CiAgICBodHRwT25seTogdHJ1ZSwKICAgIHNlY3VyZTogRGVuby5lbnYuZ2V0KCJERU5PX0VOViIpID09PSAicHJvZHVjdGlvbiIsCiAgICBzYW1lU2l0ZTogImxheCIsCiAgICBwYXRoOiAiLyIsCiAgfSwKfSk7CgovLyBPQXV0aCArIFNlc3Npb24gaW50ZWdyYXRpb24KZXhwb3J0IGNvbnN0IG9hdXRoU2Vzc2lvbnMgPSB3aXRoT0F1dGhTZXNzaW9uKAogIHNlc3Npb25TdG9yZSwKICBvYXV0aENvbmZpZywKICBvYXV0aFN0b3JhZ2UsCiAgewogICAgYXV0b1JlZnJlc2g6IHRydWUsCiAgfQopOwoKLy8gSGVscGVyIGZ1bmN0aW9uIHRvIGNyZWF0ZSB1c2VyLXNjb3BlZCBPQXV0aCBjbGllbnQKZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9BdXRoQ2xpZW50KHNlc3Npb25JZDogc3RyaW5nKTogT0F1dGhDbGllbnQgewogIHJldHVybiBuZXcgT0F1dGhDbGllbnQob2F1dGhDb25maWcsIG9hdXRoU3RvcmFnZSwgc2Vzc2lvbklkKTsKfQoKLy8gSGVscGVyIGZ1bmN0aW9uIHRvIGNyZWF0ZSBhdXRoZW50aWNhdGVkIEF0UHJvdG8gY2xpZW50IGZvciBhIHVzZXIKZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZVNlc3Npb25DbGllbnQoc2Vzc2lvbklkOiBzdHJpbmcpOiBBdFByb3RvQ2xpZW50IHsKICBjb25zdCB1c2VyT0F1dGhDbGllbnQgPSBjcmVhdGVPQXV0aENsaWVudChzZXNzaW9uSWQpOwogIHJldHVybiBuZXcgQXRQcm90b0NsaWVudChBUElfVVJMISwgU0xJQ0VfVVJJISwgdXNlck9BdXRoQ2xpZW50KTsKfQoKLy8gUHVibGljIGNsaWVudCBmb3IgdW5hdXRoZW50aWNhdGVkIHJlcXVlc3RzCmV4cG9ydCBjb25zdCBwdWJsaWNDbGllbnQgPSBuZXcgQXRQcm90b0NsaWVudChBUElfVVJMLCBTTElDRV9VUkkpOwo=" 95 }, 96 { 97 + "template": "deno-ssr", 98 "path": "src/routes/middleware.ts", 99 "content": "aW1wb3J0IHsgc2Vzc2lvblN0b3JlLCBjcmVhdGVPQXV0aENsaWVudCB9IGZyb20gIi4uL2NvbmZpZy50cyI7CgpleHBvcnQgaW50ZXJmYWNlIEF1dGhDb250ZXh0IHsKICBjdXJyZW50VXNlcjogewogICAgc3ViOiBzdHJpbmc7CiAgICBuYW1lPzogc3RyaW5nOwogICAgZW1haWw/OiBzdHJpbmc7CiAgfSB8IG51bGw7CiAgc2Vzc2lvbklkOiBzdHJpbmcgfCBudWxsOwp9CgpleHBvcnQgYXN5bmMgZnVuY3Rpb24gd2l0aEF1dGgocmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxBdXRoQ29udGV4dD4gewogIGNvbnN0IHNlc3Npb24gPSBhd2FpdCBzZXNzaW9uU3RvcmUuZ2V0U2Vzc2lvbkZyb21SZXF1ZXN0KHJlcSk7CgogIGlmICghc2Vzc2lvbikgewogICAgcmV0dXJuIHsgY3VycmVudFVzZXI6IG51bGwsIHNlc3Npb25JZDogbnVsbCB9OwogIH0KCiAgdHJ5IHsKICAgIGNvbnN0IHNlc3Npb25PQXV0aENsaWVudCA9IGNyZWF0ZU9BdXRoQ2xpZW50KHNlc3Npb24uc2Vzc2lvbklkKTsKICAgIGNvbnN0IHVzZXJJbmZvID0gYXdhaXQgc2Vzc2lvbk9BdXRoQ2xpZW50LmdldFVzZXJJbmZvKCk7CiAgICByZXR1cm4gewogICAgICBjdXJyZW50VXNlcjogdXNlckluZm8gfHwgbnVsbCwKICAgICAgc2Vzc2lvbklkOiBzZXNzaW9uLnNlc3Npb25JZCwKICAgIH07CiAgfSBjYXRjaCB7CiAgICByZXR1cm4geyBjdXJyZW50VXNlcjogbnVsbCwgc2Vzc2lvbklkOiBzZXNzaW9uLnNlc3Npb25JZCB9OwogIH0KfQoKZXhwb3J0IGZ1bmN0aW9uIHJlcXVpcmVBdXRoKAogIGhhbmRsZXI6IChyZXE6IFJlcXVlc3QsIGNvbnRleHQ6IEF1dGhDb250ZXh0KSA9PiBQcm9taXNlPFJlc3BvbnNlPgopIHsKICByZXR1cm4gYXN5bmMgKHJlcTogUmVxdWVzdCk6IFByb21pc2U8UmVzcG9uc2U+ID0+IHsKICAgIGNvbnN0IGNvbnRleHQgPSBhd2FpdCB3aXRoQXV0aChyZXEpOwoKICAgIGlmICghY29udGV4dC5jdXJyZW50VXNlcikgewogICAgICByZXR1cm4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMik7CiAgICB9CgogICAgcmV0dXJuIGhhbmRsZXIocmVxLCBjb250ZXh0KTsKICB9Owp9Cg==" 100 }, 101 { 102 + "template": "deno-ssr", 103 "path": "src/routes/mod.ts", 104 "content": "aW1wb3J0IHR5cGUgeyBSb3V0ZSB9IGZyb20gIkBzdGQvaHR0cC91bnN0YWJsZS1yb3V0ZSI7CmltcG9ydCB7IGF1dGhSb3V0ZXMgfSBmcm9tICIuLi9mZWF0dXJlcy9hdXRoL2hhbmRsZXJzLnRzeCI7CmltcG9ydCB7IGRhc2hib2FyZFJvdXRlcyB9IGZyb20gIi4uL2ZlYXR1cmVzL2Rhc2hib2FyZC9oYW5kbGVycy50c3giOwoKZXhwb3J0IGNvbnN0IGFsbFJvdXRlczogUm91dGVbXSA9IFsKICAvLyBSb290IHJlZGlyZWN0IHRvIGxvZ2luIGZvciBub3cKICB7CiAgICBtZXRob2Q6ICJHRVQiLAogICAgcGF0dGVybjogbmV3IFVSTFBhdHRlcm4oeyBwYXRobmFtZTogIi8iIH0pLAogICAgaGFuZGxlcjogKHJlcSkgPT4gUmVzcG9uc2UucmVkaXJlY3QobmV3IFVSTCgiL2xvZ2luIiwgcmVxLnVybCksIDMwMiksCiAgfSwKCiAgLy8gQXV0aCByb3V0ZXMKICAuLi5hdXRoUm91dGVzLAoKICAvLyBEYXNoYm9hcmQgcm91dGVzCiAgLi4uZGFzaGJvYXJkUm91dGVzLApdOw==" 105 } 106 ]; 107 108 + export const AVAILABLE_TEMPLATES = ["deno-ssr"]; 109 + 110 + export function getTemplateContent(templateName: string, path: string): Uint8Array | undefined { 111 + const template = EMBEDDED_TEMPLATES.find(t => t.template === templateName && t.path === path); 112 if (!template) return undefined; 113 114 const binaryString = atob(template.content); ··· 131 } 132 return result; 133 } 134 + 135 + export function getTemplatesForName(templateName: string): Map<string, Uint8Array> { 136 + const result = new Map<string, Uint8Array>(); 137 + for (const template of EMBEDDED_TEMPLATES) { 138 + if (template.template !== templateName) continue; 139 + 140 + const binaryString = atob(template.content); 141 + const bytes = new Uint8Array(binaryString.length); 142 + for (let i = 0; i < binaryString.length; i++) { 143 + bytes[i] = binaryString.charCodeAt(i); 144 + } 145 + result.set(template.path, bytes); 146 + } 147 + return result; 148 + }