+366
-27
api/Cargo.lock
+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
+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
+35
api/src/database/actors.rs
···
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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
api/src/models.rs
+19
-4
docs/getting-started.md
+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
+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
+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
+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
-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
}
-91
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
-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
+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
-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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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,
+6
-3
frontend/src/utils/time.ts
+6
-3
frontend/src/utils/time.ts
···
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
+1
-1
frontend.fly.toml
+42
-14
packages/cli/scripts/embed-templates.ts
+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
+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
+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
+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
+
}