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