+6
-2
.gitignore
+6
-2
.gitignore
+9
.pre-commit-config.yaml
+9
.pre-commit-config.yaml
-20
.tangled/workflows/check.yaml
-20
.tangled/workflows/check.yaml
···
1
-
engine: nixery
2
-
3
-
when:
4
-
- event: ["push", "pull_request"]
5
-
6
-
dependencies:
7
-
nixpkgs:
8
-
- rustc
9
-
- cargo
10
-
- rustfmt
11
-
- clippy
12
-
13
-
steps:
14
-
- name: check formatting
15
-
command: |
16
-
cargo fmt --check
17
-
18
-
- name: run clippy
19
-
command: |
20
-
cargo clippy -- -D warnings
+27
.tangled/workflows/ci.yaml
+27
.tangled/workflows/ci.yaml
···
1
+
when:
2
+
- event: ["push"]
3
+
branch: main
4
+
5
+
engine: nixery
6
+
7
+
dependencies:
8
+
nixpkgs:
9
+
- bun
10
+
- curl
11
+
12
+
environment:
13
+
WISP_DID: "did:plc:xbtmt2zjwlrfegqvch7fboei"
14
+
WISP_SITE_NAME: "at-me"
15
+
16
+
steps:
17
+
- name: install dependencies
18
+
command: bun install
19
+
20
+
- name: build
21
+
command: bun run build
22
+
23
+
- name: deploy to wisp
24
+
command: |
25
+
curl -sSL https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
26
+
chmod +x wisp-cli
27
+
./wisp-cli deploy "$WISP_DID" --path ./dist --site "$WISP_SITE_NAME" --password "$WISP_APP_PASSWORD"
-14
.tangled/workflows/deploy.yaml
-14
.tangled/workflows/deploy.yaml
+28
CLAUDE.md
+28
CLAUDE.md
···
1
+
# at-me
2
+
3
+
ATProto PDS visualization tool - shows your identity, apps, and data collections.
4
+
5
+
## Tech Stack
6
+
7
+
- Pure client-side JavaScript (no backend)
8
+
- Vite for development and building
9
+
- Direct ATProto API calls (PDS, PLC directory, Bluesky AppView)
10
+
- Jetstream WebSocket for firehose streaming
11
+
- Client-side MST (Merkle Search Tree) visualization
12
+
13
+
## Development
14
+
15
+
- Use `bun run dev` for local development with hot reloading
16
+
- `bun run build` to build for production
17
+
- `bun run preview` to preview the production build
18
+
19
+
## Key files
20
+
21
+
- `index.html` - Landing page with handle search and atmosphere visualization
22
+
- `view.html` - Main visualization page showing PDS data, MST visualization
23
+
- `public/` - Static assets (favicon, OG image, OAuth metadata)
24
+
- `public/oauth-client-metadata.json` - OAuth client configuration for public clients
25
+
26
+
## Critical reminders
27
+
28
+
- Never deploy without explicit user request
-3598
Cargo.lock
-3598
Cargo.lock
···
1
-
# This file is automatically @generated by Cargo.
2
-
# It is not intended for manual editing.
3
-
version = 4
4
-
5
-
[[package]]
6
-
name = "actix-codec"
7
-
version = "0.5.2"
8
-
source = "registry+https://github.com/rust-lang/crates.io-index"
9
-
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
10
-
dependencies = [
11
-
"bitflags",
12
-
"bytes",
13
-
"futures-core",
14
-
"futures-sink",
15
-
"memchr",
16
-
"pin-project-lite",
17
-
"tokio",
18
-
"tokio-util",
19
-
"tracing",
20
-
]
21
-
22
-
[[package]]
23
-
name = "actix-http"
24
-
version = "3.11.2"
25
-
source = "registry+https://github.com/rust-lang/crates.io-index"
26
-
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
27
-
dependencies = [
28
-
"actix-codec",
29
-
"actix-rt",
30
-
"actix-service",
31
-
"actix-utils",
32
-
"base64 0.22.1",
33
-
"bitflags",
34
-
"brotli",
35
-
"bytes",
36
-
"bytestring",
37
-
"derive_more 2.0.1",
38
-
"encoding_rs",
39
-
"flate2",
40
-
"foldhash",
41
-
"futures-core",
42
-
"h2",
43
-
"http 0.2.12",
44
-
"httparse",
45
-
"httpdate",
46
-
"itoa",
47
-
"language-tags",
48
-
"local-channel",
49
-
"mime",
50
-
"percent-encoding",
51
-
"pin-project-lite",
52
-
"rand 0.9.2",
53
-
"sha1",
54
-
"smallvec",
55
-
"tokio",
56
-
"tokio-util",
57
-
"tracing",
58
-
"zstd",
59
-
]
60
-
61
-
[[package]]
62
-
name = "actix-macros"
63
-
version = "0.2.4"
64
-
source = "registry+https://github.com/rust-lang/crates.io-index"
65
-
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
66
-
dependencies = [
67
-
"quote",
68
-
"syn 2.0.106",
69
-
]
70
-
71
-
[[package]]
72
-
name = "actix-router"
73
-
version = "0.5.3"
74
-
source = "registry+https://github.com/rust-lang/crates.io-index"
75
-
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
76
-
dependencies = [
77
-
"bytestring",
78
-
"cfg-if",
79
-
"http 0.2.12",
80
-
"regex",
81
-
"regex-lite",
82
-
"serde",
83
-
"tracing",
84
-
]
85
-
86
-
[[package]]
87
-
name = "actix-rt"
88
-
version = "2.11.0"
89
-
source = "registry+https://github.com/rust-lang/crates.io-index"
90
-
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
91
-
dependencies = [
92
-
"futures-core",
93
-
"tokio",
94
-
]
95
-
96
-
[[package]]
97
-
name = "actix-server"
98
-
version = "2.6.0"
99
-
source = "registry+https://github.com/rust-lang/crates.io-index"
100
-
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
101
-
dependencies = [
102
-
"actix-rt",
103
-
"actix-service",
104
-
"actix-utils",
105
-
"futures-core",
106
-
"futures-util",
107
-
"mio",
108
-
"socket2 0.5.10",
109
-
"tokio",
110
-
"tracing",
111
-
]
112
-
113
-
[[package]]
114
-
name = "actix-service"
115
-
version = "2.0.3"
116
-
source = "registry+https://github.com/rust-lang/crates.io-index"
117
-
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
118
-
dependencies = [
119
-
"futures-core",
120
-
"pin-project-lite",
121
-
]
122
-
123
-
[[package]]
124
-
name = "actix-session"
125
-
version = "0.10.1"
126
-
source = "registry+https://github.com/rust-lang/crates.io-index"
127
-
checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f"
128
-
dependencies = [
129
-
"actix-service",
130
-
"actix-utils",
131
-
"actix-web",
132
-
"anyhow",
133
-
"derive_more 1.0.0",
134
-
"rand 0.8.5",
135
-
"serde",
136
-
"serde_json",
137
-
"tracing",
138
-
]
139
-
140
-
[[package]]
141
-
name = "actix-utils"
142
-
version = "3.0.1"
143
-
source = "registry+https://github.com/rust-lang/crates.io-index"
144
-
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
145
-
dependencies = [
146
-
"local-waker",
147
-
"pin-project-lite",
148
-
]
149
-
150
-
[[package]]
151
-
name = "actix-web"
152
-
version = "4.11.0"
153
-
source = "registry+https://github.com/rust-lang/crates.io-index"
154
-
checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
155
-
dependencies = [
156
-
"actix-codec",
157
-
"actix-http",
158
-
"actix-macros",
159
-
"actix-router",
160
-
"actix-rt",
161
-
"actix-server",
162
-
"actix-service",
163
-
"actix-utils",
164
-
"actix-web-codegen",
165
-
"bytes",
166
-
"bytestring",
167
-
"cfg-if",
168
-
"cookie",
169
-
"derive_more 2.0.1",
170
-
"encoding_rs",
171
-
"foldhash",
172
-
"futures-core",
173
-
"futures-util",
174
-
"impl-more",
175
-
"itoa",
176
-
"language-tags",
177
-
"log",
178
-
"mime",
179
-
"once_cell",
180
-
"pin-project-lite",
181
-
"regex",
182
-
"regex-lite",
183
-
"serde",
184
-
"serde_json",
185
-
"serde_urlencoded",
186
-
"smallvec",
187
-
"socket2 0.5.10",
188
-
"time",
189
-
"tracing",
190
-
"url",
191
-
]
192
-
193
-
[[package]]
194
-
name = "actix-web-codegen"
195
-
version = "4.3.0"
196
-
source = "registry+https://github.com/rust-lang/crates.io-index"
197
-
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
198
-
dependencies = [
199
-
"actix-router",
200
-
"proc-macro2",
201
-
"quote",
202
-
"syn 2.0.106",
203
-
]
204
-
205
-
[[package]]
206
-
name = "addr2line"
207
-
version = "0.25.1"
208
-
source = "registry+https://github.com/rust-lang/crates.io-index"
209
-
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
210
-
dependencies = [
211
-
"gimli",
212
-
]
213
-
214
-
[[package]]
215
-
name = "adler2"
216
-
version = "2.0.1"
217
-
source = "registry+https://github.com/rust-lang/crates.io-index"
218
-
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
219
-
220
-
[[package]]
221
-
name = "aead"
222
-
version = "0.5.2"
223
-
source = "registry+https://github.com/rust-lang/crates.io-index"
224
-
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
225
-
dependencies = [
226
-
"crypto-common",
227
-
"generic-array",
228
-
]
229
-
230
-
[[package]]
231
-
name = "aes"
232
-
version = "0.8.4"
233
-
source = "registry+https://github.com/rust-lang/crates.io-index"
234
-
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
235
-
dependencies = [
236
-
"cfg-if",
237
-
"cipher",
238
-
"cpufeatures",
239
-
]
240
-
241
-
[[package]]
242
-
name = "aes-gcm"
243
-
version = "0.10.3"
244
-
source = "registry+https://github.com/rust-lang/crates.io-index"
245
-
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
246
-
dependencies = [
247
-
"aead",
248
-
"aes",
249
-
"cipher",
250
-
"ctr",
251
-
"ghash",
252
-
"subtle",
253
-
]
254
-
255
-
[[package]]
256
-
name = "aho-corasick"
257
-
version = "1.1.3"
258
-
source = "registry+https://github.com/rust-lang/crates.io-index"
259
-
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
260
-
dependencies = [
261
-
"memchr",
262
-
]
263
-
264
-
[[package]]
265
-
name = "alloc-no-stdlib"
266
-
version = "2.0.4"
267
-
source = "registry+https://github.com/rust-lang/crates.io-index"
268
-
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
269
-
270
-
[[package]]
271
-
name = "alloc-stdlib"
272
-
version = "0.2.2"
273
-
source = "registry+https://github.com/rust-lang/crates.io-index"
274
-
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
275
-
dependencies = [
276
-
"alloc-no-stdlib",
277
-
]
278
-
279
-
[[package]]
280
-
name = "allocator-api2"
281
-
version = "0.2.21"
282
-
source = "registry+https://github.com/rust-lang/crates.io-index"
283
-
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
284
-
285
-
[[package]]
286
-
name = "android_system_properties"
287
-
version = "0.1.5"
288
-
source = "registry+https://github.com/rust-lang/crates.io-index"
289
-
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
290
-
dependencies = [
291
-
"libc",
292
-
]
293
-
294
-
[[package]]
295
-
name = "anstream"
296
-
version = "0.6.21"
297
-
source = "registry+https://github.com/rust-lang/crates.io-index"
298
-
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
299
-
dependencies = [
300
-
"anstyle",
301
-
"anstyle-parse",
302
-
"anstyle-query",
303
-
"anstyle-wincon",
304
-
"colorchoice",
305
-
"is_terminal_polyfill",
306
-
"utf8parse",
307
-
]
308
-
309
-
[[package]]
310
-
name = "anstyle"
311
-
version = "1.0.13"
312
-
source = "registry+https://github.com/rust-lang/crates.io-index"
313
-
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
314
-
315
-
[[package]]
316
-
name = "anstyle-parse"
317
-
version = "0.2.7"
318
-
source = "registry+https://github.com/rust-lang/crates.io-index"
319
-
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
320
-
dependencies = [
321
-
"utf8parse",
322
-
]
323
-
324
-
[[package]]
325
-
name = "anstyle-query"
326
-
version = "1.1.4"
327
-
source = "registry+https://github.com/rust-lang/crates.io-index"
328
-
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
329
-
dependencies = [
330
-
"windows-sys 0.60.2",
331
-
]
332
-
333
-
[[package]]
334
-
name = "anstyle-wincon"
335
-
version = "3.0.10"
336
-
source = "registry+https://github.com/rust-lang/crates.io-index"
337
-
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
338
-
dependencies = [
339
-
"anstyle",
340
-
"once_cell_polyfill",
341
-
"windows-sys 0.60.2",
342
-
]
343
-
344
-
[[package]]
345
-
name = "anyhow"
346
-
version = "1.0.100"
347
-
source = "registry+https://github.com/rust-lang/crates.io-index"
348
-
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
349
-
350
-
[[package]]
351
-
name = "async-compression"
352
-
version = "0.4.32"
353
-
source = "registry+https://github.com/rust-lang/crates.io-index"
354
-
checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
355
-
dependencies = [
356
-
"compression-codecs",
357
-
"compression-core",
358
-
"futures-core",
359
-
"pin-project-lite",
360
-
"tokio",
361
-
]
362
-
363
-
[[package]]
364
-
name = "async-lock"
365
-
version = "3.4.1"
366
-
source = "registry+https://github.com/rust-lang/crates.io-index"
367
-
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
368
-
dependencies = [
369
-
"event-listener",
370
-
"event-listener-strategy",
371
-
"pin-project-lite",
372
-
]
373
-
374
-
[[package]]
375
-
name = "async-trait"
376
-
version = "0.1.89"
377
-
source = "registry+https://github.com/rust-lang/crates.io-index"
378
-
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
379
-
dependencies = [
380
-
"proc-macro2",
381
-
"quote",
382
-
"syn 2.0.106",
383
-
]
384
-
385
-
[[package]]
386
-
name = "at-me"
387
-
version = "0.1.0"
388
-
dependencies = [
389
-
"actix-session",
390
-
"actix-web",
391
-
"atrium-api",
392
-
"atrium-common",
393
-
"atrium-identity",
394
-
"atrium-oauth",
395
-
"env_logger",
396
-
"hickory-resolver",
397
-
"log",
398
-
"serde",
399
-
"serde_json",
400
-
"tokio",
401
-
]
402
-
403
-
[[package]]
404
-
name = "atomic-waker"
405
-
version = "1.1.2"
406
-
source = "registry+https://github.com/rust-lang/crates.io-index"
407
-
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
408
-
409
-
[[package]]
410
-
name = "atrium-api"
411
-
version = "0.25.6"
412
-
source = "registry+https://github.com/rust-lang/crates.io-index"
413
-
checksum = "ef9d5e9352fd27d99383ae1db2b6a6aa239e683a7e750e8d73a73996d82b1fd2"
414
-
dependencies = [
415
-
"atrium-common",
416
-
"atrium-xrpc",
417
-
"chrono",
418
-
"http 1.3.1",
419
-
"ipld-core",
420
-
"langtag",
421
-
"regex",
422
-
"serde",
423
-
"serde_bytes",
424
-
"serde_json",
425
-
"thiserror",
426
-
"tokio",
427
-
"trait-variant",
428
-
]
429
-
430
-
[[package]]
431
-
name = "atrium-common"
432
-
version = "0.1.2"
433
-
source = "registry+https://github.com/rust-lang/crates.io-index"
434
-
checksum = "9ed5610654043faa396a5a15afac0ac646d76aebe45aebd7cef4f8b96b0ab7f4"
435
-
dependencies = [
436
-
"dashmap",
437
-
"lru",
438
-
"moka",
439
-
"thiserror",
440
-
"tokio",
441
-
"trait-variant",
442
-
"web-time",
443
-
]
444
-
445
-
[[package]]
446
-
name = "atrium-identity"
447
-
version = "0.1.7"
448
-
source = "registry+https://github.com/rust-lang/crates.io-index"
449
-
checksum = "4d3a56cd2bb695308cb078be80a46a7a2caf79203eda27803f13ee6a38b98378"
450
-
dependencies = [
451
-
"atrium-api",
452
-
"atrium-common",
453
-
"atrium-xrpc",
454
-
"serde",
455
-
"serde_html_form",
456
-
"serde_json",
457
-
"thiserror",
458
-
"trait-variant",
459
-
]
460
-
461
-
[[package]]
462
-
name = "atrium-oauth"
463
-
version = "0.1.5"
464
-
source = "registry+https://github.com/rust-lang/crates.io-index"
465
-
checksum = "6969f29ff0a4100d05d3988f012504385ff1d7c9db82410e26830ded8da621fb"
466
-
dependencies = [
467
-
"atrium-api",
468
-
"atrium-common",
469
-
"atrium-identity",
470
-
"atrium-xrpc",
471
-
"base64 0.22.1",
472
-
"chrono",
473
-
"dashmap",
474
-
"ecdsa",
475
-
"elliptic-curve",
476
-
"jose-jwa",
477
-
"jose-jwk",
478
-
"p256",
479
-
"rand 0.8.5",
480
-
"reqwest",
481
-
"serde",
482
-
"serde_html_form",
483
-
"serde_json",
484
-
"sha2",
485
-
"thiserror",
486
-
"tokio",
487
-
"trait-variant",
488
-
]
489
-
490
-
[[package]]
491
-
name = "atrium-xrpc"
492
-
version = "0.12.3"
493
-
source = "registry+https://github.com/rust-lang/crates.io-index"
494
-
checksum = "0216ad50ce34e9ff982e171c3659e65dedaa2ed5ac2994524debdc9a9647ffa8"
495
-
dependencies = [
496
-
"http 1.3.1",
497
-
"serde",
498
-
"serde_html_form",
499
-
"serde_json",
500
-
"thiserror",
501
-
"trait-variant",
502
-
]
503
-
504
-
[[package]]
505
-
name = "autocfg"
506
-
version = "1.5.0"
507
-
source = "registry+https://github.com/rust-lang/crates.io-index"
508
-
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
509
-
510
-
[[package]]
511
-
name = "backtrace"
512
-
version = "0.3.76"
513
-
source = "registry+https://github.com/rust-lang/crates.io-index"
514
-
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
515
-
dependencies = [
516
-
"addr2line",
517
-
"cfg-if",
518
-
"libc",
519
-
"miniz_oxide",
520
-
"object",
521
-
"rustc-demangle",
522
-
"windows-link",
523
-
]
524
-
525
-
[[package]]
526
-
name = "base-x"
527
-
version = "0.2.11"
528
-
source = "registry+https://github.com/rust-lang/crates.io-index"
529
-
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
530
-
531
-
[[package]]
532
-
name = "base16ct"
533
-
version = "0.2.0"
534
-
source = "registry+https://github.com/rust-lang/crates.io-index"
535
-
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
536
-
537
-
[[package]]
538
-
name = "base256emoji"
539
-
version = "1.0.2"
540
-
source = "registry+https://github.com/rust-lang/crates.io-index"
541
-
checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c"
542
-
dependencies = [
543
-
"const-str",
544
-
"match-lookup",
545
-
]
546
-
547
-
[[package]]
548
-
name = "base64"
549
-
version = "0.20.0"
550
-
source = "registry+https://github.com/rust-lang/crates.io-index"
551
-
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
552
-
553
-
[[package]]
554
-
name = "base64"
555
-
version = "0.22.1"
556
-
source = "registry+https://github.com/rust-lang/crates.io-index"
557
-
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
558
-
559
-
[[package]]
560
-
name = "base64ct"
561
-
version = "1.8.0"
562
-
source = "registry+https://github.com/rust-lang/crates.io-index"
563
-
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
564
-
565
-
[[package]]
566
-
name = "bitflags"
567
-
version = "2.9.4"
568
-
source = "registry+https://github.com/rust-lang/crates.io-index"
569
-
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
570
-
571
-
[[package]]
572
-
name = "block-buffer"
573
-
version = "0.10.4"
574
-
source = "registry+https://github.com/rust-lang/crates.io-index"
575
-
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
576
-
dependencies = [
577
-
"generic-array",
578
-
]
579
-
580
-
[[package]]
581
-
name = "brotli"
582
-
version = "8.0.2"
583
-
source = "registry+https://github.com/rust-lang/crates.io-index"
584
-
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
585
-
dependencies = [
586
-
"alloc-no-stdlib",
587
-
"alloc-stdlib",
588
-
"brotli-decompressor",
589
-
]
590
-
591
-
[[package]]
592
-
name = "brotli-decompressor"
593
-
version = "5.0.0"
594
-
source = "registry+https://github.com/rust-lang/crates.io-index"
595
-
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
596
-
dependencies = [
597
-
"alloc-no-stdlib",
598
-
"alloc-stdlib",
599
-
]
600
-
601
-
[[package]]
602
-
name = "bumpalo"
603
-
version = "3.19.0"
604
-
source = "registry+https://github.com/rust-lang/crates.io-index"
605
-
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
606
-
607
-
[[package]]
608
-
name = "bytes"
609
-
version = "1.10.1"
610
-
source = "registry+https://github.com/rust-lang/crates.io-index"
611
-
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
612
-
613
-
[[package]]
614
-
name = "bytestring"
615
-
version = "1.5.0"
616
-
source = "registry+https://github.com/rust-lang/crates.io-index"
617
-
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
618
-
dependencies = [
619
-
"bytes",
620
-
]
621
-
622
-
[[package]]
623
-
name = "cc"
624
-
version = "1.2.40"
625
-
source = "registry+https://github.com/rust-lang/crates.io-index"
626
-
checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
627
-
dependencies = [
628
-
"find-msvc-tools",
629
-
"jobserver",
630
-
"libc",
631
-
"shlex",
632
-
]
633
-
634
-
[[package]]
635
-
name = "cfg-if"
636
-
version = "1.0.3"
637
-
source = "registry+https://github.com/rust-lang/crates.io-index"
638
-
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
639
-
640
-
[[package]]
641
-
name = "chrono"
642
-
version = "0.4.42"
643
-
source = "registry+https://github.com/rust-lang/crates.io-index"
644
-
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
645
-
dependencies = [
646
-
"iana-time-zone",
647
-
"js-sys",
648
-
"num-traits",
649
-
"serde",
650
-
"wasm-bindgen",
651
-
"windows-link",
652
-
]
653
-
654
-
[[package]]
655
-
name = "cid"
656
-
version = "0.11.1"
657
-
source = "registry+https://github.com/rust-lang/crates.io-index"
658
-
checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a"
659
-
dependencies = [
660
-
"core2",
661
-
"multibase",
662
-
"multihash",
663
-
"serde",
664
-
"serde_bytes",
665
-
"unsigned-varint",
666
-
]
667
-
668
-
[[package]]
669
-
name = "cipher"
670
-
version = "0.4.4"
671
-
source = "registry+https://github.com/rust-lang/crates.io-index"
672
-
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
673
-
dependencies = [
674
-
"crypto-common",
675
-
"inout",
676
-
]
677
-
678
-
[[package]]
679
-
name = "colorchoice"
680
-
version = "1.0.4"
681
-
source = "registry+https://github.com/rust-lang/crates.io-index"
682
-
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
683
-
684
-
[[package]]
685
-
name = "compression-codecs"
686
-
version = "0.4.31"
687
-
source = "registry+https://github.com/rust-lang/crates.io-index"
688
-
checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
689
-
dependencies = [
690
-
"compression-core",
691
-
"flate2",
692
-
"memchr",
693
-
]
694
-
695
-
[[package]]
696
-
name = "compression-core"
697
-
version = "0.4.29"
698
-
source = "registry+https://github.com/rust-lang/crates.io-index"
699
-
checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
700
-
701
-
[[package]]
702
-
name = "concurrent-queue"
703
-
version = "2.5.0"
704
-
source = "registry+https://github.com/rust-lang/crates.io-index"
705
-
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
706
-
dependencies = [
707
-
"crossbeam-utils",
708
-
]
709
-
710
-
[[package]]
711
-
name = "const-oid"
712
-
version = "0.9.6"
713
-
source = "registry+https://github.com/rust-lang/crates.io-index"
714
-
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
715
-
716
-
[[package]]
717
-
name = "const-str"
718
-
version = "0.4.3"
719
-
source = "registry+https://github.com/rust-lang/crates.io-index"
720
-
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
721
-
722
-
[[package]]
723
-
name = "cookie"
724
-
version = "0.16.2"
725
-
source = "registry+https://github.com/rust-lang/crates.io-index"
726
-
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
727
-
dependencies = [
728
-
"aes-gcm",
729
-
"base64 0.20.0",
730
-
"hkdf",
731
-
"hmac",
732
-
"percent-encoding",
733
-
"rand 0.8.5",
734
-
"sha2",
735
-
"subtle",
736
-
"time",
737
-
"version_check",
738
-
]
739
-
740
-
[[package]]
741
-
name = "core-foundation"
742
-
version = "0.9.4"
743
-
source = "registry+https://github.com/rust-lang/crates.io-index"
744
-
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
745
-
dependencies = [
746
-
"core-foundation-sys",
747
-
"libc",
748
-
]
749
-
750
-
[[package]]
751
-
name = "core-foundation-sys"
752
-
version = "0.8.7"
753
-
source = "registry+https://github.com/rust-lang/crates.io-index"
754
-
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
755
-
756
-
[[package]]
757
-
name = "core2"
758
-
version = "0.4.0"
759
-
source = "registry+https://github.com/rust-lang/crates.io-index"
760
-
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
761
-
dependencies = [
762
-
"memchr",
763
-
]
764
-
765
-
[[package]]
766
-
name = "cpufeatures"
767
-
version = "0.2.17"
768
-
source = "registry+https://github.com/rust-lang/crates.io-index"
769
-
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
770
-
dependencies = [
771
-
"libc",
772
-
]
773
-
774
-
[[package]]
775
-
name = "crc32fast"
776
-
version = "1.5.0"
777
-
source = "registry+https://github.com/rust-lang/crates.io-index"
778
-
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
779
-
dependencies = [
780
-
"cfg-if",
781
-
]
782
-
783
-
[[package]]
784
-
name = "crossbeam-channel"
785
-
version = "0.5.15"
786
-
source = "registry+https://github.com/rust-lang/crates.io-index"
787
-
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
788
-
dependencies = [
789
-
"crossbeam-utils",
790
-
]
791
-
792
-
[[package]]
793
-
name = "crossbeam-epoch"
794
-
version = "0.9.18"
795
-
source = "registry+https://github.com/rust-lang/crates.io-index"
796
-
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
797
-
dependencies = [
798
-
"crossbeam-utils",
799
-
]
800
-
801
-
[[package]]
802
-
name = "crossbeam-utils"
803
-
version = "0.8.21"
804
-
source = "registry+https://github.com/rust-lang/crates.io-index"
805
-
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
806
-
807
-
[[package]]
808
-
name = "crypto-bigint"
809
-
version = "0.5.5"
810
-
source = "registry+https://github.com/rust-lang/crates.io-index"
811
-
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
812
-
dependencies = [
813
-
"generic-array",
814
-
"rand_core 0.6.4",
815
-
"subtle",
816
-
"zeroize",
817
-
]
818
-
819
-
[[package]]
820
-
name = "crypto-common"
821
-
version = "0.1.6"
822
-
source = "registry+https://github.com/rust-lang/crates.io-index"
823
-
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
824
-
dependencies = [
825
-
"generic-array",
826
-
"rand_core 0.6.4",
827
-
"typenum",
828
-
]
829
-
830
-
[[package]]
831
-
name = "ctr"
832
-
version = "0.9.2"
833
-
source = "registry+https://github.com/rust-lang/crates.io-index"
834
-
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
835
-
dependencies = [
836
-
"cipher",
837
-
]
838
-
839
-
[[package]]
840
-
name = "dashmap"
841
-
version = "6.1.0"
842
-
source = "registry+https://github.com/rust-lang/crates.io-index"
843
-
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
844
-
dependencies = [
845
-
"cfg-if",
846
-
"crossbeam-utils",
847
-
"hashbrown 0.14.5",
848
-
"lock_api",
849
-
"once_cell",
850
-
"parking_lot_core",
851
-
]
852
-
853
-
[[package]]
854
-
name = "data-encoding"
855
-
version = "2.9.0"
856
-
source = "registry+https://github.com/rust-lang/crates.io-index"
857
-
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
858
-
859
-
[[package]]
860
-
name = "data-encoding-macro"
861
-
version = "0.1.18"
862
-
source = "registry+https://github.com/rust-lang/crates.io-index"
863
-
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
864
-
dependencies = [
865
-
"data-encoding",
866
-
"data-encoding-macro-internal",
867
-
]
868
-
869
-
[[package]]
870
-
name = "data-encoding-macro-internal"
871
-
version = "0.1.16"
872
-
source = "registry+https://github.com/rust-lang/crates.io-index"
873
-
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
874
-
dependencies = [
875
-
"data-encoding",
876
-
"syn 2.0.106",
877
-
]
878
-
879
-
[[package]]
880
-
name = "der"
881
-
version = "0.7.10"
882
-
source = "registry+https://github.com/rust-lang/crates.io-index"
883
-
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
884
-
dependencies = [
885
-
"const-oid",
886
-
"zeroize",
887
-
]
888
-
889
-
[[package]]
890
-
name = "deranged"
891
-
version = "0.5.4"
892
-
source = "registry+https://github.com/rust-lang/crates.io-index"
893
-
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
894
-
dependencies = [
895
-
"powerfmt",
896
-
]
897
-
898
-
[[package]]
899
-
name = "derive_more"
900
-
version = "1.0.0"
901
-
source = "registry+https://github.com/rust-lang/crates.io-index"
902
-
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
903
-
dependencies = [
904
-
"derive_more-impl 1.0.0",
905
-
]
906
-
907
-
[[package]]
908
-
name = "derive_more"
909
-
version = "2.0.1"
910
-
source = "registry+https://github.com/rust-lang/crates.io-index"
911
-
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
912
-
dependencies = [
913
-
"derive_more-impl 2.0.1",
914
-
]
915
-
916
-
[[package]]
917
-
name = "derive_more-impl"
918
-
version = "1.0.0"
919
-
source = "registry+https://github.com/rust-lang/crates.io-index"
920
-
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
921
-
dependencies = [
922
-
"proc-macro2",
923
-
"quote",
924
-
"syn 2.0.106",
925
-
"unicode-xid",
926
-
]
927
-
928
-
[[package]]
929
-
name = "derive_more-impl"
930
-
version = "2.0.1"
931
-
source = "registry+https://github.com/rust-lang/crates.io-index"
932
-
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
933
-
dependencies = [
934
-
"proc-macro2",
935
-
"quote",
936
-
"syn 2.0.106",
937
-
"unicode-xid",
938
-
]
939
-
940
-
[[package]]
941
-
name = "digest"
942
-
version = "0.10.7"
943
-
source = "registry+https://github.com/rust-lang/crates.io-index"
944
-
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
945
-
dependencies = [
946
-
"block-buffer",
947
-
"const-oid",
948
-
"crypto-common",
949
-
"subtle",
950
-
]
951
-
952
-
[[package]]
953
-
name = "displaydoc"
954
-
version = "0.2.5"
955
-
source = "registry+https://github.com/rust-lang/crates.io-index"
956
-
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
957
-
dependencies = [
958
-
"proc-macro2",
959
-
"quote",
960
-
"syn 2.0.106",
961
-
]
962
-
963
-
[[package]]
964
-
name = "ecdsa"
965
-
version = "0.16.9"
966
-
source = "registry+https://github.com/rust-lang/crates.io-index"
967
-
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
968
-
dependencies = [
969
-
"der",
970
-
"digest",
971
-
"elliptic-curve",
972
-
"rfc6979",
973
-
"signature",
974
-
]
975
-
976
-
[[package]]
977
-
name = "elliptic-curve"
978
-
version = "0.13.8"
979
-
source = "registry+https://github.com/rust-lang/crates.io-index"
980
-
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
981
-
dependencies = [
982
-
"base16ct",
983
-
"crypto-bigint",
984
-
"digest",
985
-
"ff",
986
-
"generic-array",
987
-
"group",
988
-
"rand_core 0.6.4",
989
-
"sec1",
990
-
"subtle",
991
-
"zeroize",
992
-
]
993
-
994
-
[[package]]
995
-
name = "encoding_rs"
996
-
version = "0.8.35"
997
-
source = "registry+https://github.com/rust-lang/crates.io-index"
998
-
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
999
-
dependencies = [
1000
-
"cfg-if",
1001
-
]
1002
-
1003
-
[[package]]
1004
-
name = "enum-as-inner"
1005
-
version = "0.6.1"
1006
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1007
-
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
1008
-
dependencies = [
1009
-
"heck",
1010
-
"proc-macro2",
1011
-
"quote",
1012
-
"syn 2.0.106",
1013
-
]
1014
-
1015
-
[[package]]
1016
-
name = "env_filter"
1017
-
version = "0.1.3"
1018
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1019
-
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
1020
-
dependencies = [
1021
-
"log",
1022
-
"regex",
1023
-
]
1024
-
1025
-
[[package]]
1026
-
name = "env_logger"
1027
-
version = "0.11.8"
1028
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1029
-
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
1030
-
dependencies = [
1031
-
"anstream",
1032
-
"anstyle",
1033
-
"env_filter",
1034
-
"jiff",
1035
-
"log",
1036
-
]
1037
-
1038
-
[[package]]
1039
-
name = "equivalent"
1040
-
version = "1.0.2"
1041
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1042
-
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
1043
-
1044
-
[[package]]
1045
-
name = "errno"
1046
-
version = "0.3.14"
1047
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1048
-
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1049
-
dependencies = [
1050
-
"libc",
1051
-
"windows-sys 0.61.1",
1052
-
]
1053
-
1054
-
[[package]]
1055
-
name = "event-listener"
1056
-
version = "5.4.1"
1057
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1058
-
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
1059
-
dependencies = [
1060
-
"concurrent-queue",
1061
-
"parking",
1062
-
"pin-project-lite",
1063
-
]
1064
-
1065
-
[[package]]
1066
-
name = "event-listener-strategy"
1067
-
version = "0.5.4"
1068
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1069
-
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
1070
-
dependencies = [
1071
-
"event-listener",
1072
-
"pin-project-lite",
1073
-
]
1074
-
1075
-
[[package]]
1076
-
name = "fastrand"
1077
-
version = "2.3.0"
1078
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1079
-
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1080
-
1081
-
[[package]]
1082
-
name = "ff"
1083
-
version = "0.13.1"
1084
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1085
-
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
1086
-
dependencies = [
1087
-
"rand_core 0.6.4",
1088
-
"subtle",
1089
-
]
1090
-
1091
-
[[package]]
1092
-
name = "find-msvc-tools"
1093
-
version = "0.1.3"
1094
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1095
-
checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
1096
-
1097
-
[[package]]
1098
-
name = "flate2"
1099
-
version = "1.1.4"
1100
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1101
-
checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
1102
-
dependencies = [
1103
-
"crc32fast",
1104
-
"miniz_oxide",
1105
-
]
1106
-
1107
-
[[package]]
1108
-
name = "fnv"
1109
-
version = "1.0.7"
1110
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1111
-
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1112
-
1113
-
[[package]]
1114
-
name = "foldhash"
1115
-
version = "0.1.5"
1116
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1117
-
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
1118
-
1119
-
[[package]]
1120
-
name = "foreign-types"
1121
-
version = "0.3.2"
1122
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1123
-
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
1124
-
dependencies = [
1125
-
"foreign-types-shared",
1126
-
]
1127
-
1128
-
[[package]]
1129
-
name = "foreign-types-shared"
1130
-
version = "0.1.1"
1131
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1132
-
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
1133
-
1134
-
[[package]]
1135
-
name = "form_urlencoded"
1136
-
version = "1.2.2"
1137
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1138
-
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
1139
-
dependencies = [
1140
-
"percent-encoding",
1141
-
]
1142
-
1143
-
[[package]]
1144
-
name = "futures-channel"
1145
-
version = "0.3.31"
1146
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1147
-
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
1148
-
dependencies = [
1149
-
"futures-core",
1150
-
]
1151
-
1152
-
[[package]]
1153
-
name = "futures-core"
1154
-
version = "0.3.31"
1155
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1156
-
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
1157
-
1158
-
[[package]]
1159
-
name = "futures-io"
1160
-
version = "0.3.31"
1161
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1162
-
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
1163
-
1164
-
[[package]]
1165
-
name = "futures-macro"
1166
-
version = "0.3.31"
1167
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1168
-
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
1169
-
dependencies = [
1170
-
"proc-macro2",
1171
-
"quote",
1172
-
"syn 2.0.106",
1173
-
]
1174
-
1175
-
[[package]]
1176
-
name = "futures-sink"
1177
-
version = "0.3.31"
1178
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1179
-
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
1180
-
1181
-
[[package]]
1182
-
name = "futures-task"
1183
-
version = "0.3.31"
1184
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1185
-
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
1186
-
1187
-
[[package]]
1188
-
name = "futures-util"
1189
-
version = "0.3.31"
1190
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1191
-
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
1192
-
dependencies = [
1193
-
"futures-core",
1194
-
"futures-macro",
1195
-
"futures-task",
1196
-
"pin-project-lite",
1197
-
"pin-utils",
1198
-
"slab",
1199
-
]
1200
-
1201
-
[[package]]
1202
-
name = "generic-array"
1203
-
version = "0.14.7"
1204
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1205
-
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
1206
-
dependencies = [
1207
-
"typenum",
1208
-
"version_check",
1209
-
"zeroize",
1210
-
]
1211
-
1212
-
[[package]]
1213
-
name = "getrandom"
1214
-
version = "0.2.16"
1215
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1216
-
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
1217
-
dependencies = [
1218
-
"cfg-if",
1219
-
"libc",
1220
-
"wasi 0.11.1+wasi-snapshot-preview1",
1221
-
]
1222
-
1223
-
[[package]]
1224
-
name = "getrandom"
1225
-
version = "0.3.3"
1226
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1227
-
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
1228
-
dependencies = [
1229
-
"cfg-if",
1230
-
"libc",
1231
-
"r-efi",
1232
-
"wasi 0.14.7+wasi-0.2.4",
1233
-
]
1234
-
1235
-
[[package]]
1236
-
name = "ghash"
1237
-
version = "0.5.1"
1238
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1239
-
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
1240
-
dependencies = [
1241
-
"opaque-debug",
1242
-
"polyval",
1243
-
]
1244
-
1245
-
[[package]]
1246
-
name = "gimli"
1247
-
version = "0.32.3"
1248
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1249
-
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
1250
-
1251
-
[[package]]
1252
-
name = "group"
1253
-
version = "0.13.0"
1254
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1255
-
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
1256
-
dependencies = [
1257
-
"ff",
1258
-
"rand_core 0.6.4",
1259
-
"subtle",
1260
-
]
1261
-
1262
-
[[package]]
1263
-
name = "h2"
1264
-
version = "0.3.27"
1265
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1266
-
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
1267
-
dependencies = [
1268
-
"bytes",
1269
-
"fnv",
1270
-
"futures-core",
1271
-
"futures-sink",
1272
-
"futures-util",
1273
-
"http 0.2.12",
1274
-
"indexmap",
1275
-
"slab",
1276
-
"tokio",
1277
-
"tokio-util",
1278
-
"tracing",
1279
-
]
1280
-
1281
-
[[package]]
1282
-
name = "hashbrown"
1283
-
version = "0.14.5"
1284
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1285
-
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
1286
-
1287
-
[[package]]
1288
-
name = "hashbrown"
1289
-
version = "0.15.5"
1290
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1291
-
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1292
-
dependencies = [
1293
-
"allocator-api2",
1294
-
"equivalent",
1295
-
"foldhash",
1296
-
]
1297
-
1298
-
[[package]]
1299
-
name = "hashbrown"
1300
-
version = "0.16.0"
1301
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1302
-
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
1303
-
1304
-
[[package]]
1305
-
name = "heck"
1306
-
version = "0.5.0"
1307
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1308
-
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
1309
-
1310
-
[[package]]
1311
-
name = "hickory-proto"
1312
-
version = "0.24.4"
1313
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1314
-
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
1315
-
dependencies = [
1316
-
"async-trait",
1317
-
"cfg-if",
1318
-
"data-encoding",
1319
-
"enum-as-inner",
1320
-
"futures-channel",
1321
-
"futures-io",
1322
-
"futures-util",
1323
-
"idna",
1324
-
"ipnet",
1325
-
"once_cell",
1326
-
"rand 0.8.5",
1327
-
"thiserror",
1328
-
"tinyvec",
1329
-
"tokio",
1330
-
"tracing",
1331
-
"url",
1332
-
]
1333
-
1334
-
[[package]]
1335
-
name = "hickory-resolver"
1336
-
version = "0.24.4"
1337
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1338
-
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
1339
-
dependencies = [
1340
-
"cfg-if",
1341
-
"futures-util",
1342
-
"hickory-proto",
1343
-
"ipconfig",
1344
-
"lru-cache",
1345
-
"once_cell",
1346
-
"parking_lot",
1347
-
"rand 0.8.5",
1348
-
"resolv-conf",
1349
-
"smallvec",
1350
-
"thiserror",
1351
-
"tokio",
1352
-
"tracing",
1353
-
]
1354
-
1355
-
[[package]]
1356
-
name = "hkdf"
1357
-
version = "0.12.4"
1358
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1359
-
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
1360
-
dependencies = [
1361
-
"hmac",
1362
-
]
1363
-
1364
-
[[package]]
1365
-
name = "hmac"
1366
-
version = "0.12.1"
1367
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1368
-
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
1369
-
dependencies = [
1370
-
"digest",
1371
-
]
1372
-
1373
-
[[package]]
1374
-
name = "http"
1375
-
version = "0.2.12"
1376
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1377
-
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
1378
-
dependencies = [
1379
-
"bytes",
1380
-
"fnv",
1381
-
"itoa",
1382
-
]
1383
-
1384
-
[[package]]
1385
-
name = "http"
1386
-
version = "1.3.1"
1387
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1388
-
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
1389
-
dependencies = [
1390
-
"bytes",
1391
-
"fnv",
1392
-
"itoa",
1393
-
]
1394
-
1395
-
[[package]]
1396
-
name = "http-body"
1397
-
version = "1.0.1"
1398
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1399
-
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
1400
-
dependencies = [
1401
-
"bytes",
1402
-
"http 1.3.1",
1403
-
]
1404
-
1405
-
[[package]]
1406
-
name = "http-body-util"
1407
-
version = "0.1.3"
1408
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1409
-
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
1410
-
dependencies = [
1411
-
"bytes",
1412
-
"futures-core",
1413
-
"http 1.3.1",
1414
-
"http-body",
1415
-
"pin-project-lite",
1416
-
]
1417
-
1418
-
[[package]]
1419
-
name = "httparse"
1420
-
version = "1.10.1"
1421
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1422
-
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
1423
-
1424
-
[[package]]
1425
-
name = "httpdate"
1426
-
version = "1.0.3"
1427
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1428
-
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
1429
-
1430
-
[[package]]
1431
-
name = "hyper"
1432
-
version = "1.7.0"
1433
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1434
-
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
1435
-
dependencies = [
1436
-
"atomic-waker",
1437
-
"bytes",
1438
-
"futures-channel",
1439
-
"futures-core",
1440
-
"http 1.3.1",
1441
-
"http-body",
1442
-
"httparse",
1443
-
"itoa",
1444
-
"pin-project-lite",
1445
-
"pin-utils",
1446
-
"smallvec",
1447
-
"tokio",
1448
-
"want",
1449
-
]
1450
-
1451
-
[[package]]
1452
-
name = "hyper-tls"
1453
-
version = "0.6.0"
1454
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1455
-
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
1456
-
dependencies = [
1457
-
"bytes",
1458
-
"http-body-util",
1459
-
"hyper",
1460
-
"hyper-util",
1461
-
"native-tls",
1462
-
"tokio",
1463
-
"tokio-native-tls",
1464
-
"tower-service",
1465
-
]
1466
-
1467
-
[[package]]
1468
-
name = "hyper-util"
1469
-
version = "0.1.17"
1470
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1471
-
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
1472
-
dependencies = [
1473
-
"base64 0.22.1",
1474
-
"bytes",
1475
-
"futures-channel",
1476
-
"futures-core",
1477
-
"futures-util",
1478
-
"http 1.3.1",
1479
-
"http-body",
1480
-
"hyper",
1481
-
"ipnet",
1482
-
"libc",
1483
-
"percent-encoding",
1484
-
"pin-project-lite",
1485
-
"socket2 0.6.0",
1486
-
"tokio",
1487
-
"tower-service",
1488
-
"tracing",
1489
-
]
1490
-
1491
-
[[package]]
1492
-
name = "iana-time-zone"
1493
-
version = "0.1.64"
1494
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1495
-
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
1496
-
dependencies = [
1497
-
"android_system_properties",
1498
-
"core-foundation-sys",
1499
-
"iana-time-zone-haiku",
1500
-
"js-sys",
1501
-
"log",
1502
-
"wasm-bindgen",
1503
-
"windows-core",
1504
-
]
1505
-
1506
-
[[package]]
1507
-
name = "iana-time-zone-haiku"
1508
-
version = "0.1.2"
1509
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1510
-
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
1511
-
dependencies = [
1512
-
"cc",
1513
-
]
1514
-
1515
-
[[package]]
1516
-
name = "icu_collections"
1517
-
version = "2.0.0"
1518
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1519
-
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
1520
-
dependencies = [
1521
-
"displaydoc",
1522
-
"potential_utf",
1523
-
"yoke",
1524
-
"zerofrom",
1525
-
"zerovec",
1526
-
]
1527
-
1528
-
[[package]]
1529
-
name = "icu_locale_core"
1530
-
version = "2.0.0"
1531
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1532
-
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
1533
-
dependencies = [
1534
-
"displaydoc",
1535
-
"litemap",
1536
-
"tinystr",
1537
-
"writeable",
1538
-
"zerovec",
1539
-
]
1540
-
1541
-
[[package]]
1542
-
name = "icu_normalizer"
1543
-
version = "2.0.0"
1544
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1545
-
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
1546
-
dependencies = [
1547
-
"displaydoc",
1548
-
"icu_collections",
1549
-
"icu_normalizer_data",
1550
-
"icu_properties",
1551
-
"icu_provider",
1552
-
"smallvec",
1553
-
"zerovec",
1554
-
]
1555
-
1556
-
[[package]]
1557
-
name = "icu_normalizer_data"
1558
-
version = "2.0.0"
1559
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1560
-
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
1561
-
1562
-
[[package]]
1563
-
name = "icu_properties"
1564
-
version = "2.0.1"
1565
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1566
-
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
1567
-
dependencies = [
1568
-
"displaydoc",
1569
-
"icu_collections",
1570
-
"icu_locale_core",
1571
-
"icu_properties_data",
1572
-
"icu_provider",
1573
-
"potential_utf",
1574
-
"zerotrie",
1575
-
"zerovec",
1576
-
]
1577
-
1578
-
[[package]]
1579
-
name = "icu_properties_data"
1580
-
version = "2.0.1"
1581
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1582
-
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
1583
-
1584
-
[[package]]
1585
-
name = "icu_provider"
1586
-
version = "2.0.0"
1587
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1588
-
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
1589
-
dependencies = [
1590
-
"displaydoc",
1591
-
"icu_locale_core",
1592
-
"stable_deref_trait",
1593
-
"tinystr",
1594
-
"writeable",
1595
-
"yoke",
1596
-
"zerofrom",
1597
-
"zerotrie",
1598
-
"zerovec",
1599
-
]
1600
-
1601
-
[[package]]
1602
-
name = "idna"
1603
-
version = "1.1.0"
1604
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1605
-
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
1606
-
dependencies = [
1607
-
"idna_adapter",
1608
-
"smallvec",
1609
-
"utf8_iter",
1610
-
]
1611
-
1612
-
[[package]]
1613
-
name = "idna_adapter"
1614
-
version = "1.2.1"
1615
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1616
-
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
1617
-
dependencies = [
1618
-
"icu_normalizer",
1619
-
"icu_properties",
1620
-
]
1621
-
1622
-
[[package]]
1623
-
name = "impl-more"
1624
-
version = "0.1.9"
1625
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1626
-
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
1627
-
1628
-
[[package]]
1629
-
name = "indexmap"
1630
-
version = "2.11.4"
1631
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1632
-
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
1633
-
dependencies = [
1634
-
"equivalent",
1635
-
"hashbrown 0.16.0",
1636
-
]
1637
-
1638
-
[[package]]
1639
-
name = "inout"
1640
-
version = "0.1.4"
1641
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1642
-
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
1643
-
dependencies = [
1644
-
"generic-array",
1645
-
]
1646
-
1647
-
[[package]]
1648
-
name = "io-uring"
1649
-
version = "0.7.10"
1650
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1651
-
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
1652
-
dependencies = [
1653
-
"bitflags",
1654
-
"cfg-if",
1655
-
"libc",
1656
-
]
1657
-
1658
-
[[package]]
1659
-
name = "ipconfig"
1660
-
version = "0.3.2"
1661
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1662
-
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
1663
-
dependencies = [
1664
-
"socket2 0.5.10",
1665
-
"widestring",
1666
-
"windows-sys 0.48.0",
1667
-
"winreg",
1668
-
]
1669
-
1670
-
[[package]]
1671
-
name = "ipld-core"
1672
-
version = "0.4.2"
1673
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1674
-
checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db"
1675
-
dependencies = [
1676
-
"cid",
1677
-
"serde",
1678
-
"serde_bytes",
1679
-
]
1680
-
1681
-
[[package]]
1682
-
name = "ipnet"
1683
-
version = "2.11.0"
1684
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1685
-
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
1686
-
1687
-
[[package]]
1688
-
name = "iri-string"
1689
-
version = "0.7.8"
1690
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1691
-
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
1692
-
dependencies = [
1693
-
"memchr",
1694
-
"serde",
1695
-
]
1696
-
1697
-
[[package]]
1698
-
name = "is_terminal_polyfill"
1699
-
version = "1.70.1"
1700
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1701
-
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
1702
-
1703
-
[[package]]
1704
-
name = "itoa"
1705
-
version = "1.0.15"
1706
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1707
-
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1708
-
1709
-
[[package]]
1710
-
name = "jiff"
1711
-
version = "0.2.15"
1712
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1713
-
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
1714
-
dependencies = [
1715
-
"jiff-static",
1716
-
"log",
1717
-
"portable-atomic",
1718
-
"portable-atomic-util",
1719
-
"serde",
1720
-
]
1721
-
1722
-
[[package]]
1723
-
name = "jiff-static"
1724
-
version = "0.2.15"
1725
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1726
-
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
1727
-
dependencies = [
1728
-
"proc-macro2",
1729
-
"quote",
1730
-
"syn 2.0.106",
1731
-
]
1732
-
1733
-
[[package]]
1734
-
name = "jobserver"
1735
-
version = "0.1.34"
1736
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1737
-
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
1738
-
dependencies = [
1739
-
"getrandom 0.3.3",
1740
-
"libc",
1741
-
]
1742
-
1743
-
[[package]]
1744
-
name = "jose-b64"
1745
-
version = "0.1.2"
1746
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1747
-
checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56"
1748
-
dependencies = [
1749
-
"base64ct",
1750
-
"serde",
1751
-
"subtle",
1752
-
"zeroize",
1753
-
]
1754
-
1755
-
[[package]]
1756
-
name = "jose-jwa"
1757
-
version = "0.1.2"
1758
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1759
-
checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7"
1760
-
dependencies = [
1761
-
"serde",
1762
-
]
1763
-
1764
-
[[package]]
1765
-
name = "jose-jwk"
1766
-
version = "0.1.2"
1767
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1768
-
checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7"
1769
-
dependencies = [
1770
-
"jose-b64",
1771
-
"jose-jwa",
1772
-
"p256",
1773
-
"serde",
1774
-
"zeroize",
1775
-
]
1776
-
1777
-
[[package]]
1778
-
name = "js-sys"
1779
-
version = "0.3.81"
1780
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1781
-
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
1782
-
dependencies = [
1783
-
"once_cell",
1784
-
"wasm-bindgen",
1785
-
]
1786
-
1787
-
[[package]]
1788
-
name = "langtag"
1789
-
version = "0.3.4"
1790
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1791
-
checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805"
1792
-
dependencies = [
1793
-
"serde",
1794
-
]
1795
-
1796
-
[[package]]
1797
-
name = "language-tags"
1798
-
version = "0.3.2"
1799
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1800
-
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
1801
-
1802
-
[[package]]
1803
-
name = "libc"
1804
-
version = "0.2.176"
1805
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1806
-
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
1807
-
1808
-
[[package]]
1809
-
name = "linked-hash-map"
1810
-
version = "0.5.6"
1811
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1812
-
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
1813
-
1814
-
[[package]]
1815
-
name = "linux-raw-sys"
1816
-
version = "0.11.0"
1817
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1818
-
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
1819
-
1820
-
[[package]]
1821
-
name = "litemap"
1822
-
version = "0.8.0"
1823
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1824
-
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
1825
-
1826
-
[[package]]
1827
-
name = "local-channel"
1828
-
version = "0.1.5"
1829
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1830
-
checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
1831
-
dependencies = [
1832
-
"futures-core",
1833
-
"futures-sink",
1834
-
"local-waker",
1835
-
]
1836
-
1837
-
[[package]]
1838
-
name = "local-waker"
1839
-
version = "0.1.4"
1840
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1841
-
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
1842
-
1843
-
[[package]]
1844
-
name = "lock_api"
1845
-
version = "0.4.14"
1846
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1847
-
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
1848
-
dependencies = [
1849
-
"scopeguard",
1850
-
]
1851
-
1852
-
[[package]]
1853
-
name = "log"
1854
-
version = "0.4.28"
1855
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1856
-
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
1857
-
1858
-
[[package]]
1859
-
name = "lru"
1860
-
version = "0.12.5"
1861
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1862
-
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
1863
-
dependencies = [
1864
-
"hashbrown 0.15.5",
1865
-
]
1866
-
1867
-
[[package]]
1868
-
name = "lru-cache"
1869
-
version = "0.1.2"
1870
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1871
-
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
1872
-
dependencies = [
1873
-
"linked-hash-map",
1874
-
]
1875
-
1876
-
[[package]]
1877
-
name = "match-lookup"
1878
-
version = "0.1.1"
1879
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1880
-
checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e"
1881
-
dependencies = [
1882
-
"proc-macro2",
1883
-
"quote",
1884
-
"syn 1.0.109",
1885
-
]
1886
-
1887
-
[[package]]
1888
-
name = "memchr"
1889
-
version = "2.7.6"
1890
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1891
-
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
1892
-
1893
-
[[package]]
1894
-
name = "mime"
1895
-
version = "0.3.17"
1896
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1897
-
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
1898
-
1899
-
[[package]]
1900
-
name = "miniz_oxide"
1901
-
version = "0.8.9"
1902
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1903
-
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
1904
-
dependencies = [
1905
-
"adler2",
1906
-
"simd-adler32",
1907
-
]
1908
-
1909
-
[[package]]
1910
-
name = "mio"
1911
-
version = "1.0.4"
1912
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1913
-
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
1914
-
dependencies = [
1915
-
"libc",
1916
-
"log",
1917
-
"wasi 0.11.1+wasi-snapshot-preview1",
1918
-
"windows-sys 0.59.0",
1919
-
]
1920
-
1921
-
[[package]]
1922
-
name = "moka"
1923
-
version = "0.12.11"
1924
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1925
-
checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077"
1926
-
dependencies = [
1927
-
"async-lock",
1928
-
"crossbeam-channel",
1929
-
"crossbeam-epoch",
1930
-
"crossbeam-utils",
1931
-
"equivalent",
1932
-
"event-listener",
1933
-
"futures-util",
1934
-
"parking_lot",
1935
-
"portable-atomic",
1936
-
"rustc_version",
1937
-
"smallvec",
1938
-
"tagptr",
1939
-
"uuid",
1940
-
]
1941
-
1942
-
[[package]]
1943
-
name = "multibase"
1944
-
version = "0.9.2"
1945
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1946
-
checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"
1947
-
dependencies = [
1948
-
"base-x",
1949
-
"base256emoji",
1950
-
"data-encoding",
1951
-
"data-encoding-macro",
1952
-
]
1953
-
1954
-
[[package]]
1955
-
name = "multihash"
1956
-
version = "0.19.3"
1957
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1958
-
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
1959
-
dependencies = [
1960
-
"core2",
1961
-
"serde",
1962
-
"unsigned-varint",
1963
-
]
1964
-
1965
-
[[package]]
1966
-
name = "native-tls"
1967
-
version = "0.2.14"
1968
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1969
-
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
1970
-
dependencies = [
1971
-
"libc",
1972
-
"log",
1973
-
"openssl",
1974
-
"openssl-probe",
1975
-
"openssl-sys",
1976
-
"schannel",
1977
-
"security-framework",
1978
-
"security-framework-sys",
1979
-
"tempfile",
1980
-
]
1981
-
1982
-
[[package]]
1983
-
name = "num-conv"
1984
-
version = "0.1.0"
1985
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1986
-
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
1987
-
1988
-
[[package]]
1989
-
name = "num-traits"
1990
-
version = "0.2.19"
1991
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1992
-
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1993
-
dependencies = [
1994
-
"autocfg",
1995
-
]
1996
-
1997
-
[[package]]
1998
-
name = "object"
1999
-
version = "0.37.3"
2000
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2001
-
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
2002
-
dependencies = [
2003
-
"memchr",
2004
-
]
2005
-
2006
-
[[package]]
2007
-
name = "once_cell"
2008
-
version = "1.21.3"
2009
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2010
-
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
2011
-
2012
-
[[package]]
2013
-
name = "once_cell_polyfill"
2014
-
version = "1.70.1"
2015
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2016
-
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
2017
-
2018
-
[[package]]
2019
-
name = "opaque-debug"
2020
-
version = "0.3.1"
2021
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2022
-
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
2023
-
2024
-
[[package]]
2025
-
name = "openssl"
2026
-
version = "0.10.73"
2027
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2028
-
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
2029
-
dependencies = [
2030
-
"bitflags",
2031
-
"cfg-if",
2032
-
"foreign-types",
2033
-
"libc",
2034
-
"once_cell",
2035
-
"openssl-macros",
2036
-
"openssl-sys",
2037
-
]
2038
-
2039
-
[[package]]
2040
-
name = "openssl-macros"
2041
-
version = "0.1.1"
2042
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2043
-
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
2044
-
dependencies = [
2045
-
"proc-macro2",
2046
-
"quote",
2047
-
"syn 2.0.106",
2048
-
]
2049
-
2050
-
[[package]]
2051
-
name = "openssl-probe"
2052
-
version = "0.1.6"
2053
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2054
-
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
2055
-
2056
-
[[package]]
2057
-
name = "openssl-sys"
2058
-
version = "0.9.109"
2059
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2060
-
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
2061
-
dependencies = [
2062
-
"cc",
2063
-
"libc",
2064
-
"pkg-config",
2065
-
"vcpkg",
2066
-
]
2067
-
2068
-
[[package]]
2069
-
name = "p256"
2070
-
version = "0.13.2"
2071
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2072
-
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
2073
-
dependencies = [
2074
-
"ecdsa",
2075
-
"elliptic-curve",
2076
-
"primeorder",
2077
-
"sha2",
2078
-
]
2079
-
2080
-
[[package]]
2081
-
name = "parking"
2082
-
version = "2.2.1"
2083
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2084
-
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
2085
-
2086
-
[[package]]
2087
-
name = "parking_lot"
2088
-
version = "0.12.5"
2089
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2090
-
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
2091
-
dependencies = [
2092
-
"lock_api",
2093
-
"parking_lot_core",
2094
-
]
2095
-
2096
-
[[package]]
2097
-
name = "parking_lot_core"
2098
-
version = "0.9.12"
2099
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2100
-
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
2101
-
dependencies = [
2102
-
"cfg-if",
2103
-
"libc",
2104
-
"redox_syscall",
2105
-
"smallvec",
2106
-
"windows-link",
2107
-
]
2108
-
2109
-
[[package]]
2110
-
name = "percent-encoding"
2111
-
version = "2.3.2"
2112
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2113
-
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
2114
-
2115
-
[[package]]
2116
-
name = "pin-project-lite"
2117
-
version = "0.2.16"
2118
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2119
-
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
2120
-
2121
-
[[package]]
2122
-
name = "pin-utils"
2123
-
version = "0.1.0"
2124
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2125
-
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
2126
-
2127
-
[[package]]
2128
-
name = "pkg-config"
2129
-
version = "0.3.32"
2130
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2131
-
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
2132
-
2133
-
[[package]]
2134
-
name = "polyval"
2135
-
version = "0.6.2"
2136
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2137
-
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
2138
-
dependencies = [
2139
-
"cfg-if",
2140
-
"cpufeatures",
2141
-
"opaque-debug",
2142
-
"universal-hash",
2143
-
]
2144
-
2145
-
[[package]]
2146
-
name = "portable-atomic"
2147
-
version = "1.11.1"
2148
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2149
-
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
2150
-
2151
-
[[package]]
2152
-
name = "portable-atomic-util"
2153
-
version = "0.2.4"
2154
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2155
-
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
2156
-
dependencies = [
2157
-
"portable-atomic",
2158
-
]
2159
-
2160
-
[[package]]
2161
-
name = "potential_utf"
2162
-
version = "0.1.3"
2163
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2164
-
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
2165
-
dependencies = [
2166
-
"zerovec",
2167
-
]
2168
-
2169
-
[[package]]
2170
-
name = "powerfmt"
2171
-
version = "0.2.0"
2172
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2173
-
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
2174
-
2175
-
[[package]]
2176
-
name = "ppv-lite86"
2177
-
version = "0.2.21"
2178
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2179
-
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
2180
-
dependencies = [
2181
-
"zerocopy",
2182
-
]
2183
-
2184
-
[[package]]
2185
-
name = "primeorder"
2186
-
version = "0.13.6"
2187
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2188
-
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
2189
-
dependencies = [
2190
-
"elliptic-curve",
2191
-
]
2192
-
2193
-
[[package]]
2194
-
name = "proc-macro2"
2195
-
version = "1.0.101"
2196
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2197
-
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
2198
-
dependencies = [
2199
-
"unicode-ident",
2200
-
]
2201
-
2202
-
[[package]]
2203
-
name = "quote"
2204
-
version = "1.0.41"
2205
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2206
-
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
2207
-
dependencies = [
2208
-
"proc-macro2",
2209
-
]
2210
-
2211
-
[[package]]
2212
-
name = "r-efi"
2213
-
version = "5.3.0"
2214
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2215
-
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
2216
-
2217
-
[[package]]
2218
-
name = "rand"
2219
-
version = "0.8.5"
2220
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2221
-
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
2222
-
dependencies = [
2223
-
"libc",
2224
-
"rand_chacha 0.3.1",
2225
-
"rand_core 0.6.4",
2226
-
]
2227
-
2228
-
[[package]]
2229
-
name = "rand"
2230
-
version = "0.9.2"
2231
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2232
-
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
2233
-
dependencies = [
2234
-
"rand_chacha 0.9.0",
2235
-
"rand_core 0.9.3",
2236
-
]
2237
-
2238
-
[[package]]
2239
-
name = "rand_chacha"
2240
-
version = "0.3.1"
2241
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2242
-
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
2243
-
dependencies = [
2244
-
"ppv-lite86",
2245
-
"rand_core 0.6.4",
2246
-
]
2247
-
2248
-
[[package]]
2249
-
name = "rand_chacha"
2250
-
version = "0.9.0"
2251
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2252
-
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
2253
-
dependencies = [
2254
-
"ppv-lite86",
2255
-
"rand_core 0.9.3",
2256
-
]
2257
-
2258
-
[[package]]
2259
-
name = "rand_core"
2260
-
version = "0.6.4"
2261
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2262
-
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
2263
-
dependencies = [
2264
-
"getrandom 0.2.16",
2265
-
]
2266
-
2267
-
[[package]]
2268
-
name = "rand_core"
2269
-
version = "0.9.3"
2270
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2271
-
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
2272
-
dependencies = [
2273
-
"getrandom 0.3.3",
2274
-
]
2275
-
2276
-
[[package]]
2277
-
name = "redox_syscall"
2278
-
version = "0.5.18"
2279
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2280
-
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
2281
-
dependencies = [
2282
-
"bitflags",
2283
-
]
2284
-
2285
-
[[package]]
2286
-
name = "regex"
2287
-
version = "1.11.3"
2288
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2289
-
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
2290
-
dependencies = [
2291
-
"aho-corasick",
2292
-
"memchr",
2293
-
"regex-automata",
2294
-
"regex-syntax",
2295
-
]
2296
-
2297
-
[[package]]
2298
-
name = "regex-automata"
2299
-
version = "0.4.11"
2300
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2301
-
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
2302
-
dependencies = [
2303
-
"aho-corasick",
2304
-
"memchr",
2305
-
"regex-syntax",
2306
-
]
2307
-
2308
-
[[package]]
2309
-
name = "regex-lite"
2310
-
version = "0.1.7"
2311
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2312
-
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
2313
-
2314
-
[[package]]
2315
-
name = "regex-syntax"
2316
-
version = "0.8.6"
2317
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2318
-
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
2319
-
2320
-
[[package]]
2321
-
name = "reqwest"
2322
-
version = "0.12.23"
2323
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2324
-
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
2325
-
dependencies = [
2326
-
"async-compression",
2327
-
"base64 0.22.1",
2328
-
"bytes",
2329
-
"futures-core",
2330
-
"futures-util",
2331
-
"http 1.3.1",
2332
-
"http-body",
2333
-
"http-body-util",
2334
-
"hyper",
2335
-
"hyper-tls",
2336
-
"hyper-util",
2337
-
"js-sys",
2338
-
"log",
2339
-
"native-tls",
2340
-
"percent-encoding",
2341
-
"pin-project-lite",
2342
-
"rustls-pki-types",
2343
-
"serde",
2344
-
"serde_json",
2345
-
"serde_urlencoded",
2346
-
"sync_wrapper",
2347
-
"tokio",
2348
-
"tokio-native-tls",
2349
-
"tokio-util",
2350
-
"tower",
2351
-
"tower-http",
2352
-
"tower-service",
2353
-
"url",
2354
-
"wasm-bindgen",
2355
-
"wasm-bindgen-futures",
2356
-
"web-sys",
2357
-
]
2358
-
2359
-
[[package]]
2360
-
name = "resolv-conf"
2361
-
version = "0.7.5"
2362
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2363
-
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
2364
-
2365
-
[[package]]
2366
-
name = "rfc6979"
2367
-
version = "0.4.0"
2368
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2369
-
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
2370
-
dependencies = [
2371
-
"hmac",
2372
-
"subtle",
2373
-
]
2374
-
2375
-
[[package]]
2376
-
name = "rustc-demangle"
2377
-
version = "0.1.26"
2378
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2379
-
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
2380
-
2381
-
[[package]]
2382
-
name = "rustc_version"
2383
-
version = "0.4.1"
2384
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2385
-
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
2386
-
dependencies = [
2387
-
"semver",
2388
-
]
2389
-
2390
-
[[package]]
2391
-
name = "rustix"
2392
-
version = "1.1.2"
2393
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2394
-
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
2395
-
dependencies = [
2396
-
"bitflags",
2397
-
"errno",
2398
-
"libc",
2399
-
"linux-raw-sys",
2400
-
"windows-sys 0.61.1",
2401
-
]
2402
-
2403
-
[[package]]
2404
-
name = "rustls-pki-types"
2405
-
version = "1.12.0"
2406
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2407
-
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
2408
-
dependencies = [
2409
-
"zeroize",
2410
-
]
2411
-
2412
-
[[package]]
2413
-
name = "rustversion"
2414
-
version = "1.0.22"
2415
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2416
-
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
2417
-
2418
-
[[package]]
2419
-
name = "ryu"
2420
-
version = "1.0.20"
2421
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2422
-
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
2423
-
2424
-
[[package]]
2425
-
name = "schannel"
2426
-
version = "0.1.28"
2427
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2428
-
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
2429
-
dependencies = [
2430
-
"windows-sys 0.61.1",
2431
-
]
2432
-
2433
-
[[package]]
2434
-
name = "scopeguard"
2435
-
version = "1.2.0"
2436
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2437
-
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
2438
-
2439
-
[[package]]
2440
-
name = "sec1"
2441
-
version = "0.7.3"
2442
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2443
-
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
2444
-
dependencies = [
2445
-
"base16ct",
2446
-
"der",
2447
-
"generic-array",
2448
-
"subtle",
2449
-
"zeroize",
2450
-
]
2451
-
2452
-
[[package]]
2453
-
name = "security-framework"
2454
-
version = "2.11.1"
2455
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2456
-
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
2457
-
dependencies = [
2458
-
"bitflags",
2459
-
"core-foundation",
2460
-
"core-foundation-sys",
2461
-
"libc",
2462
-
"security-framework-sys",
2463
-
]
2464
-
2465
-
[[package]]
2466
-
name = "security-framework-sys"
2467
-
version = "2.15.0"
2468
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2469
-
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
2470
-
dependencies = [
2471
-
"core-foundation-sys",
2472
-
"libc",
2473
-
]
2474
-
2475
-
[[package]]
2476
-
name = "semver"
2477
-
version = "1.0.27"
2478
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2479
-
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
2480
-
2481
-
[[package]]
2482
-
name = "serde"
2483
-
version = "1.0.228"
2484
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2485
-
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
2486
-
dependencies = [
2487
-
"serde_core",
2488
-
"serde_derive",
2489
-
]
2490
-
2491
-
[[package]]
2492
-
name = "serde_bytes"
2493
-
version = "0.11.19"
2494
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2495
-
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
2496
-
dependencies = [
2497
-
"serde",
2498
-
"serde_core",
2499
-
]
2500
-
2501
-
[[package]]
2502
-
name = "serde_core"
2503
-
version = "1.0.228"
2504
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2505
-
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
2506
-
dependencies = [
2507
-
"serde_derive",
2508
-
]
2509
-
2510
-
[[package]]
2511
-
name = "serde_derive"
2512
-
version = "1.0.228"
2513
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2514
-
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
2515
-
dependencies = [
2516
-
"proc-macro2",
2517
-
"quote",
2518
-
"syn 2.0.106",
2519
-
]
2520
-
2521
-
[[package]]
2522
-
name = "serde_html_form"
2523
-
version = "0.2.8"
2524
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2525
-
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
2526
-
dependencies = [
2527
-
"form_urlencoded",
2528
-
"indexmap",
2529
-
"itoa",
2530
-
"ryu",
2531
-
"serde_core",
2532
-
]
2533
-
2534
-
[[package]]
2535
-
name = "serde_json"
2536
-
version = "1.0.145"
2537
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2538
-
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
2539
-
dependencies = [
2540
-
"itoa",
2541
-
"memchr",
2542
-
"ryu",
2543
-
"serde",
2544
-
"serde_core",
2545
-
]
2546
-
2547
-
[[package]]
2548
-
name = "serde_urlencoded"
2549
-
version = "0.7.1"
2550
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2551
-
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
2552
-
dependencies = [
2553
-
"form_urlencoded",
2554
-
"itoa",
2555
-
"ryu",
2556
-
"serde",
2557
-
]
2558
-
2559
-
[[package]]
2560
-
name = "sha1"
2561
-
version = "0.10.6"
2562
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2563
-
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
2564
-
dependencies = [
2565
-
"cfg-if",
2566
-
"cpufeatures",
2567
-
"digest",
2568
-
]
2569
-
2570
-
[[package]]
2571
-
name = "sha2"
2572
-
version = "0.10.9"
2573
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2574
-
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
2575
-
dependencies = [
2576
-
"cfg-if",
2577
-
"cpufeatures",
2578
-
"digest",
2579
-
]
2580
-
2581
-
[[package]]
2582
-
name = "shlex"
2583
-
version = "1.3.0"
2584
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2585
-
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
2586
-
2587
-
[[package]]
2588
-
name = "signal-hook-registry"
2589
-
version = "1.4.6"
2590
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2591
-
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
2592
-
dependencies = [
2593
-
"libc",
2594
-
]
2595
-
2596
-
[[package]]
2597
-
name = "signature"
2598
-
version = "2.2.0"
2599
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2600
-
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
2601
-
dependencies = [
2602
-
"digest",
2603
-
"rand_core 0.6.4",
2604
-
]
2605
-
2606
-
[[package]]
2607
-
name = "simd-adler32"
2608
-
version = "0.3.7"
2609
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2610
-
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
2611
-
2612
-
[[package]]
2613
-
name = "slab"
2614
-
version = "0.4.11"
2615
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2616
-
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
2617
-
2618
-
[[package]]
2619
-
name = "smallvec"
2620
-
version = "1.15.1"
2621
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2622
-
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
2623
-
2624
-
[[package]]
2625
-
name = "socket2"
2626
-
version = "0.5.10"
2627
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2628
-
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
2629
-
dependencies = [
2630
-
"libc",
2631
-
"windows-sys 0.52.0",
2632
-
]
2633
-
2634
-
[[package]]
2635
-
name = "socket2"
2636
-
version = "0.6.0"
2637
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2638
-
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
2639
-
dependencies = [
2640
-
"libc",
2641
-
"windows-sys 0.59.0",
2642
-
]
2643
-
2644
-
[[package]]
2645
-
name = "stable_deref_trait"
2646
-
version = "1.2.0"
2647
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2648
-
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
2649
-
2650
-
[[package]]
2651
-
name = "subtle"
2652
-
version = "2.6.1"
2653
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2654
-
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
2655
-
2656
-
[[package]]
2657
-
name = "syn"
2658
-
version = "1.0.109"
2659
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2660
-
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
2661
-
dependencies = [
2662
-
"proc-macro2",
2663
-
"quote",
2664
-
"unicode-ident",
2665
-
]
2666
-
2667
-
[[package]]
2668
-
name = "syn"
2669
-
version = "2.0.106"
2670
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2671
-
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
2672
-
dependencies = [
2673
-
"proc-macro2",
2674
-
"quote",
2675
-
"unicode-ident",
2676
-
]
2677
-
2678
-
[[package]]
2679
-
name = "sync_wrapper"
2680
-
version = "1.0.2"
2681
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2682
-
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
2683
-
dependencies = [
2684
-
"futures-core",
2685
-
]
2686
-
2687
-
[[package]]
2688
-
name = "synstructure"
2689
-
version = "0.13.2"
2690
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2691
-
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
2692
-
dependencies = [
2693
-
"proc-macro2",
2694
-
"quote",
2695
-
"syn 2.0.106",
2696
-
]
2697
-
2698
-
[[package]]
2699
-
name = "tagptr"
2700
-
version = "0.2.0"
2701
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2702
-
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
2703
-
2704
-
[[package]]
2705
-
name = "tempfile"
2706
-
version = "3.23.0"
2707
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2708
-
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
2709
-
dependencies = [
2710
-
"fastrand",
2711
-
"getrandom 0.3.3",
2712
-
"once_cell",
2713
-
"rustix",
2714
-
"windows-sys 0.61.1",
2715
-
]
2716
-
2717
-
[[package]]
2718
-
name = "thiserror"
2719
-
version = "1.0.69"
2720
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2721
-
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
2722
-
dependencies = [
2723
-
"thiserror-impl",
2724
-
]
2725
-
2726
-
[[package]]
2727
-
name = "thiserror-impl"
2728
-
version = "1.0.69"
2729
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2730
-
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
2731
-
dependencies = [
2732
-
"proc-macro2",
2733
-
"quote",
2734
-
"syn 2.0.106",
2735
-
]
2736
-
2737
-
[[package]]
2738
-
name = "time"
2739
-
version = "0.3.44"
2740
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2741
-
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
2742
-
dependencies = [
2743
-
"deranged",
2744
-
"itoa",
2745
-
"num-conv",
2746
-
"powerfmt",
2747
-
"serde",
2748
-
"time-core",
2749
-
"time-macros",
2750
-
]
2751
-
2752
-
[[package]]
2753
-
name = "time-core"
2754
-
version = "0.1.6"
2755
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2756
-
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
2757
-
2758
-
[[package]]
2759
-
name = "time-macros"
2760
-
version = "0.2.24"
2761
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2762
-
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
2763
-
dependencies = [
2764
-
"num-conv",
2765
-
"time-core",
2766
-
]
2767
-
2768
-
[[package]]
2769
-
name = "tinystr"
2770
-
version = "0.8.1"
2771
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2772
-
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
2773
-
dependencies = [
2774
-
"displaydoc",
2775
-
"zerovec",
2776
-
]
2777
-
2778
-
[[package]]
2779
-
name = "tinyvec"
2780
-
version = "1.10.0"
2781
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2782
-
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
2783
-
dependencies = [
2784
-
"tinyvec_macros",
2785
-
]
2786
-
2787
-
[[package]]
2788
-
name = "tinyvec_macros"
2789
-
version = "0.1.1"
2790
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2791
-
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2792
-
2793
-
[[package]]
2794
-
name = "tokio"
2795
-
version = "1.47.1"
2796
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2797
-
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
2798
-
dependencies = [
2799
-
"backtrace",
2800
-
"bytes",
2801
-
"io-uring",
2802
-
"libc",
2803
-
"mio",
2804
-
"parking_lot",
2805
-
"pin-project-lite",
2806
-
"signal-hook-registry",
2807
-
"slab",
2808
-
"socket2 0.6.0",
2809
-
"tokio-macros",
2810
-
"windows-sys 0.59.0",
2811
-
]
2812
-
2813
-
[[package]]
2814
-
name = "tokio-macros"
2815
-
version = "2.5.0"
2816
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2817
-
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
2818
-
dependencies = [
2819
-
"proc-macro2",
2820
-
"quote",
2821
-
"syn 2.0.106",
2822
-
]
2823
-
2824
-
[[package]]
2825
-
name = "tokio-native-tls"
2826
-
version = "0.3.1"
2827
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2828
-
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
2829
-
dependencies = [
2830
-
"native-tls",
2831
-
"tokio",
2832
-
]
2833
-
2834
-
[[package]]
2835
-
name = "tokio-util"
2836
-
version = "0.7.16"
2837
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2838
-
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
2839
-
dependencies = [
2840
-
"bytes",
2841
-
"futures-core",
2842
-
"futures-sink",
2843
-
"pin-project-lite",
2844
-
"tokio",
2845
-
]
2846
-
2847
-
[[package]]
2848
-
name = "tower"
2849
-
version = "0.5.2"
2850
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2851
-
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
2852
-
dependencies = [
2853
-
"futures-core",
2854
-
"futures-util",
2855
-
"pin-project-lite",
2856
-
"sync_wrapper",
2857
-
"tokio",
2858
-
"tower-layer",
2859
-
"tower-service",
2860
-
]
2861
-
2862
-
[[package]]
2863
-
name = "tower-http"
2864
-
version = "0.6.6"
2865
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2866
-
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
2867
-
dependencies = [
2868
-
"bitflags",
2869
-
"bytes",
2870
-
"futures-util",
2871
-
"http 1.3.1",
2872
-
"http-body",
2873
-
"iri-string",
2874
-
"pin-project-lite",
2875
-
"tower",
2876
-
"tower-layer",
2877
-
"tower-service",
2878
-
]
2879
-
2880
-
[[package]]
2881
-
name = "tower-layer"
2882
-
version = "0.3.3"
2883
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2884
-
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
2885
-
2886
-
[[package]]
2887
-
name = "tower-service"
2888
-
version = "0.3.3"
2889
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2890
-
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
2891
-
2892
-
[[package]]
2893
-
name = "tracing"
2894
-
version = "0.1.41"
2895
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2896
-
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
2897
-
dependencies = [
2898
-
"log",
2899
-
"pin-project-lite",
2900
-
"tracing-attributes",
2901
-
"tracing-core",
2902
-
]
2903
-
2904
-
[[package]]
2905
-
name = "tracing-attributes"
2906
-
version = "0.1.30"
2907
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2908
-
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
2909
-
dependencies = [
2910
-
"proc-macro2",
2911
-
"quote",
2912
-
"syn 2.0.106",
2913
-
]
2914
-
2915
-
[[package]]
2916
-
name = "tracing-core"
2917
-
version = "0.1.34"
2918
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2919
-
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
2920
-
dependencies = [
2921
-
"once_cell",
2922
-
]
2923
-
2924
-
[[package]]
2925
-
name = "trait-variant"
2926
-
version = "0.1.2"
2927
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2928
-
checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
2929
-
dependencies = [
2930
-
"proc-macro2",
2931
-
"quote",
2932
-
"syn 2.0.106",
2933
-
]
2934
-
2935
-
[[package]]
2936
-
name = "try-lock"
2937
-
version = "0.2.5"
2938
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2939
-
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
2940
-
2941
-
[[package]]
2942
-
name = "typenum"
2943
-
version = "1.19.0"
2944
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2945
-
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
2946
-
2947
-
[[package]]
2948
-
name = "unicode-ident"
2949
-
version = "1.0.19"
2950
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2951
-
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
2952
-
2953
-
[[package]]
2954
-
name = "unicode-xid"
2955
-
version = "0.2.6"
2956
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2957
-
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
2958
-
2959
-
[[package]]
2960
-
name = "universal-hash"
2961
-
version = "0.5.1"
2962
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2963
-
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
2964
-
dependencies = [
2965
-
"crypto-common",
2966
-
"subtle",
2967
-
]
2968
-
2969
-
[[package]]
2970
-
name = "unsigned-varint"
2971
-
version = "0.8.0"
2972
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2973
-
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
2974
-
2975
-
[[package]]
2976
-
name = "url"
2977
-
version = "2.5.7"
2978
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2979
-
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
2980
-
dependencies = [
2981
-
"form_urlencoded",
2982
-
"idna",
2983
-
"percent-encoding",
2984
-
"serde",
2985
-
]
2986
-
2987
-
[[package]]
2988
-
name = "utf8_iter"
2989
-
version = "1.0.4"
2990
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2991
-
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2992
-
2993
-
[[package]]
2994
-
name = "utf8parse"
2995
-
version = "0.2.2"
2996
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2997
-
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
2998
-
2999
-
[[package]]
3000
-
name = "uuid"
3001
-
version = "1.18.1"
3002
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3003
-
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
3004
-
dependencies = [
3005
-
"getrandom 0.3.3",
3006
-
"js-sys",
3007
-
"wasm-bindgen",
3008
-
]
3009
-
3010
-
[[package]]
3011
-
name = "vcpkg"
3012
-
version = "0.2.15"
3013
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3014
-
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
3015
-
3016
-
[[package]]
3017
-
name = "version_check"
3018
-
version = "0.9.5"
3019
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3020
-
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
3021
-
3022
-
[[package]]
3023
-
name = "want"
3024
-
version = "0.3.1"
3025
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3026
-
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
3027
-
dependencies = [
3028
-
"try-lock",
3029
-
]
3030
-
3031
-
[[package]]
3032
-
name = "wasi"
3033
-
version = "0.11.1+wasi-snapshot-preview1"
3034
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3035
-
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
3036
-
3037
-
[[package]]
3038
-
name = "wasi"
3039
-
version = "0.14.7+wasi-0.2.4"
3040
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3041
-
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
3042
-
dependencies = [
3043
-
"wasip2",
3044
-
]
3045
-
3046
-
[[package]]
3047
-
name = "wasip2"
3048
-
version = "1.0.1+wasi-0.2.4"
3049
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3050
-
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
3051
-
dependencies = [
3052
-
"wit-bindgen",
3053
-
]
3054
-
3055
-
[[package]]
3056
-
name = "wasm-bindgen"
3057
-
version = "0.2.104"
3058
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3059
-
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
3060
-
dependencies = [
3061
-
"cfg-if",
3062
-
"once_cell",
3063
-
"rustversion",
3064
-
"wasm-bindgen-macro",
3065
-
"wasm-bindgen-shared",
3066
-
]
3067
-
3068
-
[[package]]
3069
-
name = "wasm-bindgen-backend"
3070
-
version = "0.2.104"
3071
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3072
-
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
3073
-
dependencies = [
3074
-
"bumpalo",
3075
-
"log",
3076
-
"proc-macro2",
3077
-
"quote",
3078
-
"syn 2.0.106",
3079
-
"wasm-bindgen-shared",
3080
-
]
3081
-
3082
-
[[package]]
3083
-
name = "wasm-bindgen-futures"
3084
-
version = "0.4.54"
3085
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3086
-
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
3087
-
dependencies = [
3088
-
"cfg-if",
3089
-
"js-sys",
3090
-
"once_cell",
3091
-
"wasm-bindgen",
3092
-
"web-sys",
3093
-
]
3094
-
3095
-
[[package]]
3096
-
name = "wasm-bindgen-macro"
3097
-
version = "0.2.104"
3098
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3099
-
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
3100
-
dependencies = [
3101
-
"quote",
3102
-
"wasm-bindgen-macro-support",
3103
-
]
3104
-
3105
-
[[package]]
3106
-
name = "wasm-bindgen-macro-support"
3107
-
version = "0.2.104"
3108
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3109
-
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
3110
-
dependencies = [
3111
-
"proc-macro2",
3112
-
"quote",
3113
-
"syn 2.0.106",
3114
-
"wasm-bindgen-backend",
3115
-
"wasm-bindgen-shared",
3116
-
]
3117
-
3118
-
[[package]]
3119
-
name = "wasm-bindgen-shared"
3120
-
version = "0.2.104"
3121
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3122
-
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
3123
-
dependencies = [
3124
-
"unicode-ident",
3125
-
]
3126
-
3127
-
[[package]]
3128
-
name = "web-sys"
3129
-
version = "0.3.81"
3130
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3131
-
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
3132
-
dependencies = [
3133
-
"js-sys",
3134
-
"wasm-bindgen",
3135
-
]
3136
-
3137
-
[[package]]
3138
-
name = "web-time"
3139
-
version = "1.1.0"
3140
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3141
-
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
3142
-
dependencies = [
3143
-
"js-sys",
3144
-
"wasm-bindgen",
3145
-
]
3146
-
3147
-
[[package]]
3148
-
name = "widestring"
3149
-
version = "1.2.0"
3150
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3151
-
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
3152
-
3153
-
[[package]]
3154
-
name = "windows-core"
3155
-
version = "0.62.1"
3156
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3157
-
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
3158
-
dependencies = [
3159
-
"windows-implement",
3160
-
"windows-interface",
3161
-
"windows-link",
3162
-
"windows-result",
3163
-
"windows-strings",
3164
-
]
3165
-
3166
-
[[package]]
3167
-
name = "windows-implement"
3168
-
version = "0.60.1"
3169
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3170
-
checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
3171
-
dependencies = [
3172
-
"proc-macro2",
3173
-
"quote",
3174
-
"syn 2.0.106",
3175
-
]
3176
-
3177
-
[[package]]
3178
-
name = "windows-interface"
3179
-
version = "0.59.2"
3180
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3181
-
checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
3182
-
dependencies = [
3183
-
"proc-macro2",
3184
-
"quote",
3185
-
"syn 2.0.106",
3186
-
]
3187
-
3188
-
[[package]]
3189
-
name = "windows-link"
3190
-
version = "0.2.0"
3191
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3192
-
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
3193
-
3194
-
[[package]]
3195
-
name = "windows-result"
3196
-
version = "0.4.0"
3197
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3198
-
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
3199
-
dependencies = [
3200
-
"windows-link",
3201
-
]
3202
-
3203
-
[[package]]
3204
-
name = "windows-strings"
3205
-
version = "0.5.0"
3206
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3207
-
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
3208
-
dependencies = [
3209
-
"windows-link",
3210
-
]
3211
-
3212
-
[[package]]
3213
-
name = "windows-sys"
3214
-
version = "0.48.0"
3215
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3216
-
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
3217
-
dependencies = [
3218
-
"windows-targets 0.48.5",
3219
-
]
3220
-
3221
-
[[package]]
3222
-
name = "windows-sys"
3223
-
version = "0.52.0"
3224
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3225
-
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
3226
-
dependencies = [
3227
-
"windows-targets 0.52.6",
3228
-
]
3229
-
3230
-
[[package]]
3231
-
name = "windows-sys"
3232
-
version = "0.59.0"
3233
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3234
-
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
3235
-
dependencies = [
3236
-
"windows-targets 0.52.6",
3237
-
]
3238
-
3239
-
[[package]]
3240
-
name = "windows-sys"
3241
-
version = "0.60.2"
3242
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3243
-
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
3244
-
dependencies = [
3245
-
"windows-targets 0.53.4",
3246
-
]
3247
-
3248
-
[[package]]
3249
-
name = "windows-sys"
3250
-
version = "0.61.1"
3251
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3252
-
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
3253
-
dependencies = [
3254
-
"windows-link",
3255
-
]
3256
-
3257
-
[[package]]
3258
-
name = "windows-targets"
3259
-
version = "0.48.5"
3260
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3261
-
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
3262
-
dependencies = [
3263
-
"windows_aarch64_gnullvm 0.48.5",
3264
-
"windows_aarch64_msvc 0.48.5",
3265
-
"windows_i686_gnu 0.48.5",
3266
-
"windows_i686_msvc 0.48.5",
3267
-
"windows_x86_64_gnu 0.48.5",
3268
-
"windows_x86_64_gnullvm 0.48.5",
3269
-
"windows_x86_64_msvc 0.48.5",
3270
-
]
3271
-
3272
-
[[package]]
3273
-
name = "windows-targets"
3274
-
version = "0.52.6"
3275
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3276
-
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
3277
-
dependencies = [
3278
-
"windows_aarch64_gnullvm 0.52.6",
3279
-
"windows_aarch64_msvc 0.52.6",
3280
-
"windows_i686_gnu 0.52.6",
3281
-
"windows_i686_gnullvm 0.52.6",
3282
-
"windows_i686_msvc 0.52.6",
3283
-
"windows_x86_64_gnu 0.52.6",
3284
-
"windows_x86_64_gnullvm 0.52.6",
3285
-
"windows_x86_64_msvc 0.52.6",
3286
-
]
3287
-
3288
-
[[package]]
3289
-
name = "windows-targets"
3290
-
version = "0.53.4"
3291
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3292
-
checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
3293
-
dependencies = [
3294
-
"windows-link",
3295
-
"windows_aarch64_gnullvm 0.53.0",
3296
-
"windows_aarch64_msvc 0.53.0",
3297
-
"windows_i686_gnu 0.53.0",
3298
-
"windows_i686_gnullvm 0.53.0",
3299
-
"windows_i686_msvc 0.53.0",
3300
-
"windows_x86_64_gnu 0.53.0",
3301
-
"windows_x86_64_gnullvm 0.53.0",
3302
-
"windows_x86_64_msvc 0.53.0",
3303
-
]
3304
-
3305
-
[[package]]
3306
-
name = "windows_aarch64_gnullvm"
3307
-
version = "0.48.5"
3308
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3309
-
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
3310
-
3311
-
[[package]]
3312
-
name = "windows_aarch64_gnullvm"
3313
-
version = "0.52.6"
3314
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3315
-
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
3316
-
3317
-
[[package]]
3318
-
name = "windows_aarch64_gnullvm"
3319
-
version = "0.53.0"
3320
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3321
-
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
3322
-
3323
-
[[package]]
3324
-
name = "windows_aarch64_msvc"
3325
-
version = "0.48.5"
3326
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3327
-
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
3328
-
3329
-
[[package]]
3330
-
name = "windows_aarch64_msvc"
3331
-
version = "0.52.6"
3332
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3333
-
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
3334
-
3335
-
[[package]]
3336
-
name = "windows_aarch64_msvc"
3337
-
version = "0.53.0"
3338
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3339
-
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
3340
-
3341
-
[[package]]
3342
-
name = "windows_i686_gnu"
3343
-
version = "0.48.5"
3344
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3345
-
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
3346
-
3347
-
[[package]]
3348
-
name = "windows_i686_gnu"
3349
-
version = "0.52.6"
3350
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3351
-
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
3352
-
3353
-
[[package]]
3354
-
name = "windows_i686_gnu"
3355
-
version = "0.53.0"
3356
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3357
-
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
3358
-
3359
-
[[package]]
3360
-
name = "windows_i686_gnullvm"
3361
-
version = "0.52.6"
3362
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3363
-
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
3364
-
3365
-
[[package]]
3366
-
name = "windows_i686_gnullvm"
3367
-
version = "0.53.0"
3368
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3369
-
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
3370
-
3371
-
[[package]]
3372
-
name = "windows_i686_msvc"
3373
-
version = "0.48.5"
3374
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3375
-
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
3376
-
3377
-
[[package]]
3378
-
name = "windows_i686_msvc"
3379
-
version = "0.52.6"
3380
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3381
-
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
3382
-
3383
-
[[package]]
3384
-
name = "windows_i686_msvc"
3385
-
version = "0.53.0"
3386
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3387
-
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
3388
-
3389
-
[[package]]
3390
-
name = "windows_x86_64_gnu"
3391
-
version = "0.48.5"
3392
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3393
-
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
3394
-
3395
-
[[package]]
3396
-
name = "windows_x86_64_gnu"
3397
-
version = "0.52.6"
3398
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3399
-
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
3400
-
3401
-
[[package]]
3402
-
name = "windows_x86_64_gnu"
3403
-
version = "0.53.0"
3404
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3405
-
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
3406
-
3407
-
[[package]]
3408
-
name = "windows_x86_64_gnullvm"
3409
-
version = "0.48.5"
3410
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3411
-
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
3412
-
3413
-
[[package]]
3414
-
name = "windows_x86_64_gnullvm"
3415
-
version = "0.52.6"
3416
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3417
-
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
3418
-
3419
-
[[package]]
3420
-
name = "windows_x86_64_gnullvm"
3421
-
version = "0.53.0"
3422
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3423
-
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
3424
-
3425
-
[[package]]
3426
-
name = "windows_x86_64_msvc"
3427
-
version = "0.48.5"
3428
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3429
-
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
3430
-
3431
-
[[package]]
3432
-
name = "windows_x86_64_msvc"
3433
-
version = "0.52.6"
3434
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3435
-
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
3436
-
3437
-
[[package]]
3438
-
name = "windows_x86_64_msvc"
3439
-
version = "0.53.0"
3440
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3441
-
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
3442
-
3443
-
[[package]]
3444
-
name = "winreg"
3445
-
version = "0.50.0"
3446
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3447
-
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
3448
-
dependencies = [
3449
-
"cfg-if",
3450
-
"windows-sys 0.48.0",
3451
-
]
3452
-
3453
-
[[package]]
3454
-
name = "wit-bindgen"
3455
-
version = "0.46.0"
3456
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3457
-
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
3458
-
3459
-
[[package]]
3460
-
name = "writeable"
3461
-
version = "0.6.1"
3462
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3463
-
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
3464
-
3465
-
[[package]]
3466
-
name = "yoke"
3467
-
version = "0.8.0"
3468
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3469
-
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
3470
-
dependencies = [
3471
-
"serde",
3472
-
"stable_deref_trait",
3473
-
"yoke-derive",
3474
-
"zerofrom",
3475
-
]
3476
-
3477
-
[[package]]
3478
-
name = "yoke-derive"
3479
-
version = "0.8.0"
3480
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3481
-
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
3482
-
dependencies = [
3483
-
"proc-macro2",
3484
-
"quote",
3485
-
"syn 2.0.106",
3486
-
"synstructure",
3487
-
]
3488
-
3489
-
[[package]]
3490
-
name = "zerocopy"
3491
-
version = "0.8.27"
3492
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3493
-
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
3494
-
dependencies = [
3495
-
"zerocopy-derive",
3496
-
]
3497
-
3498
-
[[package]]
3499
-
name = "zerocopy-derive"
3500
-
version = "0.8.27"
3501
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3502
-
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
3503
-
dependencies = [
3504
-
"proc-macro2",
3505
-
"quote",
3506
-
"syn 2.0.106",
3507
-
]
3508
-
3509
-
[[package]]
3510
-
name = "zerofrom"
3511
-
version = "0.1.6"
3512
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3513
-
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
3514
-
dependencies = [
3515
-
"zerofrom-derive",
3516
-
]
3517
-
3518
-
[[package]]
3519
-
name = "zerofrom-derive"
3520
-
version = "0.1.6"
3521
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3522
-
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
3523
-
dependencies = [
3524
-
"proc-macro2",
3525
-
"quote",
3526
-
"syn 2.0.106",
3527
-
"synstructure",
3528
-
]
3529
-
3530
-
[[package]]
3531
-
name = "zeroize"
3532
-
version = "1.8.2"
3533
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3534
-
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
3535
-
dependencies = [
3536
-
"serde",
3537
-
]
3538
-
3539
-
[[package]]
3540
-
name = "zerotrie"
3541
-
version = "0.2.2"
3542
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3543
-
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
3544
-
dependencies = [
3545
-
"displaydoc",
3546
-
"yoke",
3547
-
"zerofrom",
3548
-
]
3549
-
3550
-
[[package]]
3551
-
name = "zerovec"
3552
-
version = "0.11.4"
3553
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3554
-
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
3555
-
dependencies = [
3556
-
"yoke",
3557
-
"zerofrom",
3558
-
"zerovec-derive",
3559
-
]
3560
-
3561
-
[[package]]
3562
-
name = "zerovec-derive"
3563
-
version = "0.11.1"
3564
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3565
-
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
3566
-
dependencies = [
3567
-
"proc-macro2",
3568
-
"quote",
3569
-
"syn 2.0.106",
3570
-
]
3571
-
3572
-
[[package]]
3573
-
name = "zstd"
3574
-
version = "0.13.3"
3575
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3576
-
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
3577
-
dependencies = [
3578
-
"zstd-safe",
3579
-
]
3580
-
3581
-
[[package]]
3582
-
name = "zstd-safe"
3583
-
version = "7.2.4"
3584
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3585
-
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
3586
-
dependencies = [
3587
-
"zstd-sys",
3588
-
]
3589
-
3590
-
[[package]]
3591
-
name = "zstd-sys"
3592
-
version = "2.0.16+zstd.1.5.7"
3593
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3594
-
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
3595
-
dependencies = [
3596
-
"cc",
3597
-
"pkg-config",
3598
-
]
-18
Cargo.toml
-18
Cargo.toml
···
1
-
[package]
2
-
name = "at-me"
3
-
version = "0.1.0"
4
-
edition = "2021"
5
-
6
-
[dependencies]
7
-
actix-web = "4.10"
8
-
actix-session = { version = "0.10", features = ["cookie-session"] }
9
-
atrium-api = "0.25"
10
-
atrium-common = "0.1"
11
-
atrium-oauth = "0.1.0"
12
-
atrium-identity = "0.1.3"
13
-
serde = { version = "1.0", features = ["derive"] }
14
-
serde_json = "1.0"
15
-
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
16
-
hickory-resolver = "0.24"
17
-
env_logger = "0.11"
18
-
log = "0.4"
-40
Dockerfile
-40
Dockerfile
···
1
-
# Build stage
2
-
FROM rustlang/rust:nightly-slim AS builder
3
-
4
-
# Install build dependencies
5
-
RUN apt-get update && apt-get install -y \
6
-
pkg-config \
7
-
libssl-dev \
8
-
&& rm -rf /var/lib/apt/lists/*
9
-
10
-
WORKDIR /app
11
-
12
-
# Copy manifests
13
-
COPY Cargo.toml Cargo.lock ./
14
-
15
-
# Copy source code
16
-
COPY src ./src
17
-
COPY static ./static
18
-
19
-
# Build for release
20
-
RUN cargo build --release
21
-
22
-
# Runtime stage
23
-
FROM debian:bookworm-slim
24
-
25
-
# Install runtime dependencies
26
-
RUN apt-get update && apt-get install -y \
27
-
ca-certificates \
28
-
libssl3 \
29
-
&& rm -rf /var/lib/apt/lists/*
30
-
31
-
WORKDIR /app
32
-
33
-
# Copy the built binary
34
-
COPY --from=builder /app/target/release/at-me /app/at-me
35
-
36
-
# Expose port
37
-
EXPOSE 8080
38
-
39
-
# Run the binary
40
-
CMD ["./at-me"]
+21
-6
README.md
+21
-6
README.md
···
1
1
# @me
2
2
3
-
an accessible visualization of how your atproto identity connects to third-party apps.
3
+
an accessible visualization of how your atproto identity connects to atproto apps.
4
4
5
-
[at-me.fly.dev](https://at-me.fly.dev/)
5
+
[at-me.wisp.place](https://at-me.wisp.place/)
6
6
7
7
## what is this
8
8
9
-
in decentralized social networks, you own your identity and your data lives in your personal data server. third-party applications create records in your repository using different lexicons (data schemas).
9
+
in decentralized social networks, you own your identity and your data lives in your personal data server. atproto applications create records in your repository using different lexicons (data schemas).
10
10
11
-
@me shows this visually: your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what record types it stores, then click a record type to view the actual data.
11
+
@me shows this visually: your identity at the center, surrounded by the atproto apps that have created data for you. click an app to see what record types it stores, then click a record type to view the actual data.
12
12
13
13
inspired by [pdsls.dev](https://pdsls.dev).
14
14
15
15
## running locally
16
16
17
17
```bash
18
-
cargo run
18
+
bun install
19
+
bun run dev
19
20
```
20
21
21
-
then visit http://localhost:8080 and sign in with any atproto handle.
22
+
visit http://localhost:3030 to explore any atproto handle.
23
+
24
+
## commands
25
+
26
+
- `bun run dev` - start dev server with hot reloading
27
+
- `bun run build` - build for production (outputs to `dist/`)
28
+
- `bun run preview` - preview production build locally
29
+
30
+
## tech stack
31
+
32
+
- pure client-side javascript (no backend)
33
+
- vite for development and building
34
+
- direct atproto API calls (PDS, PLC directory, Bluesky AppView)
35
+
- jetstream websocket for live firehose streaming
36
+
- client-side MST (merkle search tree) visualization
bun.lockb
bun.lockb
This is a binary file and will not be displayed.
+107
docs/_artifacts/COPY_IMPROVEMENTS.md
+107
docs/_artifacts/COPY_IMPROVEMENTS.md
···
1
+
# copy improvements
2
+
3
+
## the problem
4
+
5
+
the original copy throughout @me was too technical and jargon-heavy for people unfamiliar with atproto. terms like "silos," "atproto identity," "repository," and "Personal Data Server" appeared without context or explanation. this created barriers for the primary audience: regular social media users who might be curious about decentralized social but don't yet understand the tech.
6
+
7
+
more importantly, the copy focused on **how the technology works** rather than **why it matters** to users. people don't care about protocols - they care about not losing their followers when platforms change.
8
+
9
+
## the philosophy
10
+
11
+
drawing from [overreacted.io/open-social](https://overreacted.io/open-social/), we adopted these principles:
12
+
13
+
1. **lead with relatable problems** - "built 10k followers? if you leave, you lose them all"
14
+
2. **use familiar analogies** - "what if social media worked like email?"
15
+
3. **focus on benefits, not technology** - "switch apps anytime, take everything with you"
16
+
4. **provide breadcrumbs** - link every technical term to official docs so curious users can learn more
17
+
18
+
the key insight: if you can't leave without losing something important, the platform has no incentive to respect you. that's the message that resonates with regular users, not "merkle search trees" or "decentralized identity."
19
+
20
+
## what we changed
21
+
22
+
### logged-out experience (login page "what is this?")
23
+
24
+
**before:**
25
+
- "visualize your atproto identity"
26
+
- "the problem with silos"
27
+
- "the atproto solution"
28
+
- heavy use of jargon, abstract concepts
29
+
30
+
**after:**
31
+
- "your posts should be yours" - opens with the actual problem people face
32
+
- "what if social media worked like email?" - uses an analogy everyone understands
33
+
- "see it in action" - simple call to action
34
+
- every technical term links to [atproto.com](https://atproto.com) documentation
35
+
36
+
**why:** logged-out users know nothing about atproto. this is our chance to make them care before introducing any technical concepts.
37
+
38
+
### logged-in experience (? button modal)
39
+
40
+
**before:**
41
+
- "@me - your repository"
42
+
- focused on platform switching
43
+
- generic language about ownership
44
+
45
+
**after:**
46
+
- "this is your data" - personal and direct
47
+
- explains what they're looking at: "you're looking at your Personal Data Server - where your social data actually lives"
48
+
- concrete examples: "bluesky for microblogging. whitewind for long-form posts"
49
+
- defines "open social" in plain terms: "if you don't like an app, switch"
50
+
- ends with clear instructions on how to use the tool
51
+
52
+
**why:** once someone is logged in, they're ready for slightly deeper concepts. but we still prioritize clarity over accuracy, using the visualization to teach what a PDS does.
53
+
54
+
### identity/PDS panel (clicking @ in center)
55
+
56
+
**before:**
57
+
- title: "your repository"
58
+
- subtitle: "what you've built"
59
+
- comparison boxes about traditional vs atproto platforms
60
+
- technical details at bottom
61
+
62
+
**after:**
63
+
- title: "your personal data server"
64
+
- subtitle: "where your social data lives"
65
+
- **"your pds location"** box - explicitly states where the PDS is hosted and what's stored there
66
+
- **"explore your data"** box - links to `pdsls.dev/{pds-domain}` as a next step
67
+
- removed redundant platform comparison (already covered in modals)
68
+
- kept technical details (DID, handle) at bottom
69
+
70
+
**why:** this panel should immediately answer "what is this thing in the center?" and "where is my data actually stored?" the pdsls.dev link gives power users an immediate action item.
71
+
72
+
## the pattern
73
+
74
+
every piece of copy now follows this structure:
75
+
76
+
1. **hook** - relatable problem or question
77
+
2. **explain** - use familiar analogies
78
+
3. **breadcrumb** - link technical terms to docs
79
+
4. **action** - give them something to do or explore
80
+
81
+
examples:
82
+
- login page: problem โ email analogy โ linked "Personal Data Server" โ "explore demo"
83
+
- info modal: "this is your data" โ concrete examples โ linked "open social" โ "how to explore"
84
+
- pds panel: "where your social data lives" โ linked PDS location โ pdsls.dev tool โ technical details
85
+
86
+
## success metrics
87
+
88
+
we'll know this worked if:
89
+
90
+
1. **bounce rate decreases** on login page
91
+
2. **demo mode usage increases** (people want to see it work)
92
+
3. **pdsls.dev referrals** show users are exploring further
93
+
4. **fewer confused questions** from new users
94
+
95
+
more importantly: can you explain this to your non-technical friend and have them understand why they should care? that's the test.
96
+
97
+
## files modified
98
+
99
+
- `src/templates.rs` - login page info section, logged-in info modal
100
+
- `static/app.js` - identity/PDS panel on @ click
101
+
102
+
## resources
103
+
104
+
- [overreacted.io/open-social](https://overreacted.io/open-social/) - the philosophical foundation
105
+
- [atproto.com/guides/data-repos](https://atproto.com/guides/data-repos) - what is a PDS
106
+
- [atproto.com](https://atproto.com) - protocol overview
107
+
- [pdsls.dev](https://pdsls.dev) - tool for exploring PDS contents
+92
docs/firehose.md
+92
docs/firehose.md
···
1
+
# real-time updates via firehose
2
+
3
+
at-me visualizes your atproto activity in real-time using the jetstream firehose.
4
+
5
+
## what is the firehose?
6
+
7
+
the [atproto firehose](https://docs.bsky.app/docs/advanced-guides/firehose) is a WebSocket stream of all repository events across the network. when you create, update, or delete records in your PDS, these events flow through the firehose.
8
+
9
+
we use [jetstream](https://github.com/ericvolp12/jetstream), a more efficient firehose consumer that filters and transforms events.
10
+
11
+
## architecture
12
+
13
+
### backend: rust + server-sent events
14
+
15
+
**firehose manager** (`src/firehose.rs`)
16
+
- maintains WebSocket connections to jetstream
17
+
- one broadcaster per DID being watched
18
+
- smart reconnection with exponential backoff
19
+
- thread-safe using `tokio` and `Arc<Mutex>`
20
+
21
+
**dynamic collection registration**
22
+
- when you click "watch live", we fetch your repo's collections via `com.atproto.repo.describeRepo`
23
+
- registers event ingesters for ALL collections (not just bluesky)
24
+
- this means whitewind, tangled, guestbook, and any future app automatically work
25
+
26
+
**event broadcasting** (`src/routes.rs:firehose_watch`)
27
+
- server-sent events (SSE) endpoint at `/api/firehose/watch?did=<your-did>`
28
+
- filters jetstream events to only those matching your DID and collections
29
+
- broadcasts as JSON: `{action, collection, namespace, did, rkey}`
30
+
31
+
### frontend: particles + circles
32
+
33
+
**WebSocket to SSE bridge** (`static/app.js`)
34
+
- `EventSource` connects to SSE endpoint
35
+
- parses incoming events
36
+
- creates particle animations
37
+
- shows toast notifications
38
+
39
+
**particle system**
40
+
- creates colored particles (green=create, blue=update, red=delete)
41
+
- animates from app circle โ identity (your PDS)
42
+
- uses `requestAnimationFrame` for smooth 60fps
43
+
- easing with cubic bezier for natural motion
44
+
45
+
**dynamic circle management**
46
+
- new app? โ `addAppCircle()` creates it on the fly
47
+
- delete event? โ `removeAppCircle()` cleans up when particle completes
48
+
- circles automatically reposition to maintain even spacing
49
+
50
+
## event flow
51
+
52
+
```
53
+
1. you create a post in bluesky
54
+
2. bluesky writes to your PDS
55
+
3. your PDS emits event to firehose
56
+
4. jetstream filters and forwards to our backend
57
+
5. backend matches your DID + collection
58
+
6. SSE pushes event to your browser
59
+
7. particle animates from bluesky circle to center
60
+
8. identity pulses when particle arrives
61
+
9. toast shows "created post: hello world..."
62
+
```
63
+
64
+
## why it works for any app
65
+
66
+
traditional approaches hardcode collections like `app.bsky.feed.post`. we don't.
67
+
68
+
instead, we:
69
+
1. call `describeRepo` to get YOUR actual collections
70
+
2. register ingesters for everything you have
71
+
3. dynamically create/remove app circles as events flow
72
+
73
+
this means if you use:
74
+
- whitewind โ see blog posts flow in
75
+
- tangled โ see commits flow in
76
+
- at-me guestbook โ see signatures flow in
77
+
- future apps โ automatically supported
78
+
79
+
## performance notes
80
+
81
+
- **caching**: DID resolution cached for 1 hour (`constants::CACHE_TTL`)
82
+
- **buffer**: broadcast channel with 100-event buffer
83
+
- **reconnection**: 5-second delay between retries
84
+
- **cleanup**: connections close when SSE client disconnects
85
+
86
+
## code references
87
+
88
+
- firehose manager: `src/firehose.rs`
89
+
- SSE endpoint: `src/routes.rs:951` (`firehose_watch`)
90
+
- dynamic registration: `src/routes.rs:985` (fetch collections via `describeRepo`)
91
+
- particle animation: `static/app.js:1037` (`animateFirehoseParticles`)
92
+
- circle lifecycle: `static/app.js:1419` (`addAppCircle`), `static/app.js:1646` (`removeAppCircle`)
+51
docs/lexicon.md
+51
docs/lexicon.md
···
1
+
# lexicon
2
+
3
+
## `app.at-me.visit`
4
+
5
+
**status**: unofficial, experimental
6
+
7
+
this is the record type created when users opt-in to "sign the guestbook" on at-me.
8
+
9
+
### namespace rationale
10
+
11
+
we use `app.at-me.visit` rather than a domain-based namespace (like `io.zzstoatzz.*`) because:
12
+
13
+
1. the app is hosted at `at-me.fly.dev`, not under a domain we control
14
+
2. using a personal domain namespace would incorrectly suggest this is an official/owned lexicon
15
+
3. `app.at-me.*` clearly associates records with this specific application
16
+
17
+
this is an **unofficial lexicon** - there is no formal schema definition served at a URL. it's a simple, unvalidated record type for analytics/engagement tracking.
18
+
19
+
### record structure
20
+
21
+
```json
22
+
{
23
+
"$type": "app.at-me.visit",
24
+
"timestamp": "2025-10-25T22:30:00Z",
25
+
"createdAt": "2025-10-25T22:30:00Z",
26
+
"text": "optional message from the visitor"
27
+
}
28
+
```
29
+
30
+
**fields:**
31
+
- `timestamp` (required): ISO 8601 timestamp of when the signature was created
32
+
- `createdAt` (required): ISO 8601 timestamp of when the record was created (typically same as timestamp)
33
+
- `text` (optional): a message left by the visitor, max 280 characters
34
+
35
+
### privacy
36
+
37
+
- users must explicitly authenticate and click "sign guestbook" to create these records
38
+
- records are written to the user's own PDS, which they control
39
+
- the app does not store or aggregate this data
40
+
- users can delete these records at any time through their PDS
41
+
42
+
### philosophy
43
+
44
+
this approach aligns with atproto's principles:
45
+
- user data sovereignty (records live in user's PDS)
46
+
- transparency (users see exactly what's being written)
47
+
- opt-in participation (no tracking without explicit consent)
48
+
49
+
### acknowledgments
50
+
51
+
thanks to [@thisismissem.social](https://bsky.app/profile/thisismissem.social) for putting [lexicon-guestbook](https://github.com/FujoWebDev/lexicon-guestbook) on our radar! [@essentialrandom.bsky.social](https://bsky.app/profile/essentialrandom.bsky.social)'s work on that project - a more fully-featured implementation with per-user guestbooks, moderation, and an appview - helped inform the addition of optional text messages to our simpler global guestbook.
+27
docs/oauth.md
+27
docs/oauth.md
···
1
+
# oauth
2
+
3
+
at-me uses atproto oauth for authentication.
4
+
5
+
## flow
6
+
7
+
1. user enters handle on landing page
8
+
2. app resolves handle โ DID โ authorization server via did document
9
+
3. authorization server redirects to user's pds for consent
10
+
4. user approves, receives redirect back with auth code
11
+
5. app exchanges code for access token
12
+
6. token stored in session, used for authenticated api calls
13
+
14
+
## scopes
15
+
16
+
```rust
17
+
Scope::Known(KnownScope::Atproto),
18
+
Scope::Unknown("repo:app.at-me.visit".to_string()),
19
+
```
20
+
21
+
the granular scope `repo:app.at-me.visit` limits write access to only guestbook records.
22
+
23
+
## session management
24
+
25
+
sessions use actix-web's cookie-based session middleware. authenticated agents cached in-memory by DID for performance (`AGENT_CACHE`).
26
+
27
+
see `src/oauth.rs` for implementation.
-20
fly.toml
-20
fly.toml
···
1
-
app = "at-me"
2
-
primary_region = "ord"
3
-
4
-
[build]
5
-
6
-
[env]
7
-
OAUTH_REDIRECT_URI = "https://at-me.fly.dev/oauth/callback"
8
-
9
-
[http_service]
10
-
internal_port = 8080
11
-
force_https = true
12
-
auto_stop_machines = "suspend"
13
-
auto_start_machines = true
14
-
min_machines_running = 0
15
-
processes = ["app"]
16
-
17
-
[[vm]]
18
-
memory = "256mb"
19
-
cpu_kind = "shared"
20
-
cpus = 1
+70
index.html
+70
index.html
···
1
+
<!DOCTYPE html>
2
+
<html>
3
+
<head>
4
+
<meta charset="UTF-8">
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+
<title>@me - explore your atproto identity</title>
7
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
+
9
+
<!-- Open Graph / Facebook -->
10
+
<meta property="og:type" content="website">
11
+
<meta property="og:url" content="https://at-me.wisp.place/">
12
+
<meta property="og:title" content="@me - explore your atproto identity">
13
+
<meta property="og:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
14
+
<meta property="og:image" content="https://at-me.wisp.place/og-image.png">
15
+
16
+
<!-- Twitter -->
17
+
<meta property="twitter:card" content="summary_large_image">
18
+
<meta property="twitter:url" content="https://at-me.wisp.place/">
19
+
<meta property="twitter:title" content="@me - explore your atproto identity">
20
+
<meta property="twitter:description" content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
21
+
<meta property="twitter:image" content="https://at-me.wisp.place/og-image.png">
22
+
</head>
23
+
<body>
24
+
<div class="atmosphere" id="atmosphere"></div>
25
+
26
+
<div class="container">
27
+
<div class="search-card">
28
+
<h1>@me</h1>
29
+
<div class="subtitle">explore the atmosphere</div>
30
+
<form id="searchForm" onsubmit="event.preventDefault(); handleSearch();">
31
+
<div class="input-wrapper">
32
+
<input type="text" id="handleInput" placeholder="enter any handle" autofocus autocomplete="off" autocapitalize="off" spellcheck="false">
33
+
<span class="search-spinner" id="searchSpinner" style="display: none;">...</span>
34
+
<div class="autocomplete-results" id="autocompleteResults"></div>
35
+
</div>
36
+
<button type="submit">explore</button>
37
+
</form>
38
+
39
+
<div class="divider">try these</div>
40
+
<div class="suggestions">
41
+
<button class="suggestion-btn" onclick="viewHandle('why.bsky.team')">why.bsky.team</button>
42
+
<button class="suggestion-btn" onclick="viewHandle('baileytownsend.dev')">baileytownsend.dev</button>
43
+
<button class="suggestion-btn" onclick="viewHandle('bad-example.com')">bad-example.com</button>
44
+
<button class="suggestion-btn" onclick="viewHandle('void.comind.network')">void.comind.network</button>
45
+
</div>
46
+
47
+
<button type="button" class="info-toggle" onclick="toggleInfo()">what is this?</button>
48
+
49
+
<div class="info-content" id="infoContent">
50
+
<div class="info-section">
51
+
<h3>you should own your data</h3>
52
+
<p>today's social platforms own your data. built 10k followers? wrote years of posts? if you leave, you lose it all. you don't own your network - they do.</p>
53
+
54
+
<h3>what if social media worked like email?</h3>
55
+
<p>with email, you can switch from gmail to protonmail and keep your contacts. same idea here: your posts and followers live on your own server (called a <a href="https://atproto.com/guides/data-repos" target="_blank" rel="noopener noreferrer">Personal Data Server</a>). apps like <a href="https://bsky.app" target="_blank" rel="noopener noreferrer">bluesky</a> just connect to it.</p>
56
+
57
+
<h3>see it in action</h3>
58
+
<p>enter any handle above to see how <a href="https://atproto.com" target="_blank" rel="noopener noreferrer">atproto</a> stores their social data.</p>
59
+
</div>
60
+
</div>
61
+
</div>
62
+
</div>
63
+
64
+
<div class="footer">
65
+
by <a href="https://bsky.app/profile/zzstoatzz.io" target="_blank" rel="noopener noreferrer">@zzstoatzz.io</a>
66
+
</div>
67
+
68
+
<script type="module" src="/src/landing/main.js"></script>
69
+
</body>
70
+
</html>
-7
justfile
-7
justfile
+19
package.json
+19
package.json
···
1
+
{
2
+
"name": "at-me",
3
+
"version": "1.0.0",
4
+
"description": "ATProto PDS visualization tool",
5
+
"type": "module",
6
+
"scripts": {
7
+
"dev": "vite",
8
+
"build": "vite build",
9
+
"preview": "vite preview"
10
+
},
11
+
"dependencies": {
12
+
"@atcute/client": "^2.0.0",
13
+
"@atcute/oauth-browser-client": "^1.0.0",
14
+
"@skyware/firehose": "^0.3.0"
15
+
},
16
+
"devDependencies": {
17
+
"vite": "^6.0.0"
18
+
}
19
+
}
+1
public/_redirects
+1
public/_redirects
···
1
+
# Redirects handled by view/index.html directory structure
+4
public/favicon.svg
+4
public/favicon.svg
+12
public/oauth-client-metadata.json
+12
public/oauth-client-metadata.json
···
1
+
{
2
+
"client_id": "https://at-me.fly.dev/oauth-client-metadata.json",
3
+
"client_name": "at-me",
4
+
"client_uri": "https://at-me.fly.dev",
5
+
"redirect_uris": ["https://at-me.fly.dev/app.html"],
6
+
"scope": "atproto transition:generic",
7
+
"grant_types": ["authorization_code", "refresh_token"],
8
+
"response_types": ["code"],
9
+
"token_endpoint_auth_method": "none",
10
+
"application_type": "web",
11
+
"dpop_bound_access_tokens": true
12
+
}
public/og-image.png
public/og-image.png
This is a binary file and will not be displayed.
+338
src/landing/main.js
+338
src/landing/main.js
···
1
+
// ============================================================================
2
+
// LANDING PAGE - Main Entry Point
3
+
// ============================================================================
4
+
5
+
import './styles.css';
6
+
7
+
// ============================================================================
8
+
// ATPROTO UTILITIES
9
+
// ============================================================================
10
+
11
+
// Resolve a handle to a DID
12
+
async function resolveHandle(handle) {
13
+
try {
14
+
const response = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
15
+
if (response.ok) {
16
+
const data = await response.json();
17
+
return data.did;
18
+
}
19
+
} catch (e) {
20
+
console.error('Failed to resolve handle via Bluesky:', e);
21
+
}
22
+
return null;
23
+
}
24
+
25
+
// Get profile from Bluesky AppView
26
+
async function getProfile(handleOrDid) {
27
+
try {
28
+
const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`);
29
+
if (response.ok) {
30
+
return await response.json();
31
+
}
32
+
} catch (e) {
33
+
console.error('Failed to get profile:', e);
34
+
}
35
+
return null;
36
+
}
37
+
38
+
// Search for handles using Bluesky's search
39
+
async function searchHandles(query) {
40
+
try {
41
+
const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=8`);
42
+
if (response.ok) {
43
+
const data = await response.json();
44
+
return data.actors.map(actor => ({
45
+
handle: actor.handle,
46
+
displayName: actor.displayName || actor.handle,
47
+
avatarUrl: actor.avatar || null
48
+
}));
49
+
}
50
+
} catch (e) {
51
+
console.error('Search failed:', e);
52
+
}
53
+
return [];
54
+
}
55
+
56
+
// Get app avatar from the namespace's well-known profile
57
+
async function getAppAvatar(namespace) {
58
+
const domain = namespace.split('.').reverse().join('.');
59
+
try {
60
+
const profile = await getProfile(domain);
61
+
if (profile && profile.avatar) {
62
+
return profile.avatar;
63
+
}
64
+
} catch (e) {
65
+
// Silently fail
66
+
}
67
+
return null;
68
+
}
69
+
70
+
// Batch fetch app avatars
71
+
async function fetchAppAvatars(namespaces) {
72
+
const avatars = {};
73
+
const promises = namespaces.map(async (ns) => {
74
+
const avatar = await getAppAvatar(ns);
75
+
if (avatar) {
76
+
avatars[ns] = avatar;
77
+
}
78
+
});
79
+
await Promise.all(promises);
80
+
return avatars;
81
+
}
82
+
83
+
// ============================================================================
84
+
// SEARCH & NAVIGATION
85
+
// ============================================================================
86
+
87
+
let searchTimeout = null;
88
+
let autocompleteResults = [];
89
+
90
+
function handleSearch() {
91
+
const handle = document.getElementById('handleInput').value.trim();
92
+
if (handle) {
93
+
viewHandle(handle);
94
+
}
95
+
}
96
+
97
+
function viewHandle(handle) {
98
+
window.location.href = `./view/?handle=${encodeURIComponent(handle)}`;
99
+
}
100
+
101
+
function toggleInfo() {
102
+
document.getElementById('infoContent').classList.toggle('expanded');
103
+
}
104
+
105
+
// Expose to window for onclick handlers
106
+
window.handleSearch = handleSearch;
107
+
window.viewHandle = viewHandle;
108
+
window.toggleInfo = toggleInfo;
109
+
110
+
// ============================================================================
111
+
// AUTOCOMPLETE
112
+
// ============================================================================
113
+
114
+
function escapeHtml(text) {
115
+
const div = document.createElement('div');
116
+
div.textContent = text;
117
+
return div.innerHTML;
118
+
}
119
+
120
+
function hideResults() {
121
+
document.getElementById('autocompleteResults').classList.remove('show');
122
+
}
123
+
124
+
function selectHandle(handle) {
125
+
document.getElementById('handleInput').value = handle;
126
+
autocompleteResults = [];
127
+
hideResults();
128
+
viewHandle(handle);
129
+
}
130
+
131
+
// Expose to window for onclick handlers
132
+
window.selectHandle = selectHandle;
133
+
134
+
async function doSearch(query) {
135
+
const spinner = document.getElementById('searchSpinner');
136
+
137
+
if (query.length < 2) {
138
+
autocompleteResults = [];
139
+
hideResults();
140
+
return;
141
+
}
142
+
143
+
spinner.style.display = 'block';
144
+
145
+
try {
146
+
autocompleteResults = await searchHandles(query);
147
+
renderResults();
148
+
} catch (e) {
149
+
console.error('search failed:', e);
150
+
} finally {
151
+
spinner.style.display = 'none';
152
+
}
153
+
}
154
+
155
+
function renderResults() {
156
+
const resultsDiv = document.getElementById('autocompleteResults');
157
+
158
+
if (autocompleteResults.length === 0) {
159
+
hideResults();
160
+
return;
161
+
}
162
+
163
+
resultsDiv.innerHTML = autocompleteResults.map(result => `
164
+
<button type="button" class="autocomplete-item" onclick="selectHandle('${result.handle}')">
165
+
${result.avatarUrl
166
+
? `<img src="${result.avatarUrl}" alt="" class="autocomplete-avatar">`
167
+
: `<div class="autocomplete-avatar-placeholder">${result.handle[0].toUpperCase()}</div>`
168
+
}
169
+
<div class="autocomplete-info">
170
+
<div class="autocomplete-name">${escapeHtml(result.displayName)}</div>
171
+
<div class="autocomplete-handle">@${escapeHtml(result.handle)}</div>
172
+
</div>
173
+
</button>
174
+
`).join('');
175
+
176
+
resultsDiv.classList.add('show');
177
+
}
178
+
179
+
function initAutocomplete() {
180
+
const handleInput = document.getElementById('handleInput');
181
+
const resultsDiv = document.getElementById('autocompleteResults');
182
+
183
+
handleInput.addEventListener('input', () => {
184
+
if (searchTimeout) clearTimeout(searchTimeout);
185
+
searchTimeout = setTimeout(() => doSearch(handleInput.value), 300);
186
+
});
187
+
188
+
handleInput.addEventListener('keydown', (e) => {
189
+
if (e.key === 'Escape') {
190
+
hideResults();
191
+
}
192
+
});
193
+
194
+
handleInput.addEventListener('focus', () => {
195
+
if (autocompleteResults.length > 0) {
196
+
resultsDiv.classList.add('show');
197
+
}
198
+
});
199
+
200
+
document.addEventListener('click', (e) => {
201
+
if (!e.target.closest('.input-wrapper')) {
202
+
hideResults();
203
+
}
204
+
});
205
+
}
206
+
207
+
// ============================================================================
208
+
// ATMOSPHERE VISUALIZATION
209
+
// ============================================================================
210
+
211
+
async function fetchAtmosphere() {
212
+
const CACHE_KEY = 'atme_atmosphere';
213
+
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
214
+
215
+
const cached = localStorage.getItem(CACHE_KEY);
216
+
if (cached) {
217
+
const { data, timestamp } = JSON.parse(cached);
218
+
if (Date.now() - timestamp < CACHE_DURATION) {
219
+
return data;
220
+
}
221
+
}
222
+
223
+
try {
224
+
const response = await fetch('https://ufos-api.microcosm.blue/collections?order=dids-estimate&limit=50');
225
+
const json = await response.json();
226
+
227
+
// Group by namespace (first two segments)
228
+
const namespaces = {};
229
+
json.collections.forEach(col => {
230
+
const parts = col.nsid.split('.');
231
+
if (parts.length >= 2) {
232
+
const ns = `${parts[0]}.${parts[1]}`;
233
+
if (!namespaces[ns]) {
234
+
namespaces[ns] = {
235
+
namespace: ns,
236
+
dids_total: 0,
237
+
records_total: 0,
238
+
collections: []
239
+
};
240
+
}
241
+
namespaces[ns].dids_total += col.dids_estimate;
242
+
namespaces[ns].records_total += col.creates;
243
+
namespaces[ns].collections.push(col.nsid);
244
+
}
245
+
});
246
+
247
+
const data = Object.values(namespaces).sort((a, b) => b.dids_total - a.dids_total).slice(0, 30);
248
+
249
+
localStorage.setItem(CACHE_KEY, JSON.stringify({
250
+
data,
251
+
timestamp: Date.now()
252
+
}));
253
+
254
+
return data;
255
+
} catch (e) {
256
+
console.error('Failed to fetch atmosphere data:', e);
257
+
return [];
258
+
}
259
+
}
260
+
261
+
async function renderAtmosphere() {
262
+
const data = await fetchAtmosphere();
263
+
if (!data.length) return;
264
+
265
+
const atmosphere = document.getElementById('atmosphere');
266
+
const maxSize = Math.max(...data.map(d => d.dids_total));
267
+
268
+
const namespaces = data.map(app => app.namespace);
269
+
const avatarPromise = fetchAppAvatars(namespaces);
270
+
const orbRegistry = [];
271
+
272
+
data.forEach((app, i) => {
273
+
const orb = document.createElement('div');
274
+
orb.className = 'app-orb';
275
+
276
+
// Size based on user count (20-80px)
277
+
const size = 20 + (app.dids_total / maxSize) * 60;
278
+
279
+
// Position in 3D space
280
+
const angle = (i / data.length) * Math.PI * 2;
281
+
const radius = 250 + (i % 3) * 100;
282
+
const y = (i % 5) * 80 - 160;
283
+
const x = Math.cos(angle) * radius;
284
+
const z = Math.sin(angle) * radius;
285
+
286
+
orb.style.width = `${size}px`;
287
+
orb.style.height = `${size}px`;
288
+
orb.style.left = `calc(50% + ${x}px)`;
289
+
orb.style.top = `calc(50% + ${y}px)`;
290
+
orb.style.transform = `translateZ(${z}px) translate(-50%, -50%)`;
291
+
orb.style.background = `radial-gradient(circle, rgba(255,255,255,0.1), rgba(255,255,255,0.02))`;
292
+
orb.style.border = '1px solid rgba(255,255,255,0.1)';
293
+
orb.style.boxShadow = '0 0 20px rgba(255,255,255,0.1)';
294
+
295
+
// Fallback letter
296
+
const letter = app.namespace.split('.')[1]?.[0]?.toUpperCase() || app.namespace[0].toUpperCase();
297
+
orb.innerHTML = `<div class="fallback">${letter}</div>`;
298
+
299
+
// Tooltip
300
+
const tooltip = document.createElement('div');
301
+
tooltip.className = 'app-tooltip';
302
+
const users = app.dids_total >= 1000000
303
+
? `${(app.dids_total / 1000000).toFixed(1)}M users`
304
+
: `${(app.dids_total / 1000).toFixed(0)}K users`;
305
+
tooltip.textContent = `${app.namespace} - ${users}`;
306
+
orb.appendChild(tooltip);
307
+
308
+
atmosphere.appendChild(orb);
309
+
310
+
orbRegistry.push({ orb, tooltip, namespace: app.namespace });
311
+
});
312
+
313
+
avatarPromise.then(avatarMap => {
314
+
orbRegistry.forEach(({ orb, tooltip, namespace }) => {
315
+
const avatarUrl = avatarMap[namespace];
316
+
if (avatarUrl) {
317
+
orb.innerHTML = `<img src="${avatarUrl}" alt="${namespace}" />`;
318
+
orb.appendChild(tooltip);
319
+
}
320
+
});
321
+
});
322
+
}
323
+
324
+
// ============================================================================
325
+
// INITIALIZATION
326
+
// ============================================================================
327
+
328
+
function init() {
329
+
initAutocomplete();
330
+
renderAtmosphere();
331
+
}
332
+
333
+
// Start when DOM is ready
334
+
if (document.readyState === 'loading') {
335
+
document.addEventListener('DOMContentLoaded', init);
336
+
} else {
337
+
init();
338
+
}
+420
src/landing/styles.css
+420
src/landing/styles.css
···
1
+
/* Landing page styles */
2
+
3
+
* { margin: 0; padding: 0; box-sizing: border-box; }
4
+
5
+
body {
6
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
7
+
min-height: 100vh;
8
+
background: radial-gradient(ellipse at center, #0a0a0f 0%, #000000 100%);
9
+
color: #e5e5e5;
10
+
overflow: hidden;
11
+
perspective: 1000px;
12
+
}
13
+
14
+
@media (max-width: 768px) {
15
+
body {
16
+
overflow-y: auto;
17
+
overflow-x: hidden;
18
+
}
19
+
}
20
+
21
+
.atmosphere {
22
+
position: fixed;
23
+
inset: 0;
24
+
transform-style: preserve-3d;
25
+
animation: rotate 120s infinite linear;
26
+
}
27
+
28
+
@keyframes rotate {
29
+
from { transform: rotateY(0deg); }
30
+
to { transform: rotateY(360deg); }
31
+
}
32
+
33
+
.app-orb {
34
+
position: absolute;
35
+
border-radius: 50%;
36
+
display: flex;
37
+
align-items: center;
38
+
justify-content: center;
39
+
transition: all 0.3s ease;
40
+
cursor: pointer;
41
+
backdrop-filter: blur(4px);
42
+
}
43
+
44
+
.app-orb:hover {
45
+
transform: scale(1.2) !important;
46
+
z-index: 100;
47
+
}
48
+
49
+
.app-orb img {
50
+
width: 100%;
51
+
height: 100%;
52
+
border-radius: 50%;
53
+
object-fit: cover;
54
+
}
55
+
56
+
.app-orb .fallback {
57
+
font-size: 1.5rem;
58
+
font-weight: 600;
59
+
color: rgba(255, 255, 255, 0.9);
60
+
}
61
+
62
+
.app-tooltip {
63
+
position: absolute;
64
+
background: rgba(10, 10, 15, 0.95);
65
+
border: 1px solid rgba(255, 255, 255, 0.1);
66
+
padding: 0.5rem 0.75rem;
67
+
border-radius: 4px;
68
+
font-size: 0.7rem;
69
+
white-space: nowrap;
70
+
pointer-events: none;
71
+
opacity: 0;
72
+
transition: opacity 0.2s;
73
+
z-index: 1000;
74
+
}
75
+
76
+
.app-orb:hover .app-tooltip {
77
+
opacity: 1;
78
+
}
79
+
80
+
.container {
81
+
position: fixed;
82
+
inset: 0;
83
+
display: flex;
84
+
align-items: center;
85
+
justify-content: center;
86
+
z-index: 10;
87
+
}
88
+
89
+
@media (max-width: 768px) {
90
+
.container {
91
+
position: relative;
92
+
min-height: 100vh;
93
+
padding: 2rem 0;
94
+
}
95
+
}
96
+
97
+
.search-card {
98
+
background: transparent;
99
+
border: 1px solid rgba(255, 255, 255, 0.1);
100
+
padding: 2.5rem 3rem;
101
+
border-radius: 8px;
102
+
backdrop-filter: blur(2px);
103
+
text-align: center;
104
+
max-width: min(500px, 90vw);
105
+
}
106
+
107
+
h1 {
108
+
font-size: 2rem;
109
+
margin-bottom: 0.5rem;
110
+
font-weight: 300;
111
+
letter-spacing: 0.05em;
112
+
}
113
+
114
+
.subtitle {
115
+
font-size: 0.75rem;
116
+
color: rgba(255, 255, 255, 0.5);
117
+
margin-bottom: 2rem;
118
+
}
119
+
120
+
input {
121
+
font-family: inherit;
122
+
font-size: 0.9rem;
123
+
padding: 0.75rem 1rem;
124
+
margin-bottom: 1rem;
125
+
background: rgba(10, 10, 15, 0.8);
126
+
border: 1px solid rgba(255, 255, 255, 0.2);
127
+
border-radius: 4px;
128
+
color: #e5e5e5;
129
+
width: 100%;
130
+
transition: all 0.2s;
131
+
}
132
+
133
+
input:focus {
134
+
outline: none;
135
+
border-color: rgba(255, 255, 255, 0.4);
136
+
background: rgba(10, 10, 15, 0.9);
137
+
}
138
+
139
+
input::placeholder {
140
+
color: rgba(255, 255, 255, 0.3);
141
+
}
142
+
143
+
.input-wrapper {
144
+
position: relative;
145
+
width: 100%;
146
+
}
147
+
148
+
.autocomplete-results {
149
+
position: absolute;
150
+
z-index: 100;
151
+
width: 100%;
152
+
max-height: 240px;
153
+
overflow-y: auto;
154
+
background: rgba(10, 10, 15, 0.98);
155
+
border: 1px solid rgba(255, 255, 255, 0.2);
156
+
border-radius: 4px;
157
+
margin-top: 0.25rem;
158
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
159
+
display: none;
160
+
scrollbar-width: thin;
161
+
scrollbar-color: rgba(255, 255, 255, 0.2) rgba(10, 10, 15, 0.5);
162
+
}
163
+
164
+
.autocomplete-results::-webkit-scrollbar {
165
+
width: 8px;
166
+
}
167
+
168
+
.autocomplete-results::-webkit-scrollbar-track {
169
+
background: rgba(10, 10, 15, 0.5);
170
+
border-radius: 4px;
171
+
}
172
+
173
+
.autocomplete-results::-webkit-scrollbar-thumb {
174
+
background: rgba(255, 255, 255, 0.2);
175
+
border-radius: 4px;
176
+
}
177
+
178
+
.autocomplete-results::-webkit-scrollbar-thumb:hover {
179
+
background: rgba(255, 255, 255, 0.3);
180
+
}
181
+
182
+
.autocomplete-results.show {
183
+
display: block;
184
+
}
185
+
186
+
.autocomplete-item {
187
+
width: 100%;
188
+
display: flex;
189
+
align-items: center;
190
+
gap: 0.75rem;
191
+
padding: 0.75rem;
192
+
background: transparent;
193
+
border: none;
194
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
195
+
color: #e5e5e5;
196
+
text-align: left;
197
+
font-family: inherit;
198
+
cursor: pointer;
199
+
transition: background 0.15s;
200
+
}
201
+
202
+
.autocomplete-item:last-child {
203
+
border-bottom: none;
204
+
}
205
+
206
+
.autocomplete-item:hover {
207
+
background: rgba(255, 255, 255, 0.1);
208
+
}
209
+
210
+
.autocomplete-avatar {
211
+
width: 36px;
212
+
height: 36px;
213
+
border-radius: 50%;
214
+
object-fit: cover;
215
+
border: 1px solid rgba(255, 255, 255, 0.2);
216
+
flex-shrink: 0;
217
+
}
218
+
219
+
.autocomplete-avatar-placeholder {
220
+
width: 36px;
221
+
height: 36px;
222
+
border-radius: 50%;
223
+
background: rgba(255, 255, 255, 0.1);
224
+
flex-shrink: 0;
225
+
display: flex;
226
+
align-items: center;
227
+
justify-content: center;
228
+
font-size: 0.9rem;
229
+
color: rgba(255, 255, 255, 0.5);
230
+
}
231
+
232
+
.autocomplete-info {
233
+
flex: 1;
234
+
min-width: 0;
235
+
overflow: hidden;
236
+
}
237
+
238
+
.autocomplete-name {
239
+
font-weight: 500;
240
+
color: rgba(255, 255, 255, 0.9);
241
+
margin-bottom: 0.125rem;
242
+
overflow: hidden;
243
+
text-overflow: ellipsis;
244
+
white-space: nowrap;
245
+
font-size: 0.85rem;
246
+
}
247
+
248
+
.autocomplete-handle {
249
+
font-size: 0.75rem;
250
+
color: rgba(255, 255, 255, 0.5);
251
+
overflow: hidden;
252
+
text-overflow: ellipsis;
253
+
white-space: nowrap;
254
+
}
255
+
256
+
.search-spinner {
257
+
position: absolute;
258
+
right: 0.75rem;
259
+
top: 50%;
260
+
transform: translateY(-50%);
261
+
color: rgba(255, 255, 255, 0.4);
262
+
font-size: 0.75rem;
263
+
}
264
+
265
+
button {
266
+
font-family: inherit;
267
+
font-size: 0.9rem;
268
+
padding: 0.75rem 2rem;
269
+
cursor: pointer;
270
+
background: rgba(10, 10, 15, 0.8);
271
+
border: 1px solid rgba(255, 255, 255, 0.2);
272
+
border-radius: 4px;
273
+
color: #e5e5e5;
274
+
transition: all 0.2s;
275
+
width: 100%;
276
+
}
277
+
278
+
button:hover {
279
+
background: rgba(10, 10, 15, 0.9);
280
+
border-color: rgba(255, 255, 255, 0.4);
281
+
}
282
+
283
+
.divider {
284
+
display: flex;
285
+
align-items: center;
286
+
gap: 1rem;
287
+
margin: 1.5rem 0 1rem;
288
+
color: rgba(255, 255, 255, 0.3);
289
+
font-size: 0.7rem;
290
+
}
291
+
292
+
.divider::before,
293
+
.divider::after {
294
+
content: '';
295
+
flex: 1;
296
+
height: 1px;
297
+
background: rgba(255, 255, 255, 0.1);
298
+
}
299
+
300
+
.suggestions {
301
+
display: flex;
302
+
gap: 0.75rem;
303
+
flex-wrap: wrap;
304
+
justify-content: center;
305
+
}
306
+
307
+
.suggestion-btn {
308
+
font-family: inherit;
309
+
font-size: 0.8rem;
310
+
padding: 0.5rem 1rem;
311
+
cursor: pointer;
312
+
background: transparent;
313
+
border: 1px solid rgba(255, 255, 255, 0.15);
314
+
border-radius: 4px;
315
+
color: rgba(255, 255, 255, 0.6);
316
+
transition: all 0.2s;
317
+
width: auto;
318
+
}
319
+
320
+
.suggestion-btn:hover {
321
+
background: rgba(10, 10, 15, 0.5);
322
+
border-color: rgba(255, 255, 255, 0.3);
323
+
color: rgba(255, 255, 255, 0.8);
324
+
}
325
+
326
+
.info-toggle {
327
+
margin-top: 1.5rem;
328
+
color: rgba(255, 255, 255, 0.5);
329
+
font-size: 0.75rem;
330
+
cursor: pointer;
331
+
border: none;
332
+
background: none;
333
+
padding: 0.5rem;
334
+
transition: color 0.2s;
335
+
text-decoration: underline;
336
+
text-underline-offset: 2px;
337
+
}
338
+
339
+
.info-toggle:hover {
340
+
color: rgba(255, 255, 255, 0.8);
341
+
}
342
+
343
+
.info-content {
344
+
max-height: 0;
345
+
overflow: hidden;
346
+
transition: max-height 0.3s ease;
347
+
margin-top: 1rem;
348
+
}
349
+
350
+
.info-content.expanded {
351
+
max-height: 500px;
352
+
overflow-y: auto;
353
+
}
354
+
355
+
@media (max-width: 768px) {
356
+
.info-content.expanded {
357
+
max-height: none;
358
+
overflow-y: visible;
359
+
}
360
+
}
361
+
362
+
.info-section {
363
+
background: rgba(10, 10, 15, 0.6);
364
+
border: 1px solid rgba(255, 255, 255, 0.1);
365
+
border-radius: 4px;
366
+
padding: 1.5rem;
367
+
text-align: left;
368
+
}
369
+
370
+
.info-section h3 {
371
+
font-size: 0.85rem;
372
+
font-weight: 500;
373
+
margin-bottom: 0.75rem;
374
+
color: rgba(255, 255, 255, 0.9);
375
+
}
376
+
377
+
.info-section p {
378
+
font-size: 0.7rem;
379
+
line-height: 1.6;
380
+
color: rgba(255, 255, 255, 0.6);
381
+
margin-bottom: 1rem;
382
+
}
383
+
384
+
.info-section p:last-child {
385
+
margin-bottom: 0;
386
+
}
387
+
388
+
.info-section strong {
389
+
color: rgba(255, 255, 255, 0.85);
390
+
}
391
+
392
+
.info-section a {
393
+
color: rgba(255, 255, 255, 0.8);
394
+
text-decoration: underline;
395
+
text-underline-offset: 2px;
396
+
}
397
+
398
+
.info-section a:hover {
399
+
color: rgba(255, 255, 255, 1);
400
+
}
401
+
402
+
.footer {
403
+
position: fixed;
404
+
bottom: 1rem;
405
+
left: 50%;
406
+
transform: translateX(-50%);
407
+
font-size: 0.7rem;
408
+
color: rgba(255, 255, 255, 0.3);
409
+
z-index: 20;
410
+
}
411
+
412
+
.footer a {
413
+
color: rgba(255, 255, 255, 0.5);
414
+
text-decoration: none;
415
+
transition: color 0.2s;
416
+
}
417
+
418
+
.footer a:hover {
419
+
color: rgba(255, 255, 255, 0.8);
420
+
}
-43
src/main.rs
-43
src/main.rs
···
1
-
use actix_session::{SessionMiddleware, config::PersistentSession, storage::CookieSessionStore};
2
-
use actix_web::{App, HttpServer, cookie::{Key, time::Duration}, middleware, web};
3
-
4
-
mod oauth;
5
-
mod routes;
6
-
mod templates;
7
-
8
-
#[actix_web::main]
9
-
async fn main() -> std::io::Result<()> {
10
-
env_logger::init();
11
-
12
-
let client = oauth::create_oauth_client();
13
-
14
-
println!("starting server at http://localhost:8080");
15
-
16
-
HttpServer::new(move || {
17
-
App::new()
18
-
.wrap(middleware::Logger::default())
19
-
.wrap(
20
-
SessionMiddleware::builder(
21
-
CookieSessionStore::default(),
22
-
Key::from(&[0; 64]),
23
-
)
24
-
.cookie_secure(false)
25
-
.session_lifecycle(
26
-
PersistentSession::default()
27
-
.session_ttl(Duration::days(30))
28
-
)
29
-
.build(),
30
-
)
31
-
.app_data(web::Data::new(client.clone()))
32
-
.service(routes::index)
33
-
.service(routes::login)
34
-
.service(routes::callback)
35
-
.service(routes::client_metadata)
36
-
.service(routes::logout)
37
-
.service(routes::restore_session)
38
-
.service(routes::favicon)
39
-
})
40
-
.bind(("0.0.0.0", 8080))?
41
-
.run()
42
-
.await
43
-
}
-100
src/oauth.rs
-100
src/oauth.rs
···
1
-
use atrium_identity::{
2
-
did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL},
3
-
handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver},
4
-
};
5
-
use atrium_oauth::{
6
-
AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, DefaultHttpClient,
7
-
GrantType, KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope,
8
-
store::{session::MemorySessionStore, state::MemoryStateStore},
9
-
};
10
-
use hickory_resolver::{TokioAsyncResolver, config::{ResolverConfig, ResolverOpts}};
11
-
use std::sync::Arc;
12
-
13
-
#[derive(Clone)]
14
-
pub struct HickoryDnsResolver(Arc<TokioAsyncResolver>);
15
-
16
-
impl DnsTxtResolver for HickoryDnsResolver {
17
-
async fn resolve(
18
-
&self,
19
-
domain: &str,
20
-
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
21
-
Ok(self
22
-
.0
23
-
.txt_lookup(domain)
24
-
.await?
25
-
.iter()
26
-
.map(|txt| txt.to_string())
27
-
.collect())
28
-
}
29
-
}
30
-
31
-
pub type OAuthClientType = Arc<
32
-
OAuthClient<
33
-
MemoryStateStore,
34
-
MemorySessionStore,
35
-
CommonDidResolver<DefaultHttpClient>,
36
-
AtprotoHandleResolver<HickoryDnsResolver, DefaultHttpClient>,
37
-
>,
38
-
>;
39
-
40
-
pub fn create_oauth_client() -> OAuthClientType {
41
-
let http_client = Arc::new(DefaultHttpClient::default());
42
-
let dns_resolver = HickoryDnsResolver(Arc::new(
43
-
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()),
44
-
));
45
-
46
-
let redirect_uri = std::env::var("OAUTH_REDIRECT_URI")
47
-
.unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string());
48
-
49
-
let is_production = redirect_uri.starts_with("https://");
50
-
51
-
let resolver = OAuthResolverConfig {
52
-
did_resolver: CommonDidResolver::new(CommonDidResolverConfig {
53
-
plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
54
-
http_client: http_client.clone(),
55
-
}),
56
-
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
57
-
dns_txt_resolver: dns_resolver,
58
-
http_client: http_client.clone(),
59
-
}),
60
-
authorization_server_metadata: Default::default(),
61
-
protected_resource_metadata: Default::default(),
62
-
};
63
-
64
-
if is_production {
65
-
let base_url = redirect_uri.trim_end_matches("/oauth/callback");
66
-
Arc::new(
67
-
OAuthClient::new(OAuthClientConfig {
68
-
client_metadata: AtprotoClientMetadata {
69
-
client_id: format!("{}/oauth-client-metadata.json", base_url),
70
-
client_uri: Some(base_url.to_string()),
71
-
redirect_uris: vec![redirect_uri],
72
-
token_endpoint_auth_method: AuthMethod::None,
73
-
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
74
-
scopes: vec![Scope::Known(KnownScope::Atproto)],
75
-
jwks_uri: None,
76
-
token_endpoint_auth_signing_alg: None,
77
-
},
78
-
keys: None,
79
-
resolver,
80
-
state_store: MemoryStateStore::default(),
81
-
session_store: MemorySessionStore::default(),
82
-
})
83
-
.expect("failed to create oauth client"),
84
-
)
85
-
} else {
86
-
Arc::new(
87
-
OAuthClient::new(OAuthClientConfig {
88
-
client_metadata: AtprotoLocalhostClientMetadata {
89
-
redirect_uris: Some(vec![redirect_uri]),
90
-
scopes: Some(vec![Scope::Known(KnownScope::Atproto)]),
91
-
},
92
-
keys: None,
93
-
resolver,
94
-
state_store: MemoryStateStore::default(),
95
-
session_store: MemorySessionStore::default(),
96
-
})
97
-
.expect("failed to create oauth client"),
98
-
)
99
-
}
100
-
}
-153
src/routes.rs
-153
src/routes.rs
···
1
-
use actix_session::Session;
2
-
use actix_web::{get, post, web, HttpResponse, Responder};
3
-
use atrium_oauth::{AuthorizeOptions, CallbackParams, KnownScope, Scope};
4
-
use serde::Deserialize;
5
-
6
-
use crate::oauth::OAuthClientType;
7
-
use crate::templates;
8
-
9
-
const FAVICON_SVG: &str = include_str!("../static/favicon.svg");
10
-
11
-
#[derive(Deserialize)]
12
-
pub struct LoginForm {
13
-
handle: String,
14
-
}
15
-
16
-
#[derive(Deserialize)]
17
-
pub struct OAuthParams {
18
-
state: Option<String>,
19
-
iss: Option<String>,
20
-
code: Option<String>,
21
-
error: Option<String>,
22
-
}
23
-
24
-
#[get("/")]
25
-
pub async fn index(session: Session) -> impl Responder {
26
-
let did: Option<String> = session.get("did").unwrap_or(None);
27
-
28
-
match did {
29
-
Some(did) => HttpResponse::Ok()
30
-
.content_type("text/html")
31
-
.body(templates::app_page(&did)),
32
-
None => HttpResponse::Ok()
33
-
.content_type("text/html")
34
-
.body(templates::login_page()),
35
-
}
36
-
}
37
-
38
-
#[post("/login")]
39
-
pub async fn login(
40
-
form: web::Form<LoginForm>,
41
-
client: web::Data<OAuthClientType>,
42
-
) -> HttpResponse {
43
-
let handle = match atrium_api::types::string::Handle::new(form.handle.clone()) {
44
-
Ok(h) => h,
45
-
Err(_) => return HttpResponse::BadRequest().body("invalid handle"),
46
-
};
47
-
48
-
match client
49
-
.authorize(
50
-
&handle,
51
-
AuthorizeOptions {
52
-
scopes: vec![Scope::Known(KnownScope::Atproto)],
53
-
..Default::default()
54
-
},
55
-
)
56
-
.await
57
-
{
58
-
Ok(url) => HttpResponse::SeeOther()
59
-
.append_header(("Location", url))
60
-
.finish(),
61
-
Err(_) => HttpResponse::InternalServerError().body("oauth error"),
62
-
}
63
-
}
64
-
65
-
#[get("/oauth/callback")]
66
-
pub async fn callback(
67
-
params: web::Query<OAuthParams>,
68
-
client: web::Data<OAuthClientType>,
69
-
session: Session,
70
-
) -> HttpResponse {
71
-
if let Some(error) = ¶ms.error {
72
-
return HttpResponse::BadRequest().body(format!("oauth error: {}", error));
73
-
}
74
-
75
-
let code = match ¶ms.code {
76
-
Some(c) => c.clone(),
77
-
None => return HttpResponse::BadRequest().body("missing code"),
78
-
};
79
-
80
-
let callback_params = CallbackParams {
81
-
code,
82
-
state: params.state.clone(),
83
-
iss: params.iss.clone(),
84
-
};
85
-
86
-
match client.callback(callback_params).await {
87
-
Ok((bsky_session, _)) => {
88
-
let agent = atrium_api::agent::Agent::new(bsky_session);
89
-
if let Some(did) = agent.did().await {
90
-
session.insert("did", did.to_string()).unwrap();
91
-
HttpResponse::SeeOther()
92
-
.append_header(("Location", "/"))
93
-
.finish()
94
-
} else {
95
-
HttpResponse::InternalServerError().body("no did")
96
-
}
97
-
}
98
-
Err(e) => HttpResponse::InternalServerError().body(format!("callback error: {}", e)),
99
-
}
100
-
}
101
-
102
-
#[get("/oauth-client-metadata.json")]
103
-
pub async fn client_metadata() -> HttpResponse {
104
-
let base_url = std::env::var("OAUTH_REDIRECT_URI")
105
-
.unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string())
106
-
.trim_end_matches("/oauth/callback")
107
-
.to_string();
108
-
109
-
let metadata = serde_json::json!({
110
-
"client_id": format!("{}/oauth-client-metadata.json", base_url),
111
-
"client_name": "@me",
112
-
"client_uri": base_url.clone(),
113
-
"redirect_uris": [format!("{}/oauth/callback", base_url)],
114
-
"scope": "atproto",
115
-
"grant_types": ["authorization_code", "refresh_token"],
116
-
"response_types": ["code"],
117
-
"token_endpoint_auth_method": "none",
118
-
"dpop_bound_access_tokens": true
119
-
});
120
-
121
-
HttpResponse::Ok()
122
-
.content_type("application/json")
123
-
.body(metadata.to_string())
124
-
}
125
-
126
-
#[get("/logout")]
127
-
pub async fn logout(session: Session) -> HttpResponse {
128
-
session.purge();
129
-
HttpResponse::SeeOther()
130
-
.append_header(("Location", "/"))
131
-
.finish()
132
-
}
133
-
134
-
#[derive(Deserialize)]
135
-
pub struct RestoreSession {
136
-
did: String,
137
-
}
138
-
139
-
#[post("/api/restore-session")]
140
-
pub async fn restore_session(
141
-
data: web::Json<RestoreSession>,
142
-
session: Session,
143
-
) -> HttpResponse {
144
-
session.insert("did", &data.did).unwrap();
145
-
HttpResponse::Ok().finish()
146
-
}
147
-
148
-
#[get("/favicon.svg")]
149
-
pub async fn favicon() -> HttpResponse {
150
-
HttpResponse::Ok()
151
-
.content_type("image/svg+xml")
152
-
.body(FAVICON_SVG)
153
-
}
-1053
src/templates.rs
-1053
src/templates.rs
···
1
-
pub fn login_page() -> &'static str {
2
-
r#"
3
-
<!DOCTYPE html>
4
-
<html>
5
-
<head>
6
-
<meta charset="UTF-8">
7
-
<title>@me - login</title>
8
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
9
-
<style>
10
-
* { margin: 0; padding: 0; box-sizing: border-box; }
11
-
body { font-family: 'Monaco', 'Courier New', monospace; display: flex; align-items: center; justify-content: center; height: 100vh; background: #000; color: #0f0; }
12
-
.container { text-align: center; }
13
-
h1 { font-size: 2rem; margin-bottom: 2rem; }
14
-
input { font-family: 'Monaco', 'Courier New', monospace; font-size: 0.9rem; padding: 0.5rem; margin: 0.5rem; background: #000; border: 1px solid #0f0; color: #0f0; }
15
-
button { font-family: 'Monaco', 'Courier New', monospace; font-size: 0.9rem; padding: 0.5rem 1rem; cursor: pointer; background: #000; border: 1px solid #0f0; color: #0f0; }
16
-
button:hover { background: #0f0; color: #000; }
17
-
.hidden { display: none; }
18
-
.loading { color: #0f0; opacity: 0.5; }
19
-
</style>
20
-
</head>
21
-
<body>
22
-
<div class="container">
23
-
<div id="restoring" class="loading hidden">restoring session...</div>
24
-
<form id="loginForm" method="post" action="/login">
25
-
<h1>@me</h1>
26
-
<input type="text" name="handle" placeholder="handle.bsky.social" required autofocus>
27
-
<button type="submit">login</button>
28
-
</form>
29
-
</div>
30
-
<script>
31
-
const savedDid = localStorage.getItem('atme_did');
32
-
if (savedDid) {
33
-
document.getElementById('loginForm').classList.add('hidden');
34
-
document.getElementById('restoring').classList.remove('hidden');
35
-
36
-
fetch('/api/restore-session', {
37
-
method: 'POST',
38
-
headers: { 'Content-Type': 'application/json' },
39
-
body: JSON.stringify({ did: savedDid })
40
-
}).then(r => {
41
-
if (r.ok) {
42
-
window.location.href = '/';
43
-
} else {
44
-
localStorage.removeItem('atme_did');
45
-
document.getElementById('loginForm').classList.remove('hidden');
46
-
document.getElementById('restoring').classList.add('hidden');
47
-
}
48
-
}).catch(() => {
49
-
localStorage.removeItem('atme_did');
50
-
document.getElementById('loginForm').classList.remove('hidden');
51
-
document.getElementById('restoring').classList.add('hidden');
52
-
});
53
-
}
54
-
</script>
55
-
</body>
56
-
</html>
57
-
"#
58
-
}
59
-
60
-
pub fn app_page(did: &str) -> String {
61
-
format!(r#"
62
-
<!DOCTYPE html>
63
-
<html>
64
-
<head>
65
-
<meta charset="UTF-8">
66
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
67
-
<title>@me</title>
68
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
69
-
<style>
70
-
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
71
-
72
-
:root {{
73
-
--bg: #f5f1e8;
74
-
--text: #4a4238;
75
-
--text-light: #8a7a6a;
76
-
--text-lighter: #6b5d4f;
77
-
--border: #c9bfa8;
78
-
--surface: #e5dbc8;
79
-
--surface-hover: #d9cdb5;
80
-
}}
81
-
82
-
@media (prefers-color-scheme: dark) {{
83
-
:root {{
84
-
--bg: #1a1a1a;
85
-
--text: #e5e5e5;
86
-
--text-light: #a0a0a0;
87
-
--text-lighter: #c0c0c0;
88
-
--border: #404040;
89
-
--surface: #2a2a2a;
90
-
--surface-hover: #353535;
91
-
}}
92
-
}}
93
-
94
-
body {{
95
-
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
96
-
height: 100vh;
97
-
background: var(--bg);
98
-
color: var(--text);
99
-
overflow: hidden;
100
-
position: relative;
101
-
-webkit-font-smoothing: antialiased;
102
-
-moz-osx-font-smoothing: grayscale;
103
-
}}
104
-
105
-
.canvas {{
106
-
width: 100%;
107
-
height: 100%;
108
-
position: relative;
109
-
display: flex;
110
-
align-items: center;
111
-
justify-content: center;
112
-
}}
113
-
114
-
.logout {{
115
-
position: fixed;
116
-
top: 1.5rem;
117
-
right: 1.5rem;
118
-
font-size: 0.7rem;
119
-
color: var(--text-light);
120
-
text-decoration: none;
121
-
border: 1px solid var(--border);
122
-
padding: 0.4rem 0.8rem;
123
-
transition: all 0.2s ease;
124
-
z-index: 100;
125
-
-webkit-tap-highlight-color: transparent;
126
-
cursor: pointer;
127
-
border-radius: 2px;
128
-
}}
129
-
130
-
.logout:hover, .logout:active {{
131
-
background: var(--surface);
132
-
color: var(--text);
133
-
border-color: var(--text-light);
134
-
}}
135
-
136
-
@media (max-width: 768px) {{
137
-
.logout {{
138
-
padding: 0.6rem 1rem;
139
-
font-size: 0.75rem;
140
-
top: 1rem;
141
-
right: 1rem;
142
-
}}
143
-
}}
144
-
145
-
.info {{
146
-
position: fixed;
147
-
top: 1.5rem;
148
-
left: 1.5rem;
149
-
width: 32px;
150
-
height: 32px;
151
-
border-radius: 50%;
152
-
border: 1px solid var(--border);
153
-
display: flex;
154
-
align-items: center;
155
-
justify-content: center;
156
-
font-size: 0.75rem;
157
-
color: var(--text-light);
158
-
cursor: pointer;
159
-
transition: all 0.2s ease;
160
-
z-index: 100;
161
-
-webkit-tap-highlight-color: transparent;
162
-
}}
163
-
164
-
.info:hover, .info:active {{
165
-
background: var(--surface);
166
-
color: var(--text);
167
-
border-color: var(--text-light);
168
-
}}
169
-
170
-
@media (max-width: 768px) {{
171
-
.info {{
172
-
width: 40px;
173
-
height: 40px;
174
-
font-size: 0.85rem;
175
-
top: 1rem;
176
-
left: 1rem;
177
-
}}
178
-
}}
179
-
180
-
.info-modal {{
181
-
position: fixed;
182
-
top: 50%;
183
-
left: 50%;
184
-
transform: translate(-50%, -50%);
185
-
background: var(--surface);
186
-
border: 2px solid var(--border);
187
-
padding: 2rem;
188
-
max-width: 500px;
189
-
width: 90%;
190
-
z-index: 2000;
191
-
display: none;
192
-
border-radius: 4px;
193
-
}}
194
-
195
-
@media (max-width: 768px) {{
196
-
.info-modal {{
197
-
padding: 1.5rem;
198
-
width: 95%;
199
-
}}
200
-
201
-
.info-modal h2 {{
202
-
font-size: 0.9rem;
203
-
}}
204
-
205
-
.info-modal p {{
206
-
font-size: 0.7rem;
207
-
}}
208
-
}}
209
-
210
-
.info-modal.visible {{
211
-
display: block;
212
-
}}
213
-
214
-
.info-modal h2 {{
215
-
margin-bottom: 1rem;
216
-
font-size: 1rem;
217
-
color: var(--text);
218
-
}}
219
-
220
-
.info-modal p {{
221
-
margin-bottom: 0.75rem;
222
-
font-size: 0.75rem;
223
-
line-height: 1.5;
224
-
color: var(--text-lighter);
225
-
}}
226
-
227
-
.info-modal button {{
228
-
margin-top: 1rem;
229
-
padding: 0.5rem 1rem;
230
-
background: var(--bg);
231
-
border: 1px solid var(--border);
232
-
color: var(--text);
233
-
font-family: inherit;
234
-
font-size: 0.7rem;
235
-
cursor: pointer;
236
-
transition: all 0.2s ease;
237
-
-webkit-tap-highlight-color: transparent;
238
-
border-radius: 2px;
239
-
}}
240
-
241
-
.info-modal button:hover, .info-modal button:active {{
242
-
background: var(--surface-hover);
243
-
border-color: var(--text-light);
244
-
}}
245
-
246
-
@media (max-width: 768px) {{
247
-
.info-modal button {{
248
-
padding: 0.65rem 1.2rem;
249
-
font-size: 0.75rem;
250
-
}}
251
-
}}
252
-
253
-
.overlay {{
254
-
position: fixed;
255
-
top: 0;
256
-
left: 0;
257
-
right: 0;
258
-
bottom: 0;
259
-
background: rgba(0, 0, 0, 0.5);
260
-
z-index: 1999;
261
-
display: none;
262
-
}}
263
-
264
-
.overlay.visible {{
265
-
display: block;
266
-
}}
267
-
268
-
.identity {{
269
-
position: absolute;
270
-
background: var(--surface);
271
-
border: 2px solid var(--text-light);
272
-
border-radius: 50%;
273
-
width: 120px;
274
-
height: 120px;
275
-
display: flex;
276
-
flex-direction: column;
277
-
align-items: center;
278
-
justify-content: center;
279
-
gap: 0.3rem;
280
-
z-index: 10;
281
-
cursor: pointer;
282
-
transition: all 0.2s ease;
283
-
-webkit-tap-highlight-color: transparent;
284
-
}}
285
-
286
-
.identity:hover, .identity:active {{
287
-
transform: scale(1.05);
288
-
border-color: var(--text);
289
-
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
290
-
}}
291
-
292
-
@media (max-width: 768px) {{
293
-
.identity {{
294
-
width: 100px;
295
-
height: 100px;
296
-
}}
297
-
}}
298
-
299
-
.identity-label {{
300
-
font-size: 1.2rem;
301
-
color: var(--text);
302
-
font-weight: 600;
303
-
line-height: 1;
304
-
}}
305
-
306
-
.identity-value {{
307
-
font-size: 0.7rem;
308
-
color: var(--text-lighter);
309
-
text-align: center;
310
-
word-break: break-word;
311
-
max-width: 100px;
312
-
font-weight: 400;
313
-
}}
314
-
315
-
@media (max-width: 768px) {{
316
-
.identity-label {{
317
-
font-size: 1.1rem;
318
-
}}
319
-
320
-
.identity-value {{
321
-
font-size: 0.65rem;
322
-
}}
323
-
}}
324
-
325
-
.identity-hint {{
326
-
font-size: 0.4rem;
327
-
color: var(--text-lighter);
328
-
margin-top: 0.2rem;
329
-
letter-spacing: 0.05em;
330
-
}}
331
-
332
-
.app-view {{
333
-
position: absolute;
334
-
display: flex;
335
-
flex-direction: column;
336
-
align-items: center;
337
-
gap: 0.4rem;
338
-
cursor: pointer;
339
-
transition: all 0.2s ease;
340
-
opacity: 0.7;
341
-
}}
342
-
343
-
.app-view:hover {{
344
-
opacity: 1;
345
-
transform: scale(1.1);
346
-
z-index: 100;
347
-
}}
348
-
349
-
.app-circle {{
350
-
background: var(--surface-hover);
351
-
border: 1px solid var(--border);
352
-
border-radius: 50%;
353
-
width: 60px;
354
-
height: 60px;
355
-
display: flex;
356
-
align-items: center;
357
-
justify-content: center;
358
-
transition: all 0.2s ease;
359
-
overflow: hidden;
360
-
}}
361
-
362
-
.app-logo {{
363
-
width: 100%;
364
-
height: 100%;
365
-
object-fit: cover;
366
-
}}
367
-
368
-
.app-view:hover .app-circle {{
369
-
background: var(--surface);
370
-
border-color: var(--text-light);
371
-
}}
372
-
373
-
.app-name {{
374
-
font-size: 0.65rem;
375
-
color: var(--text);
376
-
text-align: center;
377
-
max-width: 100px;
378
-
}}
379
-
380
-
.detail-panel {{
381
-
position: fixed;
382
-
top: 0;
383
-
left: 0;
384
-
bottom: 0;
385
-
width: 320px;
386
-
background: var(--surface);
387
-
border-right: 2px solid var(--border);
388
-
padding: 2.5rem 2rem;
389
-
overflow-y: auto;
390
-
opacity: 0;
391
-
transform: translateX(-100%);
392
-
transition: all 0.25s ease;
393
-
z-index: 1000;
394
-
}}
395
-
396
-
.detail-panel.visible {{
397
-
opacity: 1;
398
-
transform: translateX(0);
399
-
}}
400
-
401
-
@media (max-width: 768px) {{
402
-
.detail-panel {{
403
-
width: 100%;
404
-
padding: 4rem 1.5rem 2rem;
405
-
border-right: none;
406
-
border-bottom: 2px solid var(--border);
407
-
}}
408
-
}}
409
-
410
-
.detail-panel h3 {{
411
-
margin-bottom: 0.75rem;
412
-
font-size: 0.85rem;
413
-
color: var(--text);
414
-
}}
415
-
416
-
.detail-panel .subtitle {{
417
-
font-size: 0.7rem;
418
-
color: var(--text-light);
419
-
margin-bottom: 1.5rem;
420
-
line-height: 1.4;
421
-
}}
422
-
423
-
.detail-close {{
424
-
position: absolute;
425
-
top: 1.5rem;
426
-
right: 1.5rem;
427
-
width: 32px;
428
-
height: 32px;
429
-
border: 1px solid var(--border);
430
-
background: var(--bg);
431
-
color: var(--text-light);
432
-
cursor: pointer;
433
-
display: flex;
434
-
align-items: center;
435
-
justify-content: center;
436
-
font-size: 1.2rem;
437
-
line-height: 1;
438
-
transition: all 0.2s ease;
439
-
border-radius: 2px;
440
-
-webkit-tap-highlight-color: transparent;
441
-
}}
442
-
443
-
.detail-close:hover, .detail-close:active {{
444
-
background: var(--surface-hover);
445
-
border-color: var(--text-light);
446
-
color: var(--text);
447
-
}}
448
-
449
-
@media (max-width: 768px) {{
450
-
.detail-close {{
451
-
top: 1rem;
452
-
right: 1rem;
453
-
width: 40px;
454
-
height: 40px;
455
-
font-size: 1.4rem;
456
-
}}
457
-
}}
458
-
459
-
.tree-item {{
460
-
padding: 0.65rem 0.75rem;
461
-
font-size: 0.75rem;
462
-
color: var(--text-lighter);
463
-
background: var(--bg);
464
-
border: 1px solid var(--border);
465
-
border-radius: 2px;
466
-
margin-bottom: 0.5rem;
467
-
transition: all 0.15s ease;
468
-
cursor: pointer;
469
-
-webkit-tap-highlight-color: transparent;
470
-
}}
471
-
472
-
.tree-item:hover, .tree-item:active {{
473
-
background: var(--surface-hover);
474
-
border-color: var(--text-light);
475
-
}}
476
-
477
-
@media (max-width: 768px) {{
478
-
.tree-item {{
479
-
padding: 0.8rem 0.9rem;
480
-
font-size: 0.8rem;
481
-
}}
482
-
}}
483
-
484
-
.tree-item:last-child {{
485
-
margin-bottom: 0;
486
-
}}
487
-
488
-
.tree-item-header {{
489
-
display: flex;
490
-
justify-content: space-between;
491
-
align-items: center;
492
-
}}
493
-
494
-
.tree-item-count {{
495
-
font-size: 0.65rem;
496
-
color: var(--text-light);
497
-
}}
498
-
499
-
.record-list {{
500
-
margin-top: 0.5rem;
501
-
padding-top: 0.5rem;
502
-
border-top: 1px solid var(--border);
503
-
}}
504
-
505
-
.record {{
506
-
margin-bottom: 0.5rem;
507
-
background: var(--bg);
508
-
border: 1px solid var(--border);
509
-
border-radius: 4px;
510
-
font-size: 0.65rem;
511
-
color: var(--text-light);
512
-
transition: all 0.15s ease;
513
-
overflow: hidden;
514
-
}}
515
-
516
-
.record:hover {{
517
-
border-color: var(--text-light);
518
-
background: var(--surface);
519
-
}}
520
-
521
-
.record:last-child {{
522
-
margin-bottom: 0;
523
-
}}
524
-
525
-
.record-header {{
526
-
display: flex;
527
-
justify-content: space-between;
528
-
align-items: center;
529
-
padding: 0.5rem 0.6rem;
530
-
background: var(--surface);
531
-
border-bottom: 1px solid var(--border);
532
-
}}
533
-
534
-
.record-label {{
535
-
font-size: 0.6rem;
536
-
color: var(--text-lighter);
537
-
font-weight: 500;
538
-
}}
539
-
540
-
.copy-btn {{
541
-
background: var(--bg);
542
-
border: 1px solid var(--border);
543
-
color: var(--text-light);
544
-
font-family: inherit;
545
-
font-size: 0.55rem;
546
-
padding: 0.2rem 0.5rem;
547
-
cursor: pointer;
548
-
transition: all 0.15s ease;
549
-
border-radius: 2px;
550
-
-webkit-tap-highlight-color: transparent;
551
-
}}
552
-
553
-
.copy-btn:hover, .copy-btn:active {{
554
-
background: var(--surface-hover);
555
-
border-color: var(--text-light);
556
-
color: var(--text);
557
-
}}
558
-
559
-
.copy-btn.copied {{
560
-
color: var(--text);
561
-
border-color: var(--text);
562
-
}}
563
-
564
-
.record-content {{
565
-
padding: 0.6rem;
566
-
}}
567
-
568
-
.record-content pre {{
569
-
margin: 0;
570
-
white-space: pre-wrap;
571
-
word-break: break-word;
572
-
line-height: 1.5;
573
-
font-size: 0.625rem;
574
-
}}
575
-
576
-
.load-more {{
577
-
margin-top: 0.5rem;
578
-
padding: 0.4rem 0.6rem;
579
-
background: var(--bg);
580
-
border: 1px solid var(--border);
581
-
color: var(--text);
582
-
font-family: inherit;
583
-
font-size: 0.65rem;
584
-
cursor: pointer;
585
-
width: 100%;
586
-
transition: all 0.15s ease;
587
-
-webkit-tap-highlight-color: transparent;
588
-
border-radius: 2px;
589
-
}}
590
-
591
-
.load-more:hover, .load-more:active {{
592
-
background: var(--surface-hover);
593
-
border-color: var(--text-light);
594
-
}}
595
-
596
-
@media (max-width: 768px) {{
597
-
.load-more {{
598
-
padding: 0.6rem 0.8rem;
599
-
font-size: 0.7rem;
600
-
}}
601
-
}}
602
-
603
-
.footer {{
604
-
position: fixed;
605
-
bottom: 1rem;
606
-
left: 50%;
607
-
transform: translateX(-50%);
608
-
font-size: 0.65rem;
609
-
color: var(--text-light);
610
-
z-index: 100;
611
-
}}
612
-
613
-
.footer a {{
614
-
color: var(--text-light);
615
-
text-decoration: none;
616
-
border-bottom: 1px solid transparent;
617
-
transition: border-color 0.2s ease;
618
-
}}
619
-
620
-
.footer a:hover {{
621
-
border-bottom-color: var(--text-light);
622
-
}}
623
-
624
-
.loading {{ color: var(--text-light); font-size: 0.75rem; }}
625
-
</style>
626
-
</head>
627
-
<body>
628
-
<div class="info" id="infoBtn">i</div>
629
-
<a href="javascript:void(0)" id="logoutBtn" class="logout">logout</a>
630
-
631
-
<div class="overlay" id="overlay"></div>
632
-
<div class="info-modal" id="infoModal">
633
-
<h2>@me - your at protocol identity</h2>
634
-
<p>in decentralized social networks, you own your identity and your data lives in your personal data server (pds).</p>
635
-
<p>third-party applications create records in your repository using different lexicons (data schemas). for example, bluesky creates posts, white wind stores blog entries, tangled.org hosts code repositories, and frontpage aggregates links - all in the same place.</p>
636
-
<p>this visualization shows your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what types of records it stores, then click a record type to see the actual data.</p>
637
-
<button id="closeInfo">got it</button>
638
-
</div>
639
-
640
-
<div class="canvas">
641
-
<div class="identity">
642
-
<div class="identity-label">@</div>
643
-
<div class="identity-value" id="handle">loading...</div>
644
-
<div class="identity-hint">tap for details</div>
645
-
</div>
646
-
<div id="field" class="loading">loading...</div>
647
-
</div>
648
-
<div id="detail" class="detail-panel"></div>
649
-
650
-
<div class="footer">
651
-
<a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer">view source</a>
652
-
</div>
653
-
<script>
654
-
const did = '{}';
655
-
localStorage.setItem('atme_did', did);
656
-
657
-
let globalPds = null;
658
-
let globalHandle = null;
659
-
660
-
// Try to fetch app avatar from their bsky profile
661
-
async function fetchAppAvatar(namespace) {{
662
-
try {{
663
-
// Reverse namespace to get domain (e.g., io.zzstoatzz -> zzstoatzz.io)
664
-
const reversed = namespace.split('.').reverse().join('.');
665
-
// Try reversed domain, then reversed.bsky.social
666
-
const handles = [reversed, `${{reversed}}.bsky.social`];
667
-
668
-
for (const handle of handles) {{
669
-
try {{
670
-
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${{handle}}`);
671
-
if (!didRes.ok) continue;
672
-
673
-
const {{ did }} = await didRes.json();
674
-
const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${{did}}`);
675
-
if (!profileRes.ok) continue;
676
-
677
-
const profile = await profileRes.json();
678
-
if (profile.avatar) {{
679
-
return profile.avatar;
680
-
}}
681
-
}} catch (e) {{
682
-
continue;
683
-
}}
684
-
}}
685
-
}} catch (e) {{
686
-
console.log('Could not fetch avatar for', namespace);
687
-
}}
688
-
return null;
689
-
}}
690
-
691
-
// Logout handler
692
-
document.getElementById('logoutBtn').addEventListener('click', (e) => {{
693
-
e.preventDefault();
694
-
localStorage.removeItem('atme_did');
695
-
window.location.href = '/logout';
696
-
}});
697
-
698
-
// Info modal handlers
699
-
document.getElementById('infoBtn').addEventListener('click', () => {{
700
-
document.getElementById('infoModal').classList.add('visible');
701
-
document.getElementById('overlay').classList.add('visible');
702
-
}});
703
-
704
-
document.getElementById('closeInfo').addEventListener('click', () => {{
705
-
document.getElementById('infoModal').classList.remove('visible');
706
-
document.getElementById('overlay').classList.remove('visible');
707
-
}});
708
-
709
-
document.getElementById('overlay').addEventListener('click', () => {{
710
-
document.getElementById('infoModal').classList.remove('visible');
711
-
document.getElementById('overlay').classList.remove('visible');
712
-
const detail = document.getElementById('detail');
713
-
detail.classList.remove('visible');
714
-
}});
715
-
716
-
// First resolve DID to get PDS endpoint and handle
717
-
fetch('https://plc.directory/' + did)
718
-
.then(r => r.json())
719
-
.then(didDoc => {{
720
-
const pds = didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint;
721
-
const handle = didDoc.alsoKnownAs?.[0]?.replace('at://', '') || did;
722
-
723
-
globalPds = pds;
724
-
globalHandle = handle;
725
-
726
-
// Update identity display with handle
727
-
document.getElementById('handle').textContent = handle;
728
-
729
-
// Add identity click handler to show PDS info
730
-
document.querySelector('.identity').addEventListener('click', () => {{
731
-
const detail = document.getElementById('detail');
732
-
const pdsHost = pds.replace('https://', '').replace('http://', '');
733
-
detail.innerHTML = `
734
-
<button class="detail-close" id="detailClose">ร</button>
735
-
<h3>your identity</h3>
736
-
<div class="subtitle">decentralized identifier & storage</div>
737
-
<div class="tree-item">
738
-
<div class="tree-item-header">
739
-
<span style="color: var(--text-light);">did</span>
740
-
<span style="font-size: 0.6rem; color: var(--text);">${{did}}</span>
741
-
</div>
742
-
</div>
743
-
<div class="tree-item">
744
-
<div class="tree-item-header">
745
-
<span style="color: var(--text-light);">handle</span>
746
-
<span style="font-size: 0.6rem; color: var(--text);">@${{handle}}</span>
747
-
</div>
748
-
</div>
749
-
<div class="tree-item">
750
-
<div class="tree-item-header">
751
-
<span style="color: var(--text-light);">personal data server</span>
752
-
<span style="font-size: 0.6rem; color: var(--text);">${{pds}}</span>
753
-
</div>
754
-
</div>
755
-
<div style="margin-top: 1rem; padding: 0.6rem; background: var(--bg); border-radius: 4px; font-size: 0.65rem; line-height: 1.5; color: var(--text-lighter);">
756
-
your data lives at <strong style="color: var(--text);">${{pdsHost}}</strong>. apps like bluesky write to and read from this server. you control @<strong style="color: var(--text);">${{handle}}</strong> and can move it to a different server anytime.
757
-
</div>
758
-
`;
759
-
detail.classList.add('visible');
760
-
761
-
// Add close button handler
762
-
document.getElementById('detailClose').addEventListener('click', (e) => {{
763
-
e.stopPropagation();
764
-
detail.classList.remove('visible');
765
-
}});
766
-
}});
767
-
768
-
// Get all collections from PDS
769
-
return fetch(`${{pds}}/xrpc/com.atproto.repo.describeRepo?repo=${{did}}`);
770
-
}})
771
-
.then(r => r.json())
772
-
.then(repo => {{
773
-
const collections = repo.collections || [];
774
-
775
-
// Group by app namespace (first two parts of lexicon)
776
-
const apps = {{}};
777
-
collections.forEach(collection => {{
778
-
const parts = collection.split('.');
779
-
if (parts.length >= 2) {{
780
-
const namespace = `${{parts[0]}}.${{parts[1]}}`;
781
-
if (!apps[namespace]) apps[namespace] = [];
782
-
apps[namespace].push(collection);
783
-
}}
784
-
}});
785
-
786
-
const field = document.getElementById('field');
787
-
field.innerHTML = '';
788
-
field.classList.remove('loading');
789
-
790
-
const appNames = Object.keys(apps).sort();
791
-
const radius = 240;
792
-
const centerX = window.innerWidth / 2;
793
-
const centerY = window.innerHeight / 2;
794
-
795
-
appNames.forEach((namespace, i) => {{
796
-
const angle = (i / appNames.length) * 2 * Math.PI - Math.PI / 2; // Start from top
797
-
const x = centerX + radius * Math.cos(angle) - 25;
798
-
const y = centerY + radius * Math.sin(angle) - 30;
799
-
800
-
const div = document.createElement('div');
801
-
div.className = 'app-view';
802
-
div.style.left = `${{x}}px`;
803
-
div.style.top = `${{y}}px`;
804
-
805
-
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
806
-
807
-
div.innerHTML = `
808
-
<div class="app-circle" data-namespace="${{namespace}}">${{firstLetter}}</div>
809
-
<div class="app-name">${{namespace}}</div>
810
-
`;
811
-
812
-
// Try to fetch and display avatar
813
-
fetchAppAvatar(namespace).then(avatarUrl => {{
814
-
if (avatarUrl) {{
815
-
const circle = div.querySelector('.app-circle');
816
-
circle.innerHTML = `<img src="${{avatarUrl}}" class="app-logo" alt="${{namespace}}" />`;
817
-
}}
818
-
}});
819
-
820
-
div.addEventListener('click', () => {{
821
-
const detail = document.getElementById('detail');
822
-
const collections = apps[namespace];
823
-
824
-
let html = `
825
-
<button class="detail-close" id="detailClose">ร</button>
826
-
<h3>${{namespace}}</h3>
827
-
<div class="subtitle">records stored in your pds:</div>
828
-
`;
829
-
830
-
if (collections && collections.length > 0) {{
831
-
// Group collections by sub-namespace (third segment)
832
-
const grouped = {{}};
833
-
collections.forEach(lexicon => {{
834
-
const parts = lexicon.split('.');
835
-
const subNamespace = parts.slice(2).join('.');
836
-
const firstPart = parts[2] || lexicon;
837
-
838
-
if (!grouped[firstPart]) grouped[firstPart] = [];
839
-
grouped[firstPart].push({{ lexicon, subNamespace }});
840
-
}});
841
-
842
-
// Sort and display grouped items
843
-
Object.keys(grouped).sort().forEach(group => {{
844
-
const items = grouped[group];
845
-
846
-
if (items.length === 1 && items[0].subNamespace === group) {{
847
-
// Single item with no further nesting
848
-
html += `
849
-
<div class="tree-item" data-lexicon="${{items[0].lexicon}}">
850
-
<div class="tree-item-header">
851
-
<span>${{group}}</span>
852
-
<span class="tree-item-count">loading...</span>
853
-
</div>
854
-
</div>
855
-
`;
856
-
}} else {{
857
-
// Group header
858
-
html += `<div style="margin-bottom: 0.75rem;">`;
859
-
html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${{group}}</div>`;
860
-
861
-
// Items in group
862
-
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{
863
-
const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
864
-
html += `
865
-
<div class="tree-item" data-lexicon="${{item.lexicon}}" style="margin-left: 0.75rem;">
866
-
<div class="tree-item-header">
867
-
<span>${{displayName}}</span>
868
-
<span class="tree-item-count">loading...</span>
869
-
</div>
870
-
</div>
871
-
`;
872
-
}});
873
-
html += `</div>`;
874
-
}}
875
-
}});
876
-
}} else {{
877
-
html += `<div class="tree-item">no collections found</div>`;
878
-
}}
879
-
880
-
detail.innerHTML = html;
881
-
detail.classList.add('visible');
882
-
883
-
// Add close button handler
884
-
document.getElementById('detailClose').addEventListener('click', (e) => {{
885
-
e.stopPropagation();
886
-
detail.classList.remove('visible');
887
-
}});
888
-
889
-
// Fetch record counts for each collection
890
-
if (collections && collections.length > 0) {{
891
-
collections.forEach(lexicon => {{
892
-
fetch(`${{globalPds}}/xrpc/com.atproto.repo.listRecords?repo=${{did}}&collection=${{lexicon}}&limit=1`)
893
-
.then(r => r.json())
894
-
.then(data => {{
895
-
const item = detail.querySelector(`[data-lexicon="${{lexicon}}"]`);
896
-
if (item) {{
897
-
const countSpan = item.querySelector('.tree-item-count');
898
-
// The cursor field indicates there are more records
899
-
countSpan.textContent = data.records?.length > 0 ? 'has records' : 'empty';
900
-
}}
901
-
}})
902
-
.catch(e => {{
903
-
console.error('Error fetching count for', lexicon, e);
904
-
const item = detail.querySelector(`[data-lexicon="${{lexicon}}"]`);
905
-
if (item) {{
906
-
const countSpan = item.querySelector('.tree-item-count');
907
-
countSpan.textContent = 'error';
908
-
}}
909
-
}});
910
-
}});
911
-
}}
912
-
913
-
// Add click handlers to tree items to fetch actual records
914
-
detail.querySelectorAll('.tree-item[data-lexicon]').forEach(item => {{
915
-
item.addEventListener('click', (e) => {{
916
-
e.stopPropagation();
917
-
const lexicon = item.dataset.lexicon;
918
-
const existingRecords = item.querySelector('.record-list');
919
-
920
-
if (existingRecords) {{
921
-
existingRecords.remove();
922
-
return;
923
-
}}
924
-
925
-
const recordListDiv = document.createElement('div');
926
-
recordListDiv.className = 'record-list';
927
-
recordListDiv.innerHTML = '<div class="loading">loading records...</div>';
928
-
item.appendChild(recordListDiv);
929
-
930
-
fetch(`${{globalPds}}/xrpc/com.atproto.repo.listRecords?repo=${{did}}&collection=${{lexicon}}&limit=5`)
931
-
.then(r => r.json())
932
-
.then(data => {{
933
-
if (data.records && data.records.length > 0) {{
934
-
let recordsHtml = '';
935
-
data.records.forEach((record, idx) => {{
936
-
const json = JSON.stringify(record.value, null, 2);
937
-
const recordId = `record-${{Date.now()}}-${{idx}}`;
938
-
recordsHtml += `
939
-
<div class="record">
940
-
<div class="record-header">
941
-
<span class="record-label">record</span>
942
-
<button class="copy-btn" data-content="${{encodeURIComponent(json)}}" data-record-id="${{recordId}}">copy</button>
943
-
</div>
944
-
<div class="record-content">
945
-
<pre>${{json}}</pre>
946
-
</div>
947
-
</div>
948
-
`;
949
-
}});
950
-
951
-
if (data.cursor && data.records.length === 5) {{
952
-
recordsHtml += `<button class="load-more" data-cursor="${{data.cursor}}" data-lexicon="${{lexicon}}">load more</button>`;
953
-
}}
954
-
955
-
recordListDiv.innerHTML = recordsHtml;
956
-
957
-
// Use event delegation for copy and load more buttons
958
-
recordListDiv.addEventListener('click', (e) => {{
959
-
// Handle copy button
960
-
if (e.target.classList.contains('copy-btn')) {{
961
-
e.stopPropagation();
962
-
const copyBtn = e.target;
963
-
const content = decodeURIComponent(copyBtn.dataset.content);
964
-
965
-
navigator.clipboard.writeText(content).then(() => {{
966
-
const originalText = copyBtn.textContent;
967
-
copyBtn.textContent = 'copied!';
968
-
copyBtn.classList.add('copied');
969
-
setTimeout(() => {{
970
-
copyBtn.textContent = originalText;
971
-
copyBtn.classList.remove('copied');
972
-
}}, 1500);
973
-
}}).catch(err => {{
974
-
console.error('Failed to copy:', err);
975
-
copyBtn.textContent = 'error';
976
-
setTimeout(() => {{
977
-
copyBtn.textContent = 'copy';
978
-
}}, 1500);
979
-
}});
980
-
}}
981
-
982
-
// Handle load more button
983
-
if (e.target.classList.contains('load-more')) {{
984
-
e.stopPropagation();
985
-
const loadMoreBtn = e.target;
986
-
const cursor = loadMoreBtn.dataset.cursor;
987
-
const lexicon = loadMoreBtn.dataset.lexicon;
988
-
989
-
loadMoreBtn.textContent = 'loading...';
990
-
991
-
fetch(`${{globalPds}}/xrpc/com.atproto.repo.listRecords?repo=${{did}}&collection=${{lexicon}}&limit=5&cursor=${{cursor}}`)
992
-
.then(r => r.json())
993
-
.then(moreData => {{
994
-
let moreHtml = '';
995
-
moreData.records.forEach((record, idx) => {{
996
-
const json = JSON.stringify(record.value, null, 2);
997
-
const recordId = `record-more-${{Date.now()}}-${{idx}}`;
998
-
moreHtml += `
999
-
<div class="record">
1000
-
<div class="record-header">
1001
-
<span class="record-label">record</span>
1002
-
<button class="copy-btn" data-content="${{encodeURIComponent(json)}}" data-record-id="${{recordId}}">copy</button>
1003
-
</div>
1004
-
<div class="record-content">
1005
-
<pre>${{json}}</pre>
1006
-
</div>
1007
-
</div>
1008
-
`;
1009
-
}});
1010
-
1011
-
loadMoreBtn.remove();
1012
-
recordListDiv.insertAdjacentHTML('beforeend', moreHtml);
1013
-
1014
-
if (moreData.cursor && moreData.records.length === 5) {{
1015
-
recordListDiv.insertAdjacentHTML('beforeend',
1016
-
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`
1017
-
);
1018
-
}}
1019
-
}});
1020
-
}}
1021
-
}});
1022
-
}} else {{
1023
-
recordListDiv.innerHTML = '<div class="record">no records found</div>';
1024
-
}}
1025
-
}})
1026
-
.catch(e => {{
1027
-
console.error('Error fetching records:', e);
1028
-
recordListDiv.innerHTML = '<div class="record">error loading records</div>';
1029
-
}});
1030
-
}});
1031
-
}});
1032
-
}});
1033
-
1034
-
field.appendChild(div);
1035
-
}});
1036
-
1037
-
// Close detail panel when clicking canvas
1038
-
const canvas = document.querySelector('.canvas');
1039
-
canvas.addEventListener('click', (e) => {{
1040
-
if (e.target === canvas) {{
1041
-
document.getElementById('detail').classList.remove('visible');
1042
-
}}
1043
-
}});
1044
-
}})
1045
-
.catch(e => {{
1046
-
document.getElementById('field').innerHTML = 'error loading records';
1047
-
console.error(e);
1048
-
}});
1049
-
</script>
1050
-
</body>
1051
-
</html>
1052
-
"#, did)
1053
-
}
+201
src/view/atproto.js
+201
src/view/atproto.js
···
1
+
// ============================================================================
2
+
// ATPROTO UTILITIES - Client-side DID/PDS resolution
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
7
+
const DOMAIN_REDIRECTS = {
8
+
'tangled.sh': 'tangled.org',
9
+
};
10
+
11
+
export function applyDomainRedirect(domain) {
12
+
return DOMAIN_REDIRECTS[domain] || domain;
13
+
}
14
+
15
+
export function escapeHtml(text) {
16
+
const div = document.createElement('div');
17
+
div.textContent = text;
18
+
return div.innerHTML;
19
+
}
20
+
21
+
// Resolve a handle to a DID
22
+
export async function resolveHandle(handle) {
23
+
try {
24
+
const response = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
25
+
if (response.ok) {
26
+
const data = await response.json();
27
+
return data.did;
28
+
}
29
+
} catch (e) {
30
+
console.error('Failed to resolve handle:', e);
31
+
}
32
+
return null;
33
+
}
34
+
35
+
// Resolve a DID to its DID document (for PDS endpoint)
36
+
export async function resolveDid(did) {
37
+
try {
38
+
// For did:plc, use the plc.directory
39
+
if (did.startsWith('did:plc:')) {
40
+
const response = await fetch(`https://plc.directory/${did}`);
41
+
if (response.ok) {
42
+
return await response.json();
43
+
}
44
+
}
45
+
// For did:web, resolve via .well-known
46
+
if (did.startsWith('did:web:')) {
47
+
const domain = did.replace('did:web:', '');
48
+
const response = await fetch(`https://${domain}/.well-known/did.json`);
49
+
if (response.ok) {
50
+
return await response.json();
51
+
}
52
+
}
53
+
} catch (e) {
54
+
console.error('Failed to resolve DID:', e);
55
+
}
56
+
return null;
57
+
}
58
+
59
+
// Get PDS endpoint from DID document
60
+
export function getPdsFromDidDoc(didDoc) {
61
+
if (!didDoc || !didDoc.service) return null;
62
+
const atprotoService = didDoc.service.find(s =>
63
+
s.type === 'AtprotoPersonalDataServer' ||
64
+
s.id === '#atproto_pds'
65
+
);
66
+
return atprotoService?.serviceEndpoint || null;
67
+
}
68
+
69
+
// Get handle from DID document
70
+
export function getHandleFromDidDoc(didDoc) {
71
+
if (!didDoc || !didDoc.alsoKnownAs) return null;
72
+
const atUri = didDoc.alsoKnownAs.find(u => u.startsWith('at://'));
73
+
return atUri ? atUri.replace('at://', '') : null;
74
+
}
75
+
76
+
// Get profile from Bluesky AppView
77
+
export async function getProfile(handleOrDid) {
78
+
try {
79
+
const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`);
80
+
if (response.ok) {
81
+
return await response.json();
82
+
}
83
+
} catch (e) {
84
+
console.error('Failed to get profile:', e);
85
+
}
86
+
return null;
87
+
}
88
+
89
+
// Describe the repo to get all collections
90
+
export async function describeRepo(pds, did) {
91
+
try {
92
+
const response = await fetch(`${pds}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(did)}`);
93
+
if (response.ok) {
94
+
return await response.json();
95
+
}
96
+
} catch (e) {
97
+
console.error('Failed to describe repo:', e);
98
+
}
99
+
return null;
100
+
}
101
+
102
+
// List records in a collection
103
+
export async function listRecords(pds, did, collection, limit = 10, cursor = null) {
104
+
try {
105
+
let url = `${pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&limit=${limit}`;
106
+
if (cursor) url += `&cursor=${cursor}`;
107
+
const response = await fetch(url);
108
+
if (response.ok) {
109
+
return await response.json();
110
+
}
111
+
} catch (e) {
112
+
console.error('Failed to list records:', e);
113
+
}
114
+
return null;
115
+
}
116
+
117
+
// Get app avatar from the namespace's well-known profile
118
+
export async function getAppAvatar(namespace) {
119
+
const domain = namespace.split('.').reverse().join('.');
120
+
const redirectedDomain = applyDomainRedirect(domain);
121
+
try {
122
+
const profile = await getProfile(redirectedDomain);
123
+
if (profile && profile.avatar) {
124
+
return profile.avatar;
125
+
}
126
+
} catch (e) {
127
+
// Silently fail
128
+
}
129
+
return null;
130
+
}
131
+
132
+
// Batch fetch app avatars
133
+
export async function fetchAppAvatars(namespaces) {
134
+
const avatars = {};
135
+
const promises = namespaces.map(async (ns) => {
136
+
const avatar = await getAppAvatar(ns);
137
+
if (avatar) {
138
+
avatars[ns] = avatar;
139
+
}
140
+
});
141
+
await Promise.all(promises);
142
+
return avatars;
143
+
}
144
+
145
+
// Validate app URLs by checking if the domain is reachable
146
+
export async function validateAppUrls(appDivs) {
147
+
// Clear previous invalid apps
148
+
state.invalidApps.clear();
149
+
150
+
const validationPromises = appDivs.map(async ({ div, namespace }) => {
151
+
const link = div.querySelector('.app-name');
152
+
const url = link?.dataset.url;
153
+
if (!url || !link) return;
154
+
155
+
try {
156
+
new URL(url); // Check syntax first
157
+
} catch (e) {
158
+
markInvalid(link, namespace, 'malformed URL');
159
+
return;
160
+
}
161
+
162
+
// Try HEAD request with short timeout to check if domain is reachable
163
+
try {
164
+
const controller = new AbortController();
165
+
const timeout = setTimeout(() => controller.abort(), 3000);
166
+
167
+
await fetch(url, {
168
+
method: 'HEAD',
169
+
mode: 'no-cors',
170
+
signal: controller.signal,
171
+
});
172
+
173
+
clearTimeout(timeout);
174
+
// If we get here, domain is reachable (even if response is opaque due to CORS)
175
+
} catch (e) {
176
+
// Only mark as invalid for actual DNS/connection failures
177
+
// CORS blocks mean the server IS reachable, just not allowing our request
178
+
const errorMsg = e.message || '';
179
+
if (errorMsg.includes('ERR_NAME_NOT_RESOLVED') ||
180
+
errorMsg.includes('ERR_CONNECTION_REFUSED') ||
181
+
errorMsg.includes('ERR_CONNECTION_TIMED_OUT') ||
182
+
e.name === 'AbortError') {
183
+
markInvalid(link, namespace, 'domain not reachable');
184
+
}
185
+
// For CORS blocks (ERR_FAILED) and other errors, server exists so don't mark invalid
186
+
}
187
+
});
188
+
189
+
await Promise.all(validationPromises);
190
+
}
191
+
192
+
function markInvalid(link, namespace, reason) {
193
+
const displayName = link.textContent.replace(' โ', '').replace(' \u2197', '');
194
+
link.classList.add('invalid-link');
195
+
link.setAttribute('title', reason);
196
+
link.style.pointerEvents = 'none';
197
+
link.textContent = displayName;
198
+
if (namespace) {
199
+
state.invalidApps.add(namespace);
200
+
}
201
+
}
+155
src/view/base.css
+155
src/view/base.css
···
1
+
* {
2
+
margin: 0;
3
+
padding: 0;
4
+
box-sizing: border-box;
5
+
}
6
+
7
+
:root {
8
+
--bg: #f5f1e8;
9
+
--text: #4a4238;
10
+
--text-light: #8a7a6a;
11
+
--text-lighter: #6b5d4f;
12
+
--border: #c9bfa8;
13
+
--surface: #e5dbc8;
14
+
--surface-hover: #d9cdb5;
15
+
}
16
+
17
+
@media (prefers-color-scheme: dark) {
18
+
:root {
19
+
--bg: #1a1a1a;
20
+
--text: #e5e5e5;
21
+
--text-light: #a0a0a0;
22
+
--text-lighter: #c0c0c0;
23
+
--border: #404040;
24
+
--surface: #2a2a2a;
25
+
--surface-hover: #353535;
26
+
}
27
+
}
28
+
29
+
body {
30
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
31
+
height: 100vh;
32
+
background: var(--bg);
33
+
color: var(--text);
34
+
overflow: hidden;
35
+
position: relative;
36
+
-webkit-font-smoothing: antialiased;
37
+
-moz-osx-font-smoothing: grayscale;
38
+
}
39
+
40
+
.canvas {
41
+
position: fixed;
42
+
inset: 0;
43
+
}
44
+
45
+
.info {
46
+
position: fixed;
47
+
bottom: clamp(0.75rem, 2vmin, 1rem);
48
+
left: clamp(0.75rem, 2vmin, 1rem);
49
+
width: clamp(32px, 7vmin, 40px);
50
+
height: clamp(32px, 7vmin, 40px);
51
+
display: flex;
52
+
align-items: center;
53
+
justify-content: center;
54
+
font-size: clamp(0.85rem, 1.8vmin, 1rem);
55
+
color: var(--text-light);
56
+
cursor: pointer;
57
+
transition: all 0.2s ease;
58
+
z-index: 100;
59
+
-webkit-tap-highlight-color: transparent;
60
+
}
61
+
62
+
.info:hover,
63
+
.info:active {
64
+
color: var(--text);
65
+
}
66
+
67
+
.info-modal {
68
+
position: fixed;
69
+
top: 50%;
70
+
left: 50%;
71
+
transform: translate(-50%, -50%);
72
+
background: var(--surface);
73
+
border: 2px solid var(--border);
74
+
padding: 2rem;
75
+
max-width: 500px;
76
+
width: 90%;
77
+
z-index: 2000;
78
+
display: none;
79
+
border-radius: 4px;
80
+
}
81
+
82
+
@media (max-width: 768px) {
83
+
.info-modal {
84
+
padding: 1.5rem;
85
+
width: 95%;
86
+
}
87
+
88
+
.info-modal h2 {
89
+
font-size: 0.9rem;
90
+
}
91
+
92
+
.info-modal p {
93
+
font-size: 0.7rem;
94
+
}
95
+
}
96
+
97
+
.info-modal.visible {
98
+
display: block;
99
+
}
100
+
101
+
.info-modal h2 {
102
+
margin-bottom: 1rem;
103
+
font-size: 1rem;
104
+
color: var(--text);
105
+
}
106
+
107
+
.info-modal p {
108
+
margin-bottom: 0.75rem;
109
+
font-size: 0.75rem;
110
+
line-height: 1.5;
111
+
color: var(--text-lighter);
112
+
}
113
+
114
+
.info-modal button {
115
+
margin-top: 1rem;
116
+
padding: 0.5rem 1rem;
117
+
background: var(--bg);
118
+
border: 1px solid var(--border);
119
+
color: var(--text);
120
+
font-family: inherit;
121
+
font-size: 0.7rem;
122
+
cursor: pointer;
123
+
transition: all 0.2s ease;
124
+
-webkit-tap-highlight-color: transparent;
125
+
border-radius: 2px;
126
+
}
127
+
128
+
.info-modal button:hover,
129
+
.info-modal button:active {
130
+
background: var(--surface-hover);
131
+
border-color: var(--text-light);
132
+
}
133
+
134
+
@media (max-width: 768px) {
135
+
.info-modal button {
136
+
padding: 0.65rem 1.2rem;
137
+
font-size: 0.75rem;
138
+
}
139
+
}
140
+
141
+
.overlay {
142
+
position: fixed;
143
+
top: 0;
144
+
left: 0;
145
+
right: 0;
146
+
bottom: 0;
147
+
background: rgba(0, 0, 0, 0.5);
148
+
z-index: 1999;
149
+
display: none;
150
+
}
151
+
152
+
.overlay.visible {
153
+
display: block;
154
+
}
155
+
+262
src/view/controls.css
+262
src/view/controls.css
···
1
+
.home-btn {
2
+
position: fixed;
3
+
top: clamp(1rem, 2vmin, 1.5rem);
4
+
left: clamp(1rem, 2vmin, 1.5rem);
5
+
font-family: inherit;
6
+
font-size: clamp(0.85rem, 1.8vmin, 1rem);
7
+
color: var(--text-light);
8
+
border: 1px solid var(--border);
9
+
background: var(--bg);
10
+
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem);
11
+
transition: all 0.2s ease;
12
+
z-index: 100;
13
+
cursor: pointer;
14
+
border-radius: 2px;
15
+
text-decoration: none;
16
+
display: inline-flex;
17
+
align-items: center;
18
+
justify-content: center;
19
+
width: clamp(32px, 7vmin, 40px);
20
+
height: clamp(32px, 7vmin, 40px);
21
+
}
22
+
23
+
.home-btn:hover,
24
+
.home-btn:active {
25
+
background: var(--surface);
26
+
color: var(--text);
27
+
border-color: var(--text-light);
28
+
}
29
+
30
+
.watch-live-btn {
31
+
font-family: inherit;
32
+
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
33
+
color: var(--text-light);
34
+
border: 1px solid var(--border);
35
+
background: var(--bg);
36
+
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem);
37
+
transition: all 0.2s ease;
38
+
cursor: pointer;
39
+
border-radius: 2px;
40
+
display: flex;
41
+
align-items: center;
42
+
gap: clamp(0.3rem, 0.8vmin, 0.5rem);
43
+
}
44
+
45
+
.watch-live-btn:hover,
46
+
.watch-live-btn:active {
47
+
background: var(--surface);
48
+
color: var(--text);
49
+
border-color: var(--text-light);
50
+
}
51
+
52
+
.watch-live-btn.active {
53
+
background: var(--surface-hover);
54
+
color: var(--text);
55
+
border-color: var(--text);
56
+
}
57
+
58
+
.watch-indicator {
59
+
width: clamp(8px, 2vmin, 10px);
60
+
height: clamp(8px, 2vmin, 10px);
61
+
border-radius: 50%;
62
+
background: var(--text-light);
63
+
display: none;
64
+
}
65
+
66
+
.watch-live-btn.active .watch-indicator {
67
+
display: block;
68
+
animation: pulse 2s ease-in-out infinite;
69
+
}
70
+
71
+
@keyframes pulse {
72
+
0%, 100% { opacity: 0.5; }
73
+
50% { opacity: 1; }
74
+
}
75
+
76
+
.top-right-buttons {
77
+
position: fixed;
78
+
top: clamp(1rem, 2vmin, 1.5rem);
79
+
right: clamp(1rem, 2vmin, 1.5rem);
80
+
display: flex;
81
+
flex-direction: row;
82
+
align-items: center;
83
+
gap: clamp(0.5rem, 1vmin, 0.75rem);
84
+
z-index: 100;
85
+
}
86
+
87
+
@media (max-width: 768px) {
88
+
.top-right-buttons {
89
+
flex-direction: column;
90
+
align-items: flex-end;
91
+
}
92
+
}
93
+
94
+
.filter-btn {
95
+
font-family: inherit;
96
+
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
97
+
color: var(--text-light);
98
+
border: 1px solid var(--border);
99
+
background: var(--bg);
100
+
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem);
101
+
transition: all 0.2s ease;
102
+
cursor: pointer;
103
+
border-radius: 2px;
104
+
display: flex;
105
+
align-items: center;
106
+
gap: clamp(0.3rem, 0.8vmin, 0.5rem);
107
+
}
108
+
109
+
.filter-btn:hover,
110
+
.filter-btn:active {
111
+
background: var(--surface);
112
+
color: var(--text);
113
+
border-color: var(--text-light);
114
+
}
115
+
116
+
.filter-btn.active {
117
+
background: var(--surface-hover);
118
+
color: var(--text);
119
+
border-color: var(--text);
120
+
}
121
+
122
+
.filter-btn.has-filters {
123
+
border-color: var(--text-light);
124
+
}
125
+
126
+
.filter-count {
127
+
font-size: 0.6rem;
128
+
background: var(--text-light);
129
+
color: var(--bg);
130
+
padding: 0.1rem 0.35rem;
131
+
border-radius: 2px;
132
+
font-weight: 500;
133
+
}
134
+
135
+
.info {
136
+
position: fixed;
137
+
bottom: clamp(0.75rem, 2vmin, 1rem);
138
+
left: clamp(0.75rem, 2vmin, 1rem);
139
+
width: clamp(32px, 7vmin, 40px);
140
+
height: clamp(32px, 7vmin, 40px);
141
+
display: flex;
142
+
align-items: center;
143
+
justify-content: center;
144
+
font-size: clamp(0.85rem, 1.8vmin, 1rem);
145
+
color: var(--text-light);
146
+
cursor: pointer;
147
+
transition: all 0.2s ease;
148
+
z-index: 100;
149
+
-webkit-tap-highlight-color: transparent;
150
+
}
151
+
152
+
.info:hover,
153
+
.info:active {
154
+
color: var(--text);
155
+
}
156
+
157
+
.info-modal {
158
+
position: fixed;
159
+
top: 50%;
160
+
left: 50%;
161
+
transform: translate(-50%, -50%);
162
+
background: var(--surface);
163
+
border: 2px solid var(--border);
164
+
padding: 2rem;
165
+
max-width: 500px;
166
+
width: 90%;
167
+
z-index: 2000;
168
+
display: none;
169
+
border-radius: 4px;
170
+
}
171
+
172
+
@media (max-width: 768px) {
173
+
.info-modal {
174
+
padding: 1.5rem;
175
+
width: 95%;
176
+
}
177
+
178
+
.info-modal h2 {
179
+
font-size: 0.9rem;
180
+
}
181
+
182
+
.info-modal p {
183
+
font-size: 0.7rem;
184
+
}
185
+
}
186
+
187
+
.info-modal.visible {
188
+
display: block;
189
+
}
190
+
191
+
.info-modal h2 {
192
+
margin-bottom: 1rem;
193
+
font-size: 1rem;
194
+
color: var(--text);
195
+
}
196
+
197
+
.info-modal p {
198
+
margin-bottom: 0.75rem;
199
+
font-size: 0.75rem;
200
+
line-height: 1.5;
201
+
color: var(--text-lighter);
202
+
}
203
+
204
+
.info-modal button {
205
+
margin-top: 1rem;
206
+
padding: 0.5rem 1rem;
207
+
background: var(--bg);
208
+
border: 1px solid var(--border);
209
+
color: var(--text);
210
+
font-family: inherit;
211
+
font-size: 0.7rem;
212
+
cursor: pointer;
213
+
transition: all 0.2s ease;
214
+
-webkit-tap-highlight-color: transparent;
215
+
border-radius: 2px;
216
+
}
217
+
218
+
.info-modal button:hover,
219
+
.info-modal button:active {
220
+
background: var(--surface-hover);
221
+
border-color: var(--text-light);
222
+
}
223
+
224
+
@media (max-width: 768px) {
225
+
.info-modal button {
226
+
padding: 0.65rem 1.2rem;
227
+
font-size: 0.75rem;
228
+
}
229
+
}
230
+
231
+
.overlay {
232
+
position: fixed;
233
+
top: 0;
234
+
left: 0;
235
+
right: 0;
236
+
bottom: 0;
237
+
background: rgba(0, 0, 0, 0.5);
238
+
z-index: 1999;
239
+
display: none;
240
+
}
241
+
242
+
.overlay.visible {
243
+
display: block;
244
+
}
245
+
246
+
.pov-indicator {
247
+
position: fixed;
248
+
bottom: clamp(0.75rem, 2vmin, 1rem);
249
+
right: clamp(0.75rem, 2vmin, 1rem);
250
+
font-size: clamp(0.55rem, 1.2vmin, 0.65rem);
251
+
color: var(--text-light);
252
+
z-index: 100;
253
+
}
254
+
255
+
.pov-handle {
256
+
color: var(--text);
257
+
text-decoration: none;
258
+
}
259
+
260
+
.pov-handle:hover {
261
+
text-decoration: underline;
262
+
}
+359
src/view/detail.css
+359
src/view/detail.css
···
1
+
.detail-panel {
2
+
position: fixed;
3
+
top: 0;
4
+
left: 0;
5
+
bottom: 0;
6
+
width: 500px;
7
+
background: var(--surface);
8
+
border-right: 2px solid var(--border);
9
+
padding: 2.5rem 2rem;
10
+
overflow-y: auto;
11
+
opacity: 0;
12
+
transform: translateX(-100%);
13
+
transition: all 0.25s ease;
14
+
z-index: 1000;
15
+
scrollbar-width: none;
16
+
-ms-overflow-style: none;
17
+
}
18
+
19
+
.detail-panel::-webkit-scrollbar {
20
+
display: none;
21
+
}
22
+
23
+
.detail-panel.visible {
24
+
opacity: 1;
25
+
transform: translateX(0);
26
+
}
27
+
28
+
@media (max-width: 768px) {
29
+
.detail-panel {
30
+
width: 100%;
31
+
padding: 4rem 1.5rem 2rem;
32
+
border-right: none;
33
+
border-bottom: 2px solid var(--border);
34
+
}
35
+
}
36
+
37
+
.detail-panel h3 {
38
+
margin-bottom: 0.75rem;
39
+
font-size: 0.85rem;
40
+
color: var(--text);
41
+
}
42
+
43
+
.detail-panel .subtitle {
44
+
font-size: 0.7rem;
45
+
color: var(--text-light);
46
+
margin-bottom: 1.5rem;
47
+
line-height: 1.4;
48
+
}
49
+
50
+
.detail-close {
51
+
position: absolute;
52
+
top: 1.5rem;
53
+
right: 1.5rem;
54
+
width: 32px;
55
+
height: 32px;
56
+
border: 1px solid var(--border);
57
+
background: var(--bg);
58
+
color: var(--text-light);
59
+
cursor: pointer;
60
+
display: flex;
61
+
align-items: center;
62
+
justify-content: center;
63
+
font-size: 1.2rem;
64
+
line-height: 1;
65
+
transition: all 0.2s ease;
66
+
border-radius: 2px;
67
+
-webkit-tap-highlight-color: transparent;
68
+
}
69
+
70
+
.detail-close:hover,
71
+
.detail-close:active {
72
+
background: var(--surface-hover);
73
+
border-color: var(--text-light);
74
+
color: var(--text);
75
+
}
76
+
77
+
@media (max-width: 768px) {
78
+
.detail-close {
79
+
top: 1rem;
80
+
right: 1rem;
81
+
width: 40px;
82
+
height: 40px;
83
+
font-size: 1.4rem;
84
+
}
85
+
}
86
+
87
+
.tree-item {
88
+
padding: 0.65rem 0.75rem;
89
+
font-size: 0.75rem;
90
+
color: var(--text-lighter);
91
+
background: var(--bg);
92
+
border: 1px solid var(--border);
93
+
border-radius: 2px;
94
+
margin-bottom: 0.5rem;
95
+
transition: all 0.15s ease;
96
+
cursor: pointer;
97
+
-webkit-tap-highlight-color: transparent;
98
+
}
99
+
100
+
.tree-item:hover,
101
+
.tree-item:active {
102
+
background: var(--surface-hover);
103
+
border-color: var(--text-light);
104
+
}
105
+
106
+
@media (max-width: 768px) {
107
+
.tree-item {
108
+
padding: 0.8rem 0.9rem;
109
+
font-size: 0.8rem;
110
+
}
111
+
}
112
+
113
+
.tree-item:last-child {
114
+
margin-bottom: 0;
115
+
}
116
+
117
+
.tree-item-header {
118
+
display: flex;
119
+
justify-content: space-between;
120
+
align-items: center;
121
+
}
122
+
123
+
.tree-item-count {
124
+
font-size: 0.65rem;
125
+
color: var(--text-light);
126
+
}
127
+
128
+
.collection-content {
129
+
margin-top: 0.5rem;
130
+
padding-top: 0.5rem;
131
+
border-top: 1px solid var(--border);
132
+
}
133
+
134
+
.collection-tabs {
135
+
display: flex;
136
+
gap: 0;
137
+
margin-bottom: 0.75rem;
138
+
border: 1px solid var(--border);
139
+
border-radius: 2px;
140
+
overflow: hidden;
141
+
}
142
+
143
+
.collection-tab {
144
+
flex: 1;
145
+
padding: 0.5rem 0.75rem;
146
+
background: var(--bg);
147
+
border: none;
148
+
border-right: 1px solid var(--border);
149
+
color: var(--text-light);
150
+
font-family: inherit;
151
+
font-size: 0.65rem;
152
+
cursor: pointer;
153
+
transition: all 0.15s ease;
154
+
-webkit-tap-highlight-color: transparent;
155
+
}
156
+
157
+
.collection-tab:last-child {
158
+
border-right: none;
159
+
}
160
+
161
+
.collection-tab:hover {
162
+
background: var(--surface);
163
+
color: var(--text);
164
+
}
165
+
166
+
.collection-tab.active {
167
+
background: var(--surface-hover);
168
+
color: var(--text);
169
+
font-weight: 500;
170
+
}
171
+
172
+
.collection-view-content {
173
+
position: relative;
174
+
}
175
+
176
+
.collection-view {
177
+
display: none;
178
+
}
179
+
180
+
.collection-view.active {
181
+
display: block;
182
+
}
183
+
184
+
.structure-view {
185
+
min-height: 600px;
186
+
}
187
+
188
+
.mst-canvas {
189
+
width: 100%;
190
+
height: 600px;
191
+
border: 1px solid var(--border);
192
+
border-radius: 4px;
193
+
background: var(--bg);
194
+
margin-top: 0.5rem;
195
+
}
196
+
197
+
.mst-info {
198
+
background: var(--bg);
199
+
border: 1px solid var(--border);
200
+
padding: 0.75rem;
201
+
border-radius: 4px;
202
+
margin-bottom: 0.75rem;
203
+
}
204
+
205
+
.mst-info p {
206
+
font-size: 0.65rem;
207
+
color: var(--text-lighter);
208
+
line-height: 1.5;
209
+
margin: 0;
210
+
}
211
+
212
+
.record-list {
213
+
margin-top: 0.5rem;
214
+
padding-top: 0.5rem;
215
+
border-top: 1px solid var(--border);
216
+
}
217
+
218
+
.record {
219
+
margin-bottom: 0.5rem;
220
+
background: var(--bg);
221
+
border: 1px solid var(--border);
222
+
border-radius: 4px;
223
+
font-size: 0.65rem;
224
+
color: var(--text-light);
225
+
transition: all 0.15s ease;
226
+
overflow: hidden;
227
+
}
228
+
229
+
.record:hover {
230
+
border-color: var(--text-light);
231
+
background: var(--surface);
232
+
}
233
+
234
+
.record:last-child {
235
+
margin-bottom: 0;
236
+
}
237
+
238
+
.record-header {
239
+
display: flex;
240
+
justify-content: space-between;
241
+
align-items: center;
242
+
padding: 0.5rem 0.6rem;
243
+
background: var(--surface);
244
+
border-bottom: 1px solid var(--border);
245
+
}
246
+
247
+
.record-label {
248
+
font-size: 0.6rem;
249
+
color: var(--text-lighter);
250
+
font-weight: 500;
251
+
}
252
+
253
+
.copy-btn {
254
+
background: var(--bg);
255
+
border: 1px solid var(--border);
256
+
color: var(--text-light);
257
+
font-family: inherit;
258
+
font-size: 0.55rem;
259
+
padding: 0.2rem 0.5rem;
260
+
cursor: pointer;
261
+
transition: all 0.15s ease;
262
+
border-radius: 2px;
263
+
-webkit-tap-highlight-color: transparent;
264
+
}
265
+
266
+
.copy-btn:hover,
267
+
.copy-btn:active {
268
+
background: var(--surface-hover);
269
+
border-color: var(--text-light);
270
+
color: var(--text);
271
+
}
272
+
273
+
.copy-btn.copied {
274
+
color: var(--text);
275
+
border-color: var(--text);
276
+
}
277
+
278
+
.record-content {
279
+
padding: 0.6rem;
280
+
}
281
+
282
+
.record-content pre {
283
+
margin: 0;
284
+
white-space: pre-wrap;
285
+
word-break: break-word;
286
+
line-height: 1.5;
287
+
font-size: 0.625rem;
288
+
}
289
+
290
+
.load-more {
291
+
margin-top: 0.5rem;
292
+
padding: 0.4rem 0.6rem;
293
+
background: var(--bg);
294
+
border: 1px solid var(--border);
295
+
color: var(--text);
296
+
font-family: inherit;
297
+
font-size: 0.65rem;
298
+
cursor: pointer;
299
+
width: 100%;
300
+
transition: all 0.15s ease;
301
+
-webkit-tap-highlight-color: transparent;
302
+
border-radius: 2px;
303
+
}
304
+
305
+
.load-more:hover,
306
+
.load-more:active {
307
+
background: var(--surface-hover);
308
+
border-color: var(--text-light);
309
+
}
310
+
311
+
@media (max-width: 768px) {
312
+
.load-more {
313
+
padding: 0.6rem 0.8rem;
314
+
font-size: 0.7rem;
315
+
}
316
+
}
317
+
318
+
#field.loading {
319
+
position: fixed;
320
+
inset: 0;
321
+
display: flex;
322
+
flex-direction: column;
323
+
align-items: center;
324
+
justify-content: center;
325
+
gap: 1.5rem;
326
+
z-index: 1000;
327
+
background: var(--bg);
328
+
}
329
+
330
+
#field.loading~.identity {
331
+
display: none;
332
+
}
333
+
334
+
.loading-spinner {
335
+
width: 48px;
336
+
height: 48px;
337
+
border: 3px solid var(--border);
338
+
border-top-color: var(--text);
339
+
border-radius: 50%;
340
+
animation: spin 0.8s linear infinite;
341
+
}
342
+
343
+
@keyframes spin {
344
+
to {
345
+
transform: rotate(360deg);
346
+
}
347
+
}
348
+
349
+
.loading-text {
350
+
color: var(--text);
351
+
font-size: 0.85rem;
352
+
font-weight: 500;
353
+
letter-spacing: 0.05em;
354
+
}
355
+
356
+
.loading-progress {
357
+
color: var(--text-light);
358
+
font-size: 0.7rem;
359
+
}
+158
src/view/filter.css
+158
src/view/filter.css
···
1
+
.filter-panel {
2
+
position: fixed;
3
+
top: clamp(3.5rem, 7vmin, 4.5rem);
4
+
right: clamp(1rem, 2vmin, 1.5rem);
5
+
background: var(--surface);
6
+
border: 1px solid var(--border);
7
+
border-radius: 4px;
8
+
padding: 1rem;
9
+
z-index: 250;
10
+
max-height: 60vh;
11
+
overflow-y: auto;
12
+
min-width: 200px;
13
+
max-width: 280px;
14
+
display: none;
15
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
16
+
}
17
+
18
+
@media (max-width: 768px) {
19
+
.filter-panel {
20
+
top: clamp(6rem, 12vmin, 8rem);
21
+
}
22
+
}
23
+
24
+
@media (prefers-color-scheme: dark) {
25
+
.filter-panel {
26
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
27
+
}
28
+
}
29
+
30
+
.filter-panel.visible {
31
+
display: block;
32
+
}
33
+
34
+
.filter-panel-header {
35
+
display: flex;
36
+
justify-content: space-between;
37
+
align-items: center;
38
+
margin-bottom: 0.75rem;
39
+
padding-bottom: 0.5rem;
40
+
border-bottom: 1px solid var(--border);
41
+
}
42
+
43
+
.filter-panel-title {
44
+
font-size: 0.7rem;
45
+
font-weight: 500;
46
+
color: var(--text);
47
+
text-transform: lowercase;
48
+
}
49
+
50
+
.filter-panel-actions {
51
+
display: flex;
52
+
gap: 0.5rem;
53
+
}
54
+
55
+
.filter-action-btn {
56
+
font-family: inherit;
57
+
font-size: 0.6rem;
58
+
color: var(--text-light);
59
+
background: transparent;
60
+
border: none;
61
+
cursor: pointer;
62
+
padding: 0.2rem 0;
63
+
transition: color 0.2s ease;
64
+
}
65
+
66
+
.filter-action-btn:hover {
67
+
color: var(--text);
68
+
}
69
+
70
+
.filter-list {
71
+
display: flex;
72
+
flex-direction: column;
73
+
gap: 0.25rem;
74
+
}
75
+
76
+
.filter-item {
77
+
display: flex;
78
+
align-items: center;
79
+
gap: 0.5rem;
80
+
padding: 0.4rem 0.5rem;
81
+
border-radius: 2px;
82
+
cursor: pointer;
83
+
transition: background 0.15s ease;
84
+
}
85
+
86
+
.filter-item:hover {
87
+
background: var(--surface-hover);
88
+
}
89
+
90
+
.filter-checkbox {
91
+
width: 14px;
92
+
height: 14px;
93
+
border: 1px solid var(--border);
94
+
border-radius: 2px;
95
+
background: var(--bg);
96
+
display: flex;
97
+
align-items: center;
98
+
justify-content: center;
99
+
flex-shrink: 0;
100
+
transition: all 0.15s ease;
101
+
}
102
+
103
+
.filter-item.checked .filter-checkbox {
104
+
background: var(--text);
105
+
border-color: var(--text);
106
+
}
107
+
108
+
.filter-checkbox-icon {
109
+
width: 10px;
110
+
height: 10px;
111
+
stroke: var(--bg);
112
+
stroke-width: 2;
113
+
opacity: 0;
114
+
transition: opacity 0.15s ease;
115
+
}
116
+
117
+
.filter-item.checked .filter-checkbox-icon {
118
+
opacity: 1;
119
+
}
120
+
121
+
.filter-label {
122
+
font-size: 0.7rem;
123
+
color: var(--text-lighter);
124
+
overflow: hidden;
125
+
text-overflow: ellipsis;
126
+
white-space: nowrap;
127
+
}
128
+
129
+
.filter-item.checked .filter-label {
130
+
color: var(--text);
131
+
}
132
+
133
+
.app-view.filtered {
134
+
display: none !important;
135
+
}
136
+
137
+
@keyframes pulse {
138
+
0%,
139
+
100% {
140
+
opacity: 1;
141
+
}
142
+
50% {
143
+
opacity: 0.3;
144
+
}
145
+
}
146
+
147
+
@keyframes gentle-pulse {
148
+
0%,
149
+
100% {
150
+
transform: scale(1);
151
+
box-shadow: 0 0 0 0 var(--text-light);
152
+
}
153
+
50% {
154
+
transform: scale(1.02);
155
+
box-shadow: 0 0 0 3px rgba(160, 160, 160, 0.2);
156
+
}
157
+
}
158
+
+245
src/view/filters.js
+245
src/view/filters.js
···
1
+
// ============================================================================
2
+
// FILTER PANEL
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
import { applyDomainRedirect } from './atproto.js';
7
+
8
+
function loadHiddenApps() {
9
+
const params = new URLSearchParams(window.location.search);
10
+
const hideParam = params.get('hide');
11
+
if (hideParam) {
12
+
state.hiddenApps = new Set(hideParam.split(',').filter(Boolean));
13
+
return;
14
+
}
15
+
16
+
try {
17
+
const stored = localStorage.getItem(`atme_hidden_apps_${state.did}`);
18
+
if (stored) {
19
+
state.hiddenApps = new Set(JSON.parse(stored));
20
+
}
21
+
} catch (e) {
22
+
state.hiddenApps = new Set();
23
+
}
24
+
}
25
+
26
+
function saveHiddenApps() {
27
+
try {
28
+
localStorage.setItem(`atme_hidden_apps_${state.did}`, JSON.stringify([...state.hiddenApps]));
29
+
} catch (e) {}
30
+
31
+
const params = new URLSearchParams(window.location.search);
32
+
if (state.hiddenApps.size > 0) {
33
+
params.set('hide', [...state.hiddenApps].join(','));
34
+
} else {
35
+
params.delete('hide');
36
+
}
37
+
const newUrl = params.toString()
38
+
? `${window.location.pathname}?${params.toString()}`
39
+
: window.location.pathname;
40
+
history.replaceState(null, '', newUrl);
41
+
}
42
+
43
+
function updateFilterButton() {
44
+
const filterBtn = document.getElementById('filterBtn');
45
+
const filterCount = document.getElementById('filterCount');
46
+
47
+
if (state.hiddenApps.size > 0) {
48
+
filterBtn.classList.add('has-filters');
49
+
filterCount.textContent = state.hiddenApps.size;
50
+
filterCount.style.display = 'inline-block';
51
+
} else {
52
+
filterBtn.classList.remove('has-filters');
53
+
filterCount.style.display = 'none';
54
+
}
55
+
}
56
+
57
+
export function applyFilters() {
58
+
const appViews = document.querySelectorAll('.app-view');
59
+
appViews.forEach(view => {
60
+
const circle = view.querySelector('.app-circle');
61
+
if (circle) {
62
+
const namespace = circle.dataset.namespace;
63
+
if (state.hiddenApps.has(namespace)) {
64
+
view.classList.add('filtered');
65
+
} else {
66
+
view.classList.remove('filtered');
67
+
}
68
+
}
69
+
});
70
+
updateFilterButton();
71
+
saveHiddenApps();
72
+
// Reposition visible apps in a circle
73
+
repositionAppCircles();
74
+
}
75
+
76
+
function populateFilterList() {
77
+
if (!state.globalApps) return;
78
+
79
+
const filterList = document.getElementById('filterList');
80
+
const appNames = Object.keys(state.globalApps).filter(k => k !== '_circleSize').sort();
81
+
82
+
filterList.innerHTML = appNames.map(namespace => {
83
+
const rawDisplayName = namespace.split('.').reverse().join('.');
84
+
const displayName = applyDomainRedirect(rawDisplayName);
85
+
const isChecked = !state.hiddenApps.has(namespace);
86
+
const isInvalid = state.invalidApps.has(namespace);
87
+
return `
88
+
<div class="filter-item ${isChecked ? 'checked' : ''}" data-namespace="${namespace}">
89
+
<div class="filter-checkbox">
90
+
<svg class="filter-checkbox-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
91
+
<polyline points="20 6 9 17 4 12"></polyline>
92
+
</svg>
93
+
</div>
94
+
<span class="filter-label">${displayName}${isInvalid ? ' (unresolved)' : ''}</span>
95
+
</div>
96
+
`;
97
+
}).join('');
98
+
99
+
filterList.querySelectorAll('.filter-item').forEach(item => {
100
+
item.addEventListener('click', () => {
101
+
const namespace = item.dataset.namespace;
102
+
if (state.hiddenApps.has(namespace)) {
103
+
state.hiddenApps.delete(namespace);
104
+
item.classList.add('checked');
105
+
} else {
106
+
state.hiddenApps.add(namespace);
107
+
item.classList.remove('checked');
108
+
}
109
+
applyFilters();
110
+
});
111
+
});
112
+
}
113
+
114
+
export function initFilterPanel() {
115
+
loadHiddenApps();
116
+
117
+
// Clean up stale hidden apps that no longer exist
118
+
if (state.globalApps) {
119
+
const validNamespaces = new Set(Object.keys(state.globalApps).filter(k => k !== '_circleSize'));
120
+
state.hiddenApps = new Set([...state.hiddenApps].filter(ns => validNamespaces.has(ns)));
121
+
}
122
+
123
+
const filterBtn = document.getElementById('filterBtn');
124
+
const filterPanel = document.getElementById('filterPanel');
125
+
const filterShowAll = document.getElementById('filterShowAll');
126
+
const filterHideAll = document.getElementById('filterHideAll');
127
+
const filterHideUnresolved = document.getElementById('filterHideUnresolved');
128
+
129
+
filterBtn.addEventListener('click', (e) => {
130
+
e.stopPropagation();
131
+
filterPanel.classList.toggle('visible');
132
+
filterBtn.classList.toggle('active');
133
+
if (filterPanel.classList.contains('visible')) {
134
+
populateFilterList();
135
+
}
136
+
});
137
+
138
+
document.addEventListener('click', (e) => {
139
+
if (!filterPanel.contains(e.target) && e.target !== filterBtn && !filterBtn.contains(e.target)) {
140
+
filterPanel.classList.remove('visible');
141
+
filterBtn.classList.remove('active');
142
+
}
143
+
});
144
+
145
+
filterShowAll.addEventListener('click', (e) => {
146
+
e.preventDefault();
147
+
e.stopPropagation();
148
+
state.hiddenApps.clear();
149
+
populateFilterList();
150
+
applyFilters();
151
+
});
152
+
153
+
filterHideAll.addEventListener('click', (e) => {
154
+
e.preventDefault();
155
+
e.stopPropagation();
156
+
if (!state.globalApps) return;
157
+
const appNames = Object.keys(state.globalApps).filter(k => k !== '_circleSize');
158
+
state.hiddenApps = new Set(appNames);
159
+
populateFilterList();
160
+
applyFilters();
161
+
});
162
+
163
+
// "valid" button - hide unresolved/invalid apps, show valid ones
164
+
filterHideUnresolved.addEventListener('click', (e) => {
165
+
e.preventDefault();
166
+
e.stopPropagation();
167
+
if (!state.globalApps) return;
168
+
// Hide only invalid apps, show all valid ones
169
+
state.hiddenApps = new Set(state.invalidApps);
170
+
populateFilterList();
171
+
applyFilters();
172
+
});
173
+
174
+
applyFilters();
175
+
}
176
+
177
+
export function repositionAppCircles() {
178
+
if (!state.globalApps) return;
179
+
180
+
const allAppViews = document.querySelectorAll('.app-view');
181
+
// Get only visible (non-filtered) app views
182
+
const visibleAppViews = Array.from(allAppViews).filter(view => !view.classList.contains('filtered'));
183
+
const visibleCount = visibleAppViews.length;
184
+
185
+
if (visibleCount === 0) return;
186
+
187
+
const vmin = Math.min(window.innerWidth, window.innerHeight);
188
+
const isMobile = window.innerWidth < 768;
189
+
190
+
let circleSize, radius;
191
+
if (isMobile) {
192
+
if (visibleCount <= 5) {
193
+
circleSize = Math.min(60, vmin * 0.08);
194
+
radius = vmin * 0.38;
195
+
} else if (visibleCount <= 10) {
196
+
circleSize = Math.min(50, vmin * 0.07);
197
+
radius = vmin * 0.4;
198
+
} else if (visibleCount <= 20) {
199
+
circleSize = Math.min(40, vmin * 0.055);
200
+
radius = vmin * 0.42;
201
+
} else {
202
+
circleSize = Math.min(32, vmin * 0.045);
203
+
radius = vmin * 0.44;
204
+
}
205
+
circleSize = Math.max(circleSize, 28);
206
+
radius = Math.max(radius, 120);
207
+
} else {
208
+
if (visibleCount <= 5) {
209
+
circleSize = Math.min(70, vmin * 0.1);
210
+
} else if (visibleCount <= 10) {
211
+
circleSize = Math.min(60, vmin * 0.09);
212
+
} else if (visibleCount <= 20) {
213
+
circleSize = Math.min(50, vmin * 0.07);
214
+
} else {
215
+
circleSize = Math.min(40, vmin * 0.06);
216
+
}
217
+
circleSize = Math.max(circleSize, 35);
218
+
// Calculate radius to ensure minimum spacing between apps
219
+
// Arc length between apps should be at least circleSize + gap
220
+
const minGap = 30;
221
+
const minRadiusForSpacing = (visibleCount * (circleSize + minGap)) / (2 * Math.PI);
222
+
radius = Math.max(vmin * 0.35, minRadiusForSpacing, 150);
223
+
}
224
+
225
+
const centerX = window.innerWidth / 2;
226
+
const centerY = window.innerHeight / 2;
227
+
228
+
// Position only visible apps evenly around the circle
229
+
visibleAppViews.forEach((div, i) => {
230
+
const angle = (i / visibleCount) * 2 * Math.PI - Math.PI / 2;
231
+
const circleOffset = circleSize / 2;
232
+
const x = centerX + radius * Math.cos(angle) - circleOffset;
233
+
const y = centerY + radius * Math.sin(angle) - circleOffset;
234
+
235
+
div.style.left = `${x}px`;
236
+
div.style.top = `${y}px`;
237
+
238
+
const circle = div.querySelector('.app-circle');
239
+
if (circle) {
240
+
circle.style.width = `${circleSize}px`;
241
+
circle.style.height = `${circleSize}px`;
242
+
circle.style.fontSize = `${circleSize * 0.4}px`;
243
+
}
244
+
});
245
+
}
+58
src/view/firehose.css
+58
src/view/firehose.css
···
1
+
.firehose-toast {
2
+
position: fixed;
3
+
top: clamp(4rem, 8vmin, 5rem);
4
+
right: clamp(1rem, 2vmin, 1.5rem);
5
+
background: var(--surface);
6
+
border: 1px solid var(--border);
7
+
padding: 0.75rem 1rem;
8
+
border-radius: 4px;
9
+
font-size: 0.7rem;
10
+
color: var(--text);
11
+
z-index: 200;
12
+
opacity: 0;
13
+
transform: translateY(-10px);
14
+
transition: all 0.3s ease;
15
+
pointer-events: none;
16
+
max-width: min(300px, calc(100vw - 2rem));
17
+
width: max-content;
18
+
}
19
+
20
+
@media (max-width: 768px) {
21
+
.firehose-toast {
22
+
top: clamp(7rem, 14vmin, 9rem);
23
+
}
24
+
}
25
+
26
+
.firehose-toast.visible {
27
+
opacity: 1;
28
+
transform: translateY(0);
29
+
pointer-events: auto;
30
+
}
31
+
32
+
.firehose-toast-action {
33
+
font-weight: 600;
34
+
color: var(--text);
35
+
}
36
+
37
+
.firehose-toast-collection {
38
+
color: var(--text-light);
39
+
font-size: 0.65rem;
40
+
margin-top: 0.25rem;
41
+
}
42
+
43
+
.firehose-toast-link {
44
+
display: inline-block;
45
+
color: var(--text-light);
46
+
font-size: 0.6rem;
47
+
margin-top: 0.5rem;
48
+
text-decoration: none;
49
+
border-bottom: 1px solid transparent;
50
+
transition: all 0.2s ease;
51
+
pointer-events: auto;
52
+
}
53
+
54
+
.firehose-toast-link:hover {
55
+
color: var(--text);
56
+
border-bottom-color: var(--text);
57
+
}
58
+
+131
src/view/firehose.js
+131
src/view/firehose.js
···
1
+
// ============================================================================
2
+
// FIREHOSE (Jetstream WebSocket)
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
import {
7
+
createFirehoseParticle,
8
+
initFirehoseCanvas,
9
+
animateFirehoseParticles,
10
+
cleanupFirehoseCanvas
11
+
} from './particles.js';
12
+
13
+
function connectFirehose() {
14
+
if (!state.did || state.jetstreamWs) return;
15
+
16
+
const watchBtn = document.getElementById('watchLiveBtn');
17
+
const watchLabel = watchBtn.querySelector('.watch-label');
18
+
19
+
// Connect to Jetstream filtering by this DID
20
+
const wsUrl = `wss://jetstream2.us-east.bsky.network/subscribe?wantedDids=${encodeURIComponent(state.did)}`;
21
+
state.jetstreamWs = new WebSocket(wsUrl);
22
+
23
+
state.jetstreamWs.onopen = () => {
24
+
watchLabel.textContent = 'watching...';
25
+
watchBtn.classList.add('active');
26
+
};
27
+
28
+
state.jetstreamWs.onmessage = (event) => {
29
+
try {
30
+
const data = JSON.parse(event.data);
31
+
if (data.kind === 'commit' && data.commit) {
32
+
const commit = data.commit;
33
+
const collection = commit.collection;
34
+
const operation = commit.operation;
35
+
36
+
// Get namespace from collection
37
+
const parts = collection.split('.');
38
+
const namespace = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : collection;
39
+
40
+
const eventData = {
41
+
action: operation,
42
+
collection: collection,
43
+
namespace: namespace,
44
+
rkey: commit.rkey
45
+
};
46
+
47
+
// Create particle animation
48
+
createFirehoseParticle(eventData);
49
+
50
+
// Show toast notification
51
+
showFirehoseToast(eventData);
52
+
}
53
+
} catch (e) {
54
+
console.error('Error processing Jetstream message:', e);
55
+
}
56
+
};
57
+
58
+
state.jetstreamWs.onerror = (error) => {
59
+
console.error('Jetstream error:', error);
60
+
watchLabel.textContent = 'connection error';
61
+
};
62
+
63
+
state.jetstreamWs.onclose = () => {
64
+
if (state.isWatchingLive) {
65
+
watchLabel.textContent = 'reconnecting...';
66
+
setTimeout(() => {
67
+
state.jetstreamWs = null;
68
+
if (state.isWatchingLive) connectFirehose();
69
+
}, 3000);
70
+
}
71
+
};
72
+
}
73
+
74
+
function disconnectFirehose() {
75
+
if (state.jetstreamWs) {
76
+
state.jetstreamWs.close();
77
+
state.jetstreamWs = null;
78
+
}
79
+
}
80
+
81
+
function showFirehoseToast(event) {
82
+
const toast = document.getElementById('firehoseToast');
83
+
const actionEl = toast.querySelector('.firehose-toast-action');
84
+
const collectionEl = toast.querySelector('.firehose-toast-collection');
85
+
const linkEl = document.getElementById('firehoseToastLink');
86
+
87
+
const actionText = {
88
+
'create': 'created',
89
+
'update': 'updated',
90
+
'delete': 'deleted'
91
+
}[event.action] || event.action;
92
+
93
+
actionEl.textContent = `${actionText} record`;
94
+
collectionEl.innerHTML = `<code style="background: var(--bg); padding: 0.1rem 0.3rem; border-radius: 2px; font-size: 0.6rem;">${event.collection}</code>`;
95
+
96
+
if (event.action === 'delete') {
97
+
linkEl.style.display = 'none';
98
+
} else {
99
+
linkEl.style.display = 'inline-block';
100
+
if (state.globalPds && event.rkey) {
101
+
linkEl.href = `${state.globalPds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(state.did)}&collection=${encodeURIComponent(event.collection)}&rkey=${encodeURIComponent(event.rkey)}`;
102
+
}
103
+
}
104
+
105
+
toast.classList.add('visible');
106
+
setTimeout(() => {
107
+
toast.classList.remove('visible');
108
+
}, 4000);
109
+
}
110
+
111
+
export function initFirehoseUI() {
112
+
// Watch live button handler
113
+
document.getElementById('watchLiveBtn').addEventListener('click', () => {
114
+
const watchBtn = document.getElementById('watchLiveBtn');
115
+
const watchLabel = watchBtn.querySelector('.watch-label');
116
+
117
+
if (state.isWatchingLive) {
118
+
state.isWatchingLive = false;
119
+
watchLabel.textContent = 'watch live';
120
+
watchBtn.classList.remove('active');
121
+
disconnectFirehose();
122
+
cleanupFirehoseCanvas();
123
+
} else {
124
+
state.isWatchingLive = true;
125
+
watchLabel.textContent = 'connecting...';
126
+
initFirehoseCanvas();
127
+
animateFirehoseParticles();
128
+
connectFirehose();
129
+
}
130
+
});
131
+
}
+71
src/view/guestbook-state.js
+71
src/view/guestbook-state.js
···
1
+
// ============================================================================
2
+
// GUESTBOOK STATE (via Microcosm UFOs API)
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
7
+
const UFOS_API = 'https://ufos-api.microcosm.blue';
8
+
export let allGuestbookSignatures = [];
9
+
10
+
export async function fetchAllGuestbookSignatures() {
11
+
try {
12
+
const response = await fetch(`${UFOS_API}/records?collection=app.at-me.visit`);
13
+
if (response.ok) {
14
+
allGuestbookSignatures = await response.json();
15
+
}
16
+
} catch (e) {
17
+
console.error('Error fetching guestbook signatures from Microcosm:', e);
18
+
}
19
+
}
20
+
21
+
export async function checkGuestbookState() {
22
+
if (!state.did) return;
23
+
24
+
// Fetch all signatures if we haven't yet
25
+
if (allGuestbookSignatures.length === 0) {
26
+
await fetchAllGuestbookSignatures();
27
+
}
28
+
29
+
// Check if the viewed user has signed the guestbook
30
+
state.pageOwnerHasSigned = allGuestbookSignatures.some(sig => sig.did === state.did);
31
+
32
+
updateGuestbookUI();
33
+
}
34
+
35
+
export function updateGuestbookUI() {
36
+
const signEl = document.querySelector('.guestbook-sign');
37
+
const signBtn = document.getElementById('signGuestbookBtn');
38
+
const avatarImg = document.getElementById('guestbookAvatar');
39
+
const iconSpan = signBtn?.querySelector('.guestbook-icon');
40
+
const textSpan = signBtn?.querySelector('.guestbook-text');
41
+
42
+
if (signEl) {
43
+
signEl.textContent = state.pageOwnerHasSigned ? 'you already signed' : 'sign the guest list';
44
+
}
45
+
46
+
if (!signBtn || !iconSpan || !textSpan) return;
47
+
48
+
signBtn.classList.remove('signed', 'pulse');
49
+
50
+
if (state.pageOwnerHasSigned) {
51
+
if (avatarImg) avatarImg.style.display = 'none';
52
+
iconSpan.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
53
+
iconSpan.style.display = 'flex';
54
+
textSpan.textContent = 'signed';
55
+
signBtn.classList.add('signed');
56
+
signBtn.setAttribute('title', 'this user has signed the guestbook');
57
+
} else {
58
+
// Show the viewed user's avatar
59
+
if (state.viewedAvatar && avatarImg) {
60
+
avatarImg.src = state.viewedAvatar;
61
+
avatarImg.style.display = 'block';
62
+
iconSpan.style.display = 'none';
63
+
} else {
64
+
if (avatarImg) avatarImg.style.display = 'none';
65
+
iconSpan.style.display = 'none';
66
+
}
67
+
textSpan.textContent = 'sign as';
68
+
signBtn.classList.add('pulse');
69
+
signBtn.setAttribute('title', `sign in as @${state.globalHandle || 'user'}`);
70
+
}
71
+
}
+78
src/view/guestbook-ui.js
+78
src/view/guestbook-ui.js
···
1
+
// ============================================================================
2
+
// GUESTBOOK (simplified - just viewing, no OAuth write for now)
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
import { escapeHtml } from './atproto.js';
7
+
import { allGuestbookSignatures, fetchAllGuestbookSignatures } from './guestbook-state.js';
8
+
9
+
export function initGuestbookUI() {
10
+
document.getElementById('viewGuestbookBtn').addEventListener('click', async () => {
11
+
const modal = document.getElementById('guestbookModal');
12
+
const content = document.getElementById('guestbookContent');
13
+
14
+
modal.classList.add('visible');
15
+
content.innerHTML = `
16
+
<div class="guestbook-loading">
17
+
<div class="guestbook-loading-spinner"></div>
18
+
<div class="guestbook-loading-text">loading signatures...</div>
19
+
</div>
20
+
`;
21
+
22
+
// Fetch all guestbook signatures from Microcosm UFOs API
23
+
if (allGuestbookSignatures.length === 0) {
24
+
await fetchAllGuestbookSignatures();
25
+
}
26
+
27
+
if (allGuestbookSignatures.length > 0) {
28
+
// Sort by time_us descending (most recent first)
29
+
const sorted = [...allGuestbookSignatures].sort((a, b) => b.time_us - a.time_us);
30
+
31
+
content.innerHTML = `
32
+
<div class="guestbook-paper">
33
+
<div class="guestbook-paper-title">guestbook</div>
34
+
<div class="guestbook-paper-subtitle">visitors who have signed</div>
35
+
<div class="guestbook-tally">${sorted.length} signature${sorted.length !== 1 ? 's' : ''}</div>
36
+
<div class="guestbook-signatures-list">
37
+
${sorted.map(sig => `
38
+
<div class="guestbook-paper-signature">
39
+
<a class="guestbook-did" href="?did=${encodeURIComponent(sig.did)}" title="view ${sig.did}">${sig.did}</a>
40
+
${sig.record?.text ? `<div class="guestbook-message">${escapeHtml(sig.record.text)}</div>` : ''}
41
+
</div>
42
+
`).join('')}
43
+
</div>
44
+
</div>
45
+
`;
46
+
} else {
47
+
content.innerHTML = `
48
+
<div class="guestbook-paper">
49
+
<div class="guestbook-paper-title">guestbook</div>
50
+
<div class="guestbook-paper-subtitle">no signatures yet</div>
51
+
<div class="guestbook-tally">be the first to sign!</div>
52
+
</div>
53
+
`;
54
+
}
55
+
});
56
+
57
+
document.getElementById('guestbookClose').addEventListener('click', () => {
58
+
document.getElementById('guestbookModal').classList.remove('visible');
59
+
});
60
+
61
+
// Escape key to close guestbook modal
62
+
document.addEventListener('keydown', (e) => {
63
+
if (e.key === 'Escape') {
64
+
document.getElementById('guestbookModal').classList.remove('visible');
65
+
}
66
+
});
67
+
68
+
// Sign guestbook button - for now just show a message about OAuth
69
+
document.getElementById('signGuestbookBtn').addEventListener('click', () => {
70
+
if (state.pageOwnerHasSigned) {
71
+
// Already signed - show the guestbook
72
+
document.getElementById('viewGuestbookBtn').click();
73
+
} else {
74
+
// Not signed - would need OAuth
75
+
alert(`OAuth signing coming soon! For now, visit your own PDS to create an app.at-me.visit record.`);
76
+
}
77
+
});
78
+
}
+405
src/view/guestbook.css
+405
src/view/guestbook.css
···
1
+
.guestbook-buttons-container {
2
+
position: fixed;
3
+
bottom: clamp(0.75rem, 2vmin, 1rem);
4
+
right: clamp(0.75rem, 2vmin, 1rem);
5
+
display: flex;
6
+
flex-direction: row;
7
+
align-items: center;
8
+
gap: clamp(0.5rem, 1.5vmin, 0.75rem);
9
+
z-index: 100;
10
+
}
11
+
12
+
.view-guestbook-btn {
13
+
font-family: inherit;
14
+
font-size: clamp(0.85rem, 1.8vmin, 1rem);
15
+
color: var(--text-light);
16
+
border: 1px solid var(--border);
17
+
background: var(--bg);
18
+
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem);
19
+
transition: all 0.2s ease;
20
+
cursor: pointer;
21
+
border-radius: 2px;
22
+
width: clamp(32px, 7vmin, 40px);
23
+
height: clamp(32px, 7vmin, 40px);
24
+
display: flex;
25
+
align-items: center;
26
+
justify-content: center;
27
+
}
28
+
29
+
.view-guestbook-btn:hover,
30
+
.view-guestbook-btn:active {
31
+
background: var(--surface);
32
+
color: var(--text);
33
+
border-color: var(--text-light);
34
+
}
35
+
36
+
.sign-guestbook-btn {
37
+
font-family: inherit;
38
+
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
39
+
color: var(--text-light);
40
+
border: 1px solid var(--border);
41
+
background: var(--bg);
42
+
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.6rem, 1.5vmin, 0.8rem);
43
+
transition: all 0.2s ease;
44
+
cursor: pointer;
45
+
border-radius: 2px;
46
+
display: flex;
47
+
align-items: center;
48
+
gap: clamp(0.3rem, 0.8vmin, 0.5rem);
49
+
height: clamp(32px, 7vmin, 40px);
50
+
white-space: nowrap;
51
+
}
52
+
53
+
.sign-guestbook-btn:hover,
54
+
.sign-guestbook-btn:active {
55
+
background: var(--surface);
56
+
color: var(--text);
57
+
border-color: var(--text-light);
58
+
}
59
+
60
+
.sign-guestbook-btn:disabled {
61
+
opacity: 0.5;
62
+
cursor: not-allowed;
63
+
}
64
+
65
+
.sign-guestbook-btn.signed {
66
+
border-color: var(--text-light);
67
+
background: var(--surface);
68
+
}
69
+
70
+
.sign-guestbook-btn.pulse {
71
+
animation: gentle-pulse 2s ease-in-out infinite;
72
+
}
73
+
74
+
.guestbook-icon {
75
+
display: flex;
76
+
align-items: center;
77
+
line-height: 1;
78
+
}
79
+
80
+
.guestbook-avatar {
81
+
width: clamp(20px, 4.5vmin, 24px);
82
+
height: clamp(20px, 4.5vmin, 24px);
83
+
border-radius: 50%;
84
+
object-fit: cover;
85
+
border: 1px solid var(--border);
86
+
flex-shrink: 0;
87
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.1);
88
+
}
89
+
90
+
@media (prefers-color-scheme: dark) {
91
+
.guestbook-avatar {
92
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
93
+
}
94
+
}
95
+
96
+
.guestbook-modal {
97
+
position: fixed;
98
+
inset: 0;
99
+
background: var(--bg);
100
+
z-index: 2000;
101
+
display: none;
102
+
overflow-y: auto;
103
+
padding: clamp(4rem, 8vmin, 6rem) clamp(1rem, 3vmin, 2rem) clamp(2rem, 4vmin, 3rem);
104
+
}
105
+
106
+
.guestbook-modal.visible {
107
+
display: block;
108
+
}
109
+
110
+
.guestbook-paper {
111
+
max-width: 700px;
112
+
margin: 0 auto;
113
+
background:
114
+
repeating-linear-gradient(0deg,
115
+
transparent,
116
+
transparent 31px,
117
+
rgba(212, 197, 168, 0.15) 31px,
118
+
rgba(212, 197, 168, 0.15) 32px),
119
+
linear-gradient(to bottom, #fdfcf8 0%, #f9f7f1 100%);
120
+
border: 1px solid #d4c5a8;
121
+
box-shadow:
122
+
0 4px 6px rgba(0, 0, 0, 0.1),
123
+
0 2px 4px rgba(0, 0, 0, 0.06),
124
+
inset 0 0 80px rgba(255, 248, 240, 0.6);
125
+
padding: clamp(2.5rem, 6vmin, 4rem) clamp(2rem, 5vmin, 3rem);
126
+
position: relative;
127
+
}
128
+
129
+
@media (prefers-color-scheme: dark) {
130
+
.guestbook-paper {
131
+
background:
132
+
repeating-linear-gradient(0deg,
133
+
transparent,
134
+
transparent 31px,
135
+
rgba(90, 80, 70, 0.2) 31px,
136
+
rgba(90, 80, 70, 0.2) 32px),
137
+
linear-gradient(to bottom, #2a2520 0%, #1f1b17 100%);
138
+
border-color: #3a3530;
139
+
box-shadow:
140
+
0 4px 6px rgba(0, 0, 0, 0.5),
141
+
0 2px 4px rgba(0, 0, 0, 0.3),
142
+
inset 0 0 80px rgba(60, 50, 40, 0.4);
143
+
}
144
+
}
145
+
146
+
.guestbook-paper::before {
147
+
content: '';
148
+
position: absolute;
149
+
top: 0;
150
+
left: clamp(2rem, 5vmin, 3rem);
151
+
width: 2px;
152
+
height: 100%;
153
+
background: linear-gradient(to bottom,
154
+
transparent 0%,
155
+
rgba(212, 100, 100, 0.2) 5%,
156
+
rgba(212, 100, 100, 0.2) 95%,
157
+
transparent 100%);
158
+
}
159
+
160
+
@media (prefers-color-scheme: dark) {
161
+
.guestbook-paper::before {
162
+
background: linear-gradient(to bottom,
163
+
transparent 0%,
164
+
rgba(180, 80, 80, 0.15) 5%,
165
+
rgba(180, 80, 80, 0.15) 95%,
166
+
transparent 100%);
167
+
}
168
+
}
169
+
170
+
.guestbook-paper-title {
171
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
172
+
font-size: clamp(1.8rem, 4.5vmin, 2.5rem);
173
+
color: #3a2f25;
174
+
text-align: center;
175
+
margin-bottom: clamp(0.5rem, 1.5vmin, 1rem);
176
+
font-weight: 500;
177
+
letter-spacing: 0.02em;
178
+
}
179
+
180
+
@media (prefers-color-scheme: dark) {
181
+
.guestbook-paper-title {
182
+
color: #d4c5a8;
183
+
}
184
+
}
185
+
186
+
.guestbook-paper-subtitle {
187
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
188
+
font-size: clamp(0.75rem, 1.6vmin, 0.9rem);
189
+
color: #6b5d4f;
190
+
text-align: center;
191
+
margin-bottom: clamp(2rem, 5vmin, 3rem);
192
+
font-style: normal;
193
+
}
194
+
195
+
@media (prefers-color-scheme: dark) {
196
+
.guestbook-paper-subtitle {
197
+
color: #8a7a6a;
198
+
}
199
+
}
200
+
201
+
.guestbook-tally {
202
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, 'Courier New', monospace;
203
+
text-align: center;
204
+
font-size: clamp(0.7rem, 1.8vmin, 0.85rem);
205
+
color: #6b5d4f;
206
+
margin: clamp(1rem, 2.5vmin, 1.5rem) 0 0;
207
+
font-weight: 500;
208
+
letter-spacing: 0.03em;
209
+
text-transform: lowercase;
210
+
}
211
+
212
+
@media (prefers-color-scheme: dark) {
213
+
.guestbook-tally {
214
+
color: #8a7a6a;
215
+
}
216
+
}
217
+
218
+
.guestbook-signatures-list {
219
+
margin-top: clamp(1.5rem, 4vmin, 2.5rem);
220
+
}
221
+
222
+
.guestbook-paper-signature {
223
+
padding: clamp(1rem, 2.5vmin, 1.5rem) 0;
224
+
border-bottom: 1px solid rgba(212, 197, 168, 0.3);
225
+
position: relative;
226
+
cursor: pointer;
227
+
transition: all 0.3s ease;
228
+
}
229
+
230
+
.guestbook-paper-signature:last-child {
231
+
border-bottom: none;
232
+
}
233
+
234
+
.guestbook-paper-signature:hover {
235
+
background: rgba(255, 248, 240, 0.3);
236
+
padding-left: 0.5rem;
237
+
padding-right: 0.5rem;
238
+
margin-left: -0.5rem;
239
+
margin-right: -0.5rem;
240
+
}
241
+
242
+
@media (prefers-color-scheme: dark) {
243
+
.guestbook-paper-signature {
244
+
border-bottom-color: rgba(90, 80, 70, 0.3);
245
+
}
246
+
247
+
.guestbook-paper-signature:hover {
248
+
background: rgba(60, 50, 40, 0.3);
249
+
}
250
+
}
251
+
252
+
.guestbook-did {
253
+
font-family: 'Brush Script MT', cursive, 'Georgia', serif;
254
+
font-size: clamp(1.1rem, 2.5vmin, 1.4rem);
255
+
color: #2a2520;
256
+
margin-bottom: clamp(0.3rem, 0.8vmin, 0.5rem);
257
+
font-weight: 400;
258
+
letter-spacing: 0.02em;
259
+
word-break: break-all;
260
+
cursor: pointer;
261
+
transition: all 0.2s ease;
262
+
position: relative;
263
+
}
264
+
265
+
.guestbook-did:hover {
266
+
color: #4a4238;
267
+
transform: translateX(2px);
268
+
}
269
+
270
+
@media (prefers-color-scheme: dark) {
271
+
.guestbook-did {
272
+
color: #c9bfa8;
273
+
}
274
+
275
+
.guestbook-did:hover {
276
+
color: #d4c5a8;
277
+
}
278
+
}
279
+
280
+
.guestbook-message {
281
+
font-size: 0.65rem;
282
+
color: #6b6052;
283
+
font-style: italic;
284
+
margin-top: 0.2rem;
285
+
padding-left: 0.5rem;
286
+
border-left: 2px solid #d4c5a866;
287
+
}
288
+
289
+
@media (prefers-color-scheme: dark) {
290
+
.guestbook-message {
291
+
color: #a89f8c;
292
+
border-left-color: #c9bfa866;
293
+
}
294
+
}
295
+
296
+
.guestbook-close {
297
+
position: fixed;
298
+
top: clamp(1rem, 2vmin, 1.5rem);
299
+
right: clamp(1rem, 2vmin, 1.5rem);
300
+
width: clamp(40px, 8vmin, 48px);
301
+
height: clamp(40px, 8vmin, 48px);
302
+
border: 2px solid var(--border);
303
+
background: var(--surface);
304
+
color: var(--text-light);
305
+
cursor: pointer;
306
+
display: flex;
307
+
align-items: center;
308
+
justify-content: center;
309
+
font-size: clamp(1.2rem, 3vmin, 1.5rem);
310
+
line-height: 1;
311
+
transition: all 0.2s ease;
312
+
border-radius: 4px;
313
+
z-index: 2001;
314
+
}
315
+
316
+
.guestbook-close:hover,
317
+
.guestbook-close:active {
318
+
background: var(--surface-hover);
319
+
border-color: var(--text-light);
320
+
color: var(--text);
321
+
}
322
+
323
+
.guestbook-loading {
324
+
max-width: 800px;
325
+
margin: 0 auto;
326
+
text-align: center;
327
+
padding: clamp(3rem, 8vmin, 5rem) clamp(1rem, 3vmin, 2rem);
328
+
}
329
+
330
+
.guestbook-loading-spinner {
331
+
width: 40px;
332
+
height: 40px;
333
+
border: 3px solid var(--border);
334
+
border-top-color: var(--text);
335
+
border-radius: 50%;
336
+
animation: spin 0.8s linear infinite;
337
+
margin: 0 auto clamp(1rem, 3vmin, 1.5rem);
338
+
}
339
+
340
+
.guestbook-loading-text {
341
+
font-size: clamp(0.75rem, 1.6vmin, 0.9rem);
342
+
color: var(--text-light);
343
+
}
344
+
345
+
.guestbook-sign {
346
+
position: fixed;
347
+
bottom: clamp(3.5rem, 8.5vmin, 5.5rem);
348
+
right: clamp(0.75rem, 2vmin, 1rem);
349
+
font-family: ui-monospace, 'SF Mono', Monaco, monospace;
350
+
font-size: clamp(0.6rem, 1.3vmin, 0.7rem);
351
+
color: var(--text-light);
352
+
text-transform: lowercase;
353
+
letter-spacing: 0.1em;
354
+
z-index: 50;
355
+
opacity: 0.6;
356
+
text-shadow: 0 0 4px currentColor;
357
+
animation: neon-flicker 8s infinite;
358
+
pointer-events: none;
359
+
user-select: none;
360
+
white-space: nowrap;
361
+
}
362
+
363
+
@media (prefers-color-scheme: dark) {
364
+
.guestbook-sign {
365
+
color: #ff6b9d;
366
+
opacity: 0.5;
367
+
text-shadow: 0 0 6px currentColor, 0 0 12px rgba(255, 107, 157, 0.3);
368
+
}
369
+
}
370
+
371
+
.pov-indicator {
372
+
position: fixed;
373
+
left: 50%;
374
+
top: clamp(1rem, 2vmin, 1.5rem);
375
+
transform: translateX(-50%);
376
+
font-family: ui-monospace, 'SF Mono', Monaco, monospace;
377
+
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
378
+
color: var(--text-light);
379
+
text-transform: lowercase;
380
+
letter-spacing: 0.12em;
381
+
z-index: 50;
382
+
opacity: 0.4;
383
+
text-shadow: 0 0 3px currentColor;
384
+
animation: pov-subtle-flicker 37s infinite;
385
+
pointer-events: none;
386
+
user-select: none;
387
+
text-align: center;
388
+
line-height: 1.4;
389
+
}
390
+
391
+
.pov-handle {
392
+
display: inline;
393
+
margin-left: 0.3rem;
394
+
font-size: inherit;
395
+
opacity: 0.9;
396
+
pointer-events: auto;
397
+
text-decoration: none;
398
+
color: inherit;
399
+
transition: opacity 0.2s ease;
400
+
}
401
+
402
+
.pov-handle:hover {
403
+
opacity: 1;
404
+
text-decoration: underline;
405
+
}
+173
src/view/layout.css
+173
src/view/layout.css
···
1
+
.identity {
2
+
position: absolute;
3
+
left: 50%;
4
+
top: 50%;
5
+
transform: translate(-50%, -50%);
6
+
background: var(--surface);
7
+
border: 2px solid var(--text-light);
8
+
border-radius: 50%;
9
+
width: clamp(100px, 20vmin, 140px);
10
+
height: clamp(100px, 20vmin, 140px);
11
+
display: flex;
12
+
flex-direction: column;
13
+
align-items: center;
14
+
justify-content: center;
15
+
z-index: 10;
16
+
cursor: pointer;
17
+
transition: all 0.2s ease;
18
+
-webkit-tap-highlight-color: transparent;
19
+
}
20
+
21
+
.identity:hover,
22
+
.identity:active {
23
+
transform: translate(-50%, -50%) scale(1.05);
24
+
border-color: var(--text);
25
+
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
26
+
}
27
+
28
+
.identity.pulse {
29
+
animation: identityPulse 0.3s ease-out;
30
+
}
31
+
32
+
@keyframes identityPulse {
33
+
0% { box-shadow: 0 0 0 0 rgba(139, 164, 184, 0.4); }
34
+
70% { box-shadow: 0 0 0 15px rgba(139, 164, 184, 0); }
35
+
100% { box-shadow: 0 0 0 0 rgba(139, 164, 184, 0); }
36
+
}
37
+
38
+
.identity-label {
39
+
font-size: clamp(1rem, 2vmin, 1.2rem);
40
+
color: var(--text);
41
+
font-weight: 600;
42
+
line-height: 1;
43
+
}
44
+
45
+
.identity-value {
46
+
font-size: 0.7rem;
47
+
color: var(--text-lighter);
48
+
text-align: center;
49
+
white-space: nowrap;
50
+
font-weight: 400;
51
+
line-height: 1.2;
52
+
}
53
+
54
+
.identity-value:hover {
55
+
opacity: 0.7;
56
+
}
57
+
58
+
.identity-pds-label {
59
+
position: absolute;
60
+
bottom: clamp(-1.5rem, -3vmin, -2rem);
61
+
font-size: clamp(0.55rem, 1.1vmin, 0.65rem);
62
+
color: var(--text-light);
63
+
letter-spacing: 0.05em;
64
+
font-weight: 500;
65
+
text-decoration: none;
66
+
white-space: nowrap;
67
+
transition: opacity 0.2s ease;
68
+
}
69
+
70
+
.identity-pds-label:hover {
71
+
opacity: 0.7;
72
+
}
73
+
74
+
.identity-avatar {
75
+
position: absolute;
76
+
top: 0;
77
+
left: 0;
78
+
width: 100%;
79
+
height: 100%;
80
+
border-radius: 50%;
81
+
object-fit: cover;
82
+
}
83
+
84
+
.identity-handle {
85
+
position: absolute;
86
+
bottom: -1.8rem;
87
+
left: 50%;
88
+
transform: translateX(-50%);
89
+
font-size: clamp(0.7rem, 1.5vmin, 0.85rem);
90
+
color: var(--text-light);
91
+
white-space: nowrap;
92
+
}
93
+
94
+
.app-view {
95
+
position: absolute;
96
+
display: flex;
97
+
flex-direction: column;
98
+
align-items: center;
99
+
gap: clamp(0.3rem, 1vmin, 0.5rem);
100
+
cursor: pointer;
101
+
transition: all 0.2s ease;
102
+
opacity: 0.7;
103
+
}
104
+
105
+
.app-view:hover {
106
+
opacity: 1;
107
+
transform: scale(1.1);
108
+
z-index: 100;
109
+
}
110
+
111
+
.app-circle {
112
+
background: var(--surface-hover);
113
+
border: 1px solid var(--border);
114
+
border-radius: 50%;
115
+
width: clamp(55px, 10vmin, 70px);
116
+
height: clamp(55px, 10vmin, 70px);
117
+
display: flex;
118
+
align-items: center;
119
+
justify-content: center;
120
+
transition: all 0.2s ease;
121
+
overflow: hidden;
122
+
font-size: clamp(1rem, 2vmin, 1.5rem);
123
+
}
124
+
125
+
.app-logo {
126
+
width: 100%;
127
+
height: 100%;
128
+
object-fit: cover;
129
+
}
130
+
131
+
.app-view:hover .app-circle {
132
+
background: var(--surface);
133
+
border-color: var(--text-light);
134
+
}
135
+
136
+
.app-name {
137
+
font-size: clamp(0.55rem, 1.2vmin, 0.7rem);
138
+
color: var(--text);
139
+
text-align: center;
140
+
max-width: clamp(70px, 15vmin, 120px);
141
+
text-decoration: none;
142
+
display: block;
143
+
overflow: hidden;
144
+
text-overflow: ellipsis;
145
+
white-space: nowrap;
146
+
}
147
+
148
+
@media (max-width: 768px) {
149
+
.app-name {
150
+
font-size: clamp(0.5rem, 1vmin, 0.6rem);
151
+
max-width: clamp(60px, 12vmin, 100px);
152
+
}
153
+
154
+
#field.many-apps .app-name {
155
+
display: none;
156
+
}
157
+
}
158
+
159
+
.app-name:hover {
160
+
text-decoration: underline;
161
+
color: var(--text);
162
+
}
163
+
164
+
.app-name.invalid-link {
165
+
color: var(--text-light);
166
+
opacity: 0.5;
167
+
cursor: not-allowed;
168
+
}
169
+
170
+
.app-name.invalid-link:hover {
171
+
text-decoration: none;
172
+
color: var(--text-light);
173
+
}
+133
src/view/main.js
+133
src/view/main.js
···
1
+
// ============================================================================
2
+
// MAIN ENTRY POINT - View Page
3
+
// ============================================================================
4
+
5
+
import './styles.css';
6
+
import { state, urlParams, paramDid, paramHandle } from './state.js';
7
+
import {
8
+
resolveHandle,
9
+
resolveDid,
10
+
getPdsFromDidDoc,
11
+
getHandleFromDidDoc,
12
+
getProfile,
13
+
describeRepo
14
+
} from './atproto.js';
15
+
import { renderVisualization } from './visualization.js';
16
+
import { checkGuestbookState } from './guestbook-state.js';
17
+
import { initGuestbookUI } from './guestbook-ui.js';
18
+
import { initFirehoseUI } from './firehose.js';
19
+
20
+
// ============================================================================
21
+
// INITIALIZATION
22
+
// ============================================================================
23
+
24
+
async function init() {
25
+
const statusEl = document.getElementById('status');
26
+
27
+
try {
28
+
// Get DID from URL params
29
+
let did = paramDid;
30
+
31
+
// If handle provided, resolve to DID
32
+
if (!did && paramHandle) {
33
+
statusEl.textContent = 'resolving handle...';
34
+
did = await resolveHandle(paramHandle);
35
+
if (!did) {
36
+
statusEl.textContent = 'could not resolve handle';
37
+
return;
38
+
}
39
+
}
40
+
41
+
if (!did) {
42
+
statusEl.textContent = 'no identity specified';
43
+
return;
44
+
}
45
+
46
+
// Store DID in state
47
+
state.did = did;
48
+
49
+
statusEl.textContent = 'resolving identity...';
50
+
51
+
// Resolve DID document
52
+
const didDoc = await resolveDid(did);
53
+
if (!didDoc) {
54
+
statusEl.textContent = 'could not resolve DID';
55
+
return;
56
+
}
57
+
58
+
// Get PDS endpoint
59
+
const pds = getPdsFromDidDoc(didDoc);
60
+
if (!pds) {
61
+
statusEl.textContent = 'could not find PDS';
62
+
return;
63
+
}
64
+
65
+
// Get handle from DID doc
66
+
const handle = getHandleFromDidDoc(didDoc);
67
+
state.globalPds = pds;
68
+
state.globalHandle = handle || did;
69
+
70
+
// Update identity display
71
+
const handleEl = document.getElementById('handleDisplay');
72
+
if (handleEl) {
73
+
handleEl.textContent = `@${state.globalHandle}`;
74
+
}
75
+
76
+
statusEl.textContent = 'loading profile...';
77
+
78
+
// Get profile for avatar
79
+
const profile = await getProfile(did);
80
+
if (profile?.avatar) {
81
+
state.viewedAvatar = profile.avatar;
82
+
const avatarEl = document.getElementById('identityAvatar');
83
+
if (avatarEl) {
84
+
avatarEl.src = profile.avatar;
85
+
avatarEl.style.display = 'block';
86
+
}
87
+
}
88
+
89
+
statusEl.textContent = 'discovering apps...';
90
+
91
+
// Describe repo to get collections
92
+
const repoInfo = await describeRepo(pds, did);
93
+
if (!repoInfo?.collections) {
94
+
statusEl.textContent = 'could not load repository';
95
+
return;
96
+
}
97
+
98
+
// Group collections by namespace (first two parts)
99
+
const apps = {};
100
+
repoInfo.collections.forEach(collection => {
101
+
const parts = collection.split('.');
102
+
const namespace = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : collection;
103
+
if (!apps[namespace]) apps[namespace] = [];
104
+
apps[namespace].push(collection);
105
+
});
106
+
107
+
state.globalApps = apps;
108
+
109
+
// Hide status
110
+
statusEl.style.display = 'none';
111
+
112
+
// Render visualization
113
+
renderVisualization(apps, profile);
114
+
115
+
// Initialize UI components
116
+
initGuestbookUI();
117
+
initFirehoseUI();
118
+
119
+
// Check guestbook state
120
+
await checkGuestbookState();
121
+
122
+
} catch (error) {
123
+
console.error('Initialization error:', error);
124
+
statusEl.textContent = 'an error occurred';
125
+
}
126
+
}
127
+
128
+
// Start the app when DOM is ready
129
+
if (document.readyState === 'loading') {
130
+
document.addEventListener('DOMContentLoaded', init);
131
+
} else {
132
+
init();
133
+
}
+150
src/view/mst.css
+150
src/view/mst.css
···
1
+
.mst-canvas {
2
+
width: 100%;
3
+
height: 600px;
4
+
border: 1px solid var(--border);
5
+
border-radius: 4px;
6
+
background: var(--bg);
7
+
margin-top: 0.5rem;
8
+
}
9
+
10
+
.mst-info {
11
+
background: var(--bg);
12
+
border: 1px solid var(--border);
13
+
padding: 0.75rem;
14
+
border-radius: 4px;
15
+
margin-bottom: 0.75rem;
16
+
}
17
+
18
+
.mst-info p {
19
+
font-size: 0.65rem;
20
+
color: var(--text-lighter);
21
+
line-height: 1.5;
22
+
margin: 0;
23
+
}
24
+
25
+
.mst-node-modal {
26
+
position: fixed;
27
+
inset: 0;
28
+
background: rgba(0, 0, 0, 0.75);
29
+
display: flex;
30
+
align-items: center;
31
+
justify-content: center;
32
+
z-index: 3000;
33
+
padding: 1rem;
34
+
}
35
+
36
+
.mst-node-modal-content {
37
+
background: var(--surface);
38
+
border: 2px solid var(--border);
39
+
padding: 2rem;
40
+
border-radius: 4px;
41
+
max-width: 600px;
42
+
width: 100%;
43
+
max-height: 80vh;
44
+
overflow-y: auto;
45
+
position: relative;
46
+
}
47
+
48
+
.mst-node-close {
49
+
position: absolute;
50
+
top: 1rem;
51
+
right: 1rem;
52
+
width: 32px;
53
+
height: 32px;
54
+
border: 1px solid var(--border);
55
+
background: var(--bg);
56
+
color: var(--text-light);
57
+
cursor: pointer;
58
+
display: flex;
59
+
align-items: center;
60
+
justify-content: center;
61
+
font-size: 1.2rem;
62
+
line-height: 1;
63
+
transition: all 0.2s ease;
64
+
border-radius: 2px;
65
+
}
66
+
67
+
.mst-node-close:hover {
68
+
background: var(--surface-hover);
69
+
border-color: var(--text-light);
70
+
color: var(--text);
71
+
}
72
+
73
+
.mst-node-modal-content h3 {
74
+
margin-bottom: 1rem;
75
+
font-size: 0.9rem;
76
+
color: var(--text);
77
+
}
78
+
79
+
.mst-node-info {
80
+
background: var(--bg);
81
+
border: 1px solid var(--border);
82
+
padding: 0.75rem;
83
+
border-radius: 4px;
84
+
margin-bottom: 1rem;
85
+
}
86
+
87
+
.mst-node-field {
88
+
display: flex;
89
+
gap: 0.5rem;
90
+
margin-bottom: 0.5rem;
91
+
font-size: 0.65rem;
92
+
}
93
+
94
+
.mst-node-field:last-child {
95
+
margin-bottom: 0;
96
+
}
97
+
98
+
.mst-node-label {
99
+
color: var(--text-light);
100
+
font-weight: 500;
101
+
min-width: 40px;
102
+
}
103
+
104
+
.mst-node-value {
105
+
color: var(--text);
106
+
word-break: break-all;
107
+
font-family: monospace;
108
+
}
109
+
110
+
.mst-node-explanation {
111
+
background: var(--bg);
112
+
border: 1px solid var(--border);
113
+
padding: 0.75rem;
114
+
border-radius: 4px;
115
+
margin-bottom: 1rem;
116
+
}
117
+
118
+
.mst-node-explanation p {
119
+
font-size: 0.65rem;
120
+
color: var(--text-lighter);
121
+
line-height: 1.5;
122
+
margin: 0;
123
+
}
124
+
125
+
.mst-node-data {
126
+
background: var(--bg);
127
+
border: 1px solid var(--border);
128
+
border-radius: 4px;
129
+
overflow: hidden;
130
+
}
131
+
132
+
.mst-node-data-header {
133
+
font-size: 0.65rem;
134
+
color: var(--text-light);
135
+
padding: 0.5rem 0.75rem;
136
+
border-bottom: 1px solid var(--border);
137
+
font-weight: 500;
138
+
}
139
+
140
+
.mst-node-data pre {
141
+
margin: 0;
142
+
padding: 0.75rem;
143
+
font-size: 0.625rem;
144
+
color: var(--text);
145
+
white-space: pre-wrap;
146
+
word-break: break-word;
147
+
line-height: 1.5;
148
+
max-height: 300px;
149
+
overflow-y: auto;
150
+
}
+353
src/view/mst.js
+353
src/view/mst.js
···
1
+
// ============================================================================
2
+
// MST (Merkle Search Tree) VISUALIZATION
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
import { listRecords, escapeHtml } from './atproto.js';
7
+
8
+
const MST_MAX_DEPTH = 5;
9
+
const MST_FETCH_LIMIT = 100;
10
+
11
+
function calculateKeyDepth(key) {
12
+
// Hash the key using a simple hash function
13
+
let hash = 0;
14
+
for (let i = 0; i < key.length; i++) {
15
+
hash = (hash << 5) - hash + key.charCodeAt(i);
16
+
hash = hash & hash; // Convert to 32bit integer
17
+
}
18
+
19
+
// Count leading zero pairs in binary representation
20
+
const absHash = Math.abs(hash) >>> 0;
21
+
const binary = absHash.toString(2).padStart(32, '0');
22
+
23
+
let depth = 0;
24
+
for (let i = 0; i < binary.length - 1; i += 2) {
25
+
if (binary[i] === '0' && binary[i + 1] === '0') {
26
+
depth++;
27
+
} else {
28
+
break;
29
+
}
30
+
}
31
+
32
+
return Math.min(depth, MST_MAX_DEPTH);
33
+
}
34
+
35
+
function buildMST(records) {
36
+
const recordCount = records.length;
37
+
38
+
// Extract and sort by key (rkey from URI)
39
+
let nodes = records.map(r => {
40
+
const key = r.uri.split('/').pop() || '';
41
+
return {
42
+
key: key,
43
+
cid: r.cid,
44
+
uri: r.uri,
45
+
value: r.value,
46
+
depth: calculateKeyDepth(key),
47
+
children: []
48
+
};
49
+
});
50
+
51
+
nodes.sort((a, b) => a.key.localeCompare(b.key));
52
+
53
+
// Build tree structure
54
+
const root = buildTree(nodes);
55
+
56
+
return { root, recordCount };
57
+
}
58
+
59
+
function buildTree(nodes) {
60
+
if (nodes.length === 0) {
61
+
return {
62
+
key: 'root',
63
+
cid: null,
64
+
uri: null,
65
+
value: null,
66
+
depth: -1,
67
+
children: []
68
+
};
69
+
}
70
+
71
+
// Group by depth
72
+
const byDepth = {};
73
+
for (const node of nodes) {
74
+
if (!byDepth[node.depth]) byDepth[node.depth] = [];
75
+
byDepth[node.depth].push(node);
76
+
}
77
+
78
+
const depths = Object.keys(byDepth).map(Number).sort((a, b) => a - b);
79
+
80
+
// Build tree bottom-up
81
+
let currentLevel = byDepth[depths[depths.length - 1]] || [];
82
+
83
+
// Work backwards through depths
84
+
for (let i = depths.length - 2; i >= 0; i--) {
85
+
const depth = depths[i];
86
+
const parentNodes = byDepth[depth] || [];
87
+
88
+
if (parentNodes.length === 0) continue;
89
+
90
+
// Distribute children to parents
91
+
const childrenPerParent = Math.ceil(currentLevel.length / parentNodes.length);
92
+
93
+
for (let j = 0; j < parentNodes.length; j++) {
94
+
const start = j * childrenPerParent;
95
+
const end = Math.min((j + 1) * childrenPerParent, currentLevel.length);
96
+
if (start < currentLevel.length) {
97
+
parentNodes[j].children = currentLevel.slice(start, end);
98
+
}
99
+
}
100
+
101
+
currentLevel = parentNodes;
102
+
}
103
+
104
+
// Create root and attach top-level nodes
105
+
return {
106
+
key: 'root',
107
+
cid: null,
108
+
uri: null,
109
+
value: null,
110
+
depth: -1,
111
+
children: currentLevel
112
+
};
113
+
}
114
+
115
+
export async function loadMSTStructure(lexicon, containerView) {
116
+
try {
117
+
// Fetch records for MST building
118
+
const data = await listRecords(state.globalPds, state.did, lexicon, MST_FETCH_LIMIT);
119
+
120
+
if (!data?.records?.length) {
121
+
containerView.innerHTML = '<div class="mst-info"><p>no records to visualize</p></div>';
122
+
return;
123
+
}
124
+
125
+
const { root, recordCount } = buildMST(data.records);
126
+
127
+
containerView.innerHTML = `
128
+
<div class="mst-info">
129
+
<p>this shows the <a href="https://atproto.com/specs/repository#mst-structure" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Merkle Search Tree (MST)</a> structure used to store your ${recordCount} record${recordCount !== 1 ? 's' : ''} in your repository. records are organized by their <a href="https://atproto.com/specs/record-key#record-key-type-tid" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">TIDs</a> (timestamp identifiers), which determines how they're arranged in the tree.</p>
130
+
</div>
131
+
<canvas class="mst-canvas" id="mstCanvas-${Date.now()}"></canvas>
132
+
`;
133
+
134
+
setTimeout(() => {
135
+
const canvas = containerView.querySelector('.mst-canvas');
136
+
if (canvas) {
137
+
renderMSTTree(canvas, root);
138
+
}
139
+
}, 50);
140
+
141
+
} catch (e) {
142
+
console.error('Error loading MST structure:', e);
143
+
containerView.innerHTML = '<div class="mst-info"><p>error loading structure</p></div>';
144
+
}
145
+
}
146
+
147
+
function renderMSTTree(canvas, tree) {
148
+
const ctx = canvas.getContext('2d');
149
+
const width = canvas.width = canvas.offsetWidth;
150
+
const height = canvas.height = canvas.offsetHeight;
151
+
152
+
const layout = layoutTree(tree, width, height);
153
+
154
+
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border').trim();
155
+
const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text').trim();
156
+
const surfaceColor = getComputedStyle(document.documentElement).getPropertyValue('--surface').trim();
157
+
const surfaceHoverColor = getComputedStyle(document.documentElement).getPropertyValue('--surface-hover').trim();
158
+
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim();
159
+
160
+
let hoveredNode = null;
161
+
162
+
function draw() {
163
+
ctx.clearRect(0, 0, width, height);
164
+
165
+
// Draw connections first
166
+
layout.forEach(node => {
167
+
if (node.children) {
168
+
node.children.forEach(child => {
169
+
ctx.beginPath();
170
+
ctx.moveTo(node.x, node.y);
171
+
ctx.lineTo(child.x, child.y);
172
+
ctx.strokeStyle = borderColor;
173
+
ctx.lineWidth = 1;
174
+
ctx.stroke();
175
+
});
176
+
}
177
+
});
178
+
179
+
// Draw nodes
180
+
layout.forEach(node => {
181
+
const isRoot = node.depth === -1;
182
+
const isLeaf = !node.children || node.children.length === 0;
183
+
const isHovered = hoveredNode === node;
184
+
185
+
ctx.beginPath();
186
+
ctx.arc(node.x, node.y, isRoot ? 12 : 8, 0, Math.PI * 2);
187
+
188
+
ctx.fillStyle = isRoot ? textColor : isLeaf ? surfaceHoverColor : surfaceColor;
189
+
ctx.fill();
190
+
191
+
ctx.strokeStyle = isHovered ? textColor : borderColor;
192
+
ctx.lineWidth = isRoot ? 2 : isHovered ? 2 : 1;
193
+
ctx.stroke();
194
+
});
195
+
196
+
// Draw label for hovered node
197
+
if (hoveredNode && hoveredNode.key && hoveredNode.key !== 'root') {
198
+
const padding = 6;
199
+
const fontSize = 10;
200
+
ctx.font = `${fontSize}px monospace`;
201
+
const textWidth = ctx.measureText(hoveredNode.key).width;
202
+
203
+
const tooltipX = hoveredNode.x;
204
+
const tooltipY = hoveredNode.y - 20;
205
+
const boxWidth = textWidth + padding * 2;
206
+
const boxHeight = fontSize + padding * 2;
207
+
208
+
ctx.fillStyle = bgColor;
209
+
ctx.fillRect(tooltipX - boxWidth / 2, tooltipY - boxHeight / 2, boxWidth, boxHeight);
210
+
211
+
ctx.strokeStyle = borderColor;
212
+
ctx.lineWidth = 1;
213
+
ctx.strokeRect(tooltipX - boxWidth / 2, tooltipY - boxHeight / 2, boxWidth, boxHeight);
214
+
215
+
ctx.fillStyle = textColor;
216
+
ctx.textAlign = 'center';
217
+
ctx.textBaseline = 'middle';
218
+
ctx.fillText(hoveredNode.key, tooltipX, tooltipY);
219
+
}
220
+
}
221
+
222
+
canvas.addEventListener('mousemove', (e) => {
223
+
const rect = canvas.getBoundingClientRect();
224
+
const mouseX = e.clientX - rect.left;
225
+
const mouseY = e.clientY - rect.top;
226
+
227
+
let foundNode = null;
228
+
for (const node of layout) {
229
+
const isRoot = node.depth === -1;
230
+
const radius = isRoot ? 12 : 8;
231
+
const dist = Math.sqrt((mouseX - node.x) ** 2 + (mouseY - node.y) ** 2);
232
+
if (dist <= radius) {
233
+
foundNode = node;
234
+
break;
235
+
}
236
+
}
237
+
238
+
if (foundNode !== hoveredNode) {
239
+
hoveredNode = foundNode;
240
+
canvas.style.cursor = hoveredNode ? 'pointer' : 'default';
241
+
draw();
242
+
}
243
+
});
244
+
245
+
canvas.addEventListener('mouseleave', () => {
246
+
if (hoveredNode) {
247
+
hoveredNode = null;
248
+
canvas.style.cursor = 'default';
249
+
draw();
250
+
}
251
+
});
252
+
253
+
canvas.addEventListener('click', (e) => {
254
+
if (hoveredNode && hoveredNode.key && hoveredNode.key !== 'root') {
255
+
showNodeModal(hoveredNode);
256
+
}
257
+
});
258
+
259
+
draw();
260
+
}
261
+
262
+
function layoutTree(tree, width, height) {
263
+
const nodes = [];
264
+
const padding = 40;
265
+
const availableHeight = height - padding * 2;
266
+
267
+
// Calculate max depth
268
+
const depthCounts = {};
269
+
function countDepths(node, depth) {
270
+
if (!depthCounts[depth]) depthCounts[depth] = 0;
271
+
depthCounts[depth]++;
272
+
if (node.children) {
273
+
node.children.forEach(child => countDepths(child, depth + 1));
274
+
}
275
+
}
276
+
countDepths(tree, 0);
277
+
278
+
const maxDepth = Math.max(...Object.keys(depthCounts).map(Number));
279
+
const verticalSpacing = availableHeight / (maxDepth + 1);
280
+
281
+
function traverse(node, depth, minX, maxX) {
282
+
const x = (minX + maxX) / 2;
283
+
const y = padding + verticalSpacing * depth;
284
+
285
+
const layoutNode = { ...node, x, y };
286
+
nodes.push(layoutNode);
287
+
288
+
if (node.children && node.children.length > 0) {
289
+
layoutNode.children = [];
290
+
const childWidth = (maxX - minX) / node.children.length;
291
+
292
+
node.children.forEach((child, idx) => {
293
+
const childMinX = minX + childWidth * idx;
294
+
const childMaxX = minX + childWidth * (idx + 1);
295
+
const childLayout = traverse(child, depth + 1, childMinX, childMaxX);
296
+
layoutNode.children.push(childLayout);
297
+
});
298
+
}
299
+
300
+
return layoutNode;
301
+
}
302
+
303
+
traverse(tree, 0, padding, width - padding);
304
+
return nodes;
305
+
}
306
+
307
+
function showNodeModal(node) {
308
+
const modal = document.createElement('div');
309
+
modal.className = 'mst-node-modal';
310
+
modal.innerHTML = `
311
+
<div class="mst-node-modal-content">
312
+
<button class="mst-node-close">x</button>
313
+
<h3>record in MST</h3>
314
+
<div class="mst-node-info">
315
+
<div class="mst-node-field">
316
+
<span class="mst-node-label">TID:</span>
317
+
<span class="mst-node-value">${node.key}</span>
318
+
</div>
319
+
<div class="mst-node-field">
320
+
<span class="mst-node-label">CID:</span>
321
+
<span class="mst-node-value">${node.cid || 'N/A'}</span>
322
+
</div>
323
+
${node.uri ? `
324
+
<div class="mst-node-field">
325
+
<span class="mst-node-label">URI:</span>
326
+
<span class="mst-node-value">${node.uri}</span>
327
+
</div>
328
+
` : ''}
329
+
</div>
330
+
<div class="mst-node-explanation">
331
+
<p>this is a leaf node in your Merkle Search Tree. the TID (timestamp identifier) determines its position in the tree. records are sorted by TID, making range queries efficient.</p>
332
+
</div>
333
+
${node.value ? `
334
+
<div class="mst-node-data">
335
+
<div class="mst-node-data-header">record data</div>
336
+
<pre>${escapeHtml(JSON.stringify(node.value, null, 2))}</pre>
337
+
</div>
338
+
` : ''}
339
+
</div>
340
+
`;
341
+
342
+
document.body.appendChild(modal);
343
+
344
+
modal.querySelector('.mst-node-close').addEventListener('click', () => {
345
+
modal.remove();
346
+
});
347
+
348
+
modal.addEventListener('click', (e) => {
349
+
if (e.target === modal) {
350
+
modal.remove();
351
+
}
352
+
});
353
+
}
+156
src/view/particles.js
+156
src/view/particles.js
···
1
+
// ============================================================================
2
+
// FIREHOSE PARTICLE ANIMATION
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
7
+
export class FirehoseParticle {
8
+
constructor(startX, startY, endX, endY, color, metadata) {
9
+
this.x = startX;
10
+
this.y = startY;
11
+
this.startX = startX;
12
+
this.startY = startY;
13
+
this.endX = endX;
14
+
this.endY = endY;
15
+
this.color = color;
16
+
this.metadata = metadata;
17
+
this.progress = 0;
18
+
this.speed = 0.008;
19
+
this.size = 6;
20
+
this.glowSize = 14;
21
+
}
22
+
23
+
update() {
24
+
if (this.progress < 1) {
25
+
this.progress += this.speed;
26
+
const eased = 1 - Math.pow(1 - this.progress, 3);
27
+
this.x = this.startX + (this.endX - this.startX) * eased;
28
+
this.y = this.startY + (this.endY - this.startY) * eased;
29
+
}
30
+
return this.progress < 1;
31
+
}
32
+
33
+
draw(ctx) {
34
+
const fadeIn = Math.min(this.progress * 4, 1);
35
+
const fadeOut = this.progress > 0.8 ? 1 - ((this.progress - 0.8) / 0.2) : 1;
36
+
const opacity = Math.min(fadeIn, fadeOut);
37
+
38
+
ctx.beginPath();
39
+
ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2);
40
+
const gradient = ctx.createRadialGradient(
41
+
this.x, this.y, 0,
42
+
this.x, this.y, this.glowSize
43
+
);
44
+
gradient.addColorStop(0, this.color + Math.floor(opacity * 60).toString(16).padStart(2, '0'));
45
+
gradient.addColorStop(0.5, this.color + Math.floor(opacity * 30).toString(16).padStart(2, '0'));
46
+
gradient.addColorStop(1, this.color + '00');
47
+
ctx.fillStyle = gradient;
48
+
ctx.fill();
49
+
50
+
ctx.beginPath();
51
+
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
52
+
ctx.fillStyle = this.color + Math.floor(opacity * 180).toString(16).padStart(2, '0');
53
+
ctx.fill();
54
+
}
55
+
}
56
+
57
+
export function initFirehoseCanvas() {
58
+
if (state.firehoseCanvas) return;
59
+
60
+
state.firehoseCanvas = document.createElement('canvas');
61
+
state.firehoseCanvas.id = 'firehoseCanvas';
62
+
state.firehoseCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:50;';
63
+
state.firehoseCanvas.width = window.innerWidth;
64
+
state.firehoseCanvas.height = window.innerHeight;
65
+
document.body.appendChild(state.firehoseCanvas);
66
+
state.firehoseCtx = state.firehoseCanvas.getContext('2d');
67
+
68
+
window.addEventListener('resize', () => {
69
+
if (state.firehoseCanvas) {
70
+
state.firehoseCanvas.width = window.innerWidth;
71
+
state.firehoseCanvas.height = window.innerHeight;
72
+
}
73
+
});
74
+
}
75
+
76
+
function getParticleColor() {
77
+
const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text').trim();
78
+
if (textColor.startsWith('rgb')) {
79
+
const match = textColor.match(/(\d+),\s*(\d+),\s*(\d+)/);
80
+
if (match) {
81
+
const r = parseInt(match[1]);
82
+
const g = parseInt(match[2]);
83
+
const b = parseInt(match[3]);
84
+
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
85
+
}
86
+
}
87
+
return '#8ba4b8';
88
+
}
89
+
90
+
export function createFirehoseParticle(event) {
91
+
const identity = document.querySelector('.identity');
92
+
if (!identity) return;
93
+
94
+
const identityRect = identity.getBoundingClientRect();
95
+
const endX = identityRect.left + identityRect.width / 2;
96
+
const endY = identityRect.top + identityRect.height / 2;
97
+
98
+
// Find the app circle for this event
99
+
const appCircle = document.querySelector(`[data-namespace="${event.namespace}"]`);
100
+
101
+
let startX, startY;
102
+
if (appCircle) {
103
+
const appRect = appCircle.getBoundingClientRect();
104
+
startX = appRect.left + appRect.width / 2;
105
+
startY = appRect.top + appRect.height / 2;
106
+
} else {
107
+
startX = endX;
108
+
startY = endY;
109
+
}
110
+
111
+
const particle = new FirehoseParticle(
112
+
startX, startY,
113
+
endX, endY,
114
+
getParticleColor(),
115
+
{ action: event.action, collection: event.collection, namespace: event.namespace }
116
+
);
117
+
state.firehoseParticles.push(particle);
118
+
}
119
+
120
+
function pulseIdentity() {
121
+
const identity = document.querySelector('.identity');
122
+
if (!identity) return;
123
+
identity.classList.add('pulse');
124
+
setTimeout(() => identity.classList.remove('pulse'), 300);
125
+
}
126
+
127
+
export function animateFirehoseParticles() {
128
+
if (!state.firehoseCtx) return;
129
+
130
+
state.firehoseCtx.clearRect(0, 0, state.firehoseCanvas.width, state.firehoseCanvas.height);
131
+
132
+
state.firehoseParticles = state.firehoseParticles.filter(particle => {
133
+
const alive = particle.update();
134
+
if (alive) {
135
+
particle.draw(state.firehoseCtx);
136
+
} else {
137
+
pulseIdentity();
138
+
}
139
+
return alive;
140
+
});
141
+
142
+
state.firehoseAnimationId = requestAnimationFrame(animateFirehoseParticles);
143
+
}
144
+
145
+
export function cleanupFirehoseCanvas() {
146
+
if (state.firehoseAnimationId) {
147
+
cancelAnimationFrame(state.firehoseAnimationId);
148
+
state.firehoseAnimationId = null;
149
+
}
150
+
state.firehoseParticles = [];
151
+
if (state.firehoseCanvas) {
152
+
state.firehoseCanvas.remove();
153
+
state.firehoseCanvas = null;
154
+
state.firehoseCtx = null;
155
+
}
156
+
}
+27
src/view/state.js
+27
src/view/state.js
···
1
+
// Shared state for the view page
2
+
3
+
export const state = {
4
+
did: null,
5
+
globalPds: null,
6
+
globalHandle: null,
7
+
globalApps: null,
8
+
hiddenApps: new Set(),
9
+
invalidApps: new Set(),
10
+
pageOwnerHasSigned: false,
11
+
viewedAvatar: null,
12
+
13
+
// Firehose animation state
14
+
firehoseParticles: [],
15
+
firehoseCanvas: null,
16
+
firehoseCtx: null,
17
+
firehoseAnimationId: null,
18
+
19
+
// WebSocket state
20
+
jetstreamWs: null,
21
+
isWatchingLive: false
22
+
};
23
+
24
+
// URL params
25
+
export const urlParams = new URLSearchParams(window.location.search);
26
+
export const paramDid = urlParams.get('did');
27
+
export const paramHandle = urlParams.get('handle');
+9
src/view/styles.css
+9
src/view/styles.css
+410
src/view/visualization.js
+410
src/view/visualization.js
···
1
+
// ============================================================================
2
+
// VISUALIZATION
3
+
// ============================================================================
4
+
5
+
import { state } from './state.js';
6
+
import {
7
+
applyDomainRedirect,
8
+
escapeHtml,
9
+
fetchAppAvatars,
10
+
validateAppUrls,
11
+
listRecords
12
+
} from './atproto.js';
13
+
import { initFilterPanel, repositionAppCircles } from './filters.js';
14
+
import { loadMSTStructure } from './mst.js';
15
+
16
+
export function renderVisualization(apps, profile) {
17
+
const field = document.getElementById('field');
18
+
field.innerHTML = '';
19
+
field.classList.remove('loading');
20
+
21
+
const appNames = Object.keys(apps).sort();
22
+
const appCount = appNames.length;
23
+
const allCollections = Object.values(apps).flat();
24
+
25
+
// Hide labels on mobile when there are too many apps
26
+
const isMobileView = window.innerWidth < 768;
27
+
if (isMobileView && appCount > 20) {
28
+
field.classList.add('many-apps');
29
+
}
30
+
31
+
// Calculate dimensions
32
+
const vmin = Math.min(window.innerWidth, window.innerHeight);
33
+
const isMobile = window.innerWidth < 768;
34
+
35
+
let circleSize, radius;
36
+
if (isMobile) {
37
+
if (appCount <= 5) {
38
+
circleSize = Math.min(60, vmin * 0.08);
39
+
radius = vmin * 0.38;
40
+
} else if (appCount <= 10) {
41
+
circleSize = Math.min(50, vmin * 0.07);
42
+
radius = vmin * 0.4;
43
+
} else if (appCount <= 20) {
44
+
circleSize = Math.min(40, vmin * 0.055);
45
+
radius = vmin * 0.42;
46
+
} else {
47
+
circleSize = Math.min(32, vmin * 0.045);
48
+
radius = vmin * 0.44;
49
+
}
50
+
circleSize = Math.max(circleSize, 28);
51
+
radius = Math.max(radius, 120);
52
+
} else {
53
+
if (appCount <= 5) {
54
+
circleSize = Math.min(70, vmin * 0.1);
55
+
} else if (appCount <= 10) {
56
+
circleSize = Math.min(60, vmin * 0.09);
57
+
} else if (appCount <= 20) {
58
+
circleSize = Math.min(50, vmin * 0.07);
59
+
} else {
60
+
circleSize = Math.min(40, vmin * 0.06);
61
+
}
62
+
circleSize = Math.max(circleSize, 35);
63
+
radius = Math.max(vmin * 0.35, 150);
64
+
}
65
+
66
+
const centerX = window.innerWidth / 2;
67
+
const centerY = window.innerHeight / 2;
68
+
69
+
state.globalApps._circleSize = circleSize;
70
+
71
+
// Create app circles
72
+
const appDivs = appNames.map((namespace, i) => {
73
+
const angle = (i / appNames.length) * 2 * Math.PI - Math.PI / 2;
74
+
const circleOffset = circleSize / 2;
75
+
const x = centerX + radius * Math.cos(angle) - circleOffset;
76
+
const y = centerY + radius * Math.sin(angle) - circleOffset;
77
+
78
+
const div = document.createElement('div');
79
+
div.className = 'app-view';
80
+
div.style.left = `${x}px`;
81
+
div.style.top = `${y}px`;
82
+
83
+
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
84
+
const rawDisplayName = namespace.split('.').reverse().join('.');
85
+
const displayName = applyDomainRedirect(rawDisplayName);
86
+
const url = `https://${displayName}`;
87
+
88
+
div.innerHTML = `
89
+
<div class="app-circle" data-namespace="${namespace}" style="width: ${circleSize}px; height: ${circleSize}px; font-size: ${circleSize * 0.4}px;">${firstLetter}</div>
90
+
<a href="${url}" target="_blank" rel="noopener noreferrer" class="app-name" data-url="${url}">${displayName} ↗</a>
91
+
`;
92
+
93
+
div.addEventListener('click', () => showAppDetail(namespace, apps[namespace], displayName, url));
94
+
95
+
return { div, namespace };
96
+
});
97
+
98
+
// Add all divs to field
99
+
appDivs.forEach(({ div }) => field.appendChild(div));
100
+
101
+
// Fetch avatars asynchronously
102
+
fetchAppAvatars(appNames).then(avatarMap => {
103
+
appDivs.forEach(({ div, namespace }) => {
104
+
const avatarUrl = avatarMap[namespace];
105
+
if (avatarUrl) {
106
+
const circle = div.querySelector('.app-circle');
107
+
circle.innerHTML = `<img src="${avatarUrl}" class="app-logo" alt="${namespace}" />`;
108
+
}
109
+
});
110
+
});
111
+
112
+
// Validate app URLs (client-side check via image load)
113
+
validateAppUrls(appDivs);
114
+
115
+
// Set up identity click handler
116
+
setupIdentityClickHandler(allCollections, appCount, profile);
117
+
118
+
// Set up filter panel
119
+
initFilterPanel();
120
+
121
+
// Handle window resize
122
+
let resizeTimeout;
123
+
window.addEventListener('resize', () => {
124
+
clearTimeout(resizeTimeout);
125
+
resizeTimeout = setTimeout(repositionAppCircles, 50);
126
+
});
127
+
}
128
+
129
+
function setupIdentityClickHandler(allCollections, appCount, profile) {
130
+
const pdsHost = state.globalPds.replace('https://', '').replace('http://', '');
131
+
132
+
document.querySelector('.identity').addEventListener('click', () => {
133
+
const detail = document.getElementById('detail');
134
+
135
+
detail.innerHTML = `
136
+
<button class="detail-close" id="detailClose">x</button>
137
+
<h3>your personal data server</h3>
138
+
<div class="subtitle">where your social data lives</div>
139
+
140
+
<div class="stats-box">
141
+
<div class="stat">
142
+
<div class="stat-value">${allCollections.length}</div>
143
+
<div class="stat-label">record types</div>
144
+
</div>
145
+
<div class="stat">
146
+
<div class="stat-value">${appCount}</div>
147
+
<div class="stat-label">apps</div>
148
+
</div>
149
+
</div>
150
+
151
+
<div class="ownership-box yours">
152
+
<div class="ownership-header">your pds location</div>
153
+
<div class="ownership-text">your <a href="https://atproto.com/guides/data-repos" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data Server</a> is hosted at <a href="${state.globalPds}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;"><strong>${pdsHost}</strong></a>. all your posts, likes, and follows are stored here. apps like bluesky just connect to it.</div>
154
+
</div>
155
+
156
+
<div class="ownership-box">
157
+
<div class="ownership-header">explore your data</div>
158
+
<div class="ownership-text">want to see everything stored on your PDS? check out <a href="https://pdsls.dev/${pdsHost}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">pdsls.dev/${pdsHost}</a> - a tool for browsing all the records in your repository.</div>
159
+
</div>
160
+
161
+
<a href="https://bsky.app/profile/${state.globalHandle}" target="_blank" rel="noopener noreferrer" class="tree-item" style="text-decoration: none; display: block; margin-top: 1rem;">
162
+
<div class="tree-item-header">
163
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
164
+
<svg width="16" height="16" viewBox="0 0 600 530" fill="none" xmlns="http://www.w3.org/2000/svg">
165
+
<path d="M135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="var(--text)" />
166
+
</svg>
167
+
<span style="color: var(--text-light);">view profile on bluesky</span>
168
+
</div>
169
+
<span style="font-size: 0.6rem; color: var(--text);">↗</span>
170
+
</div>
171
+
</a>
172
+
173
+
<div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);">
174
+
<div style="font-size: 0.65rem; color: var(--text-light); margin-bottom: 0.5rem;">technical details</div>
175
+
<div class="tree-item">
176
+
<div class="tree-item-header">
177
+
<span style="color: var(--text-light);">did</span>
178
+
<span style="font-size: 0.55rem; color: var(--text);">${state.did}</span>
179
+
</div>
180
+
</div>
181
+
<div class="tree-item">
182
+
<div class="tree-item-header">
183
+
<span style="color: var(--text-light);">handle</span>
184
+
<span style="font-size: 0.6rem; color: var(--text);">@${state.globalHandle}</span>
185
+
</div>
186
+
</div>
187
+
</div>
188
+
`;
189
+
detail.classList.add('visible');
190
+
191
+
document.getElementById('detailClose').addEventListener('click', (e) => {
192
+
e.stopPropagation();
193
+
detail.classList.remove('visible');
194
+
});
195
+
});
196
+
}
197
+
198
+
async function showAppDetail(namespace, collections, displayName, appUrl) {
199
+
const detail = document.getElementById('detail');
200
+
201
+
let html = `
202
+
<button class="detail-close" id="detailClose">x</button>
203
+
<h3><a href="${appUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);">${displayName} ↗</a></h3>
204
+
<div class="subtitle">records stored in your <a href="https://atproto.com/guides/self-hosting" target="_blank" rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">PDS</a>:</div>
205
+
`;
206
+
207
+
if (collections && collections.length > 0) {
208
+
const grouped = {};
209
+
collections.forEach(lexicon => {
210
+
const parts = lexicon.split('.');
211
+
const subNamespace = parts.slice(2).join('.');
212
+
const firstPart = parts[2] || lexicon;
213
+
214
+
if (!grouped[firstPart]) grouped[firstPart] = [];
215
+
grouped[firstPart].push({ lexicon, subNamespace });
216
+
});
217
+
218
+
Object.keys(grouped).sort().forEach(group => {
219
+
const items = grouped[group];
220
+
221
+
if (items.length === 1 && items[0].subNamespace === group) {
222
+
html += `
223
+
<div class="tree-item" data-lexicon="${items[0].lexicon}">
224
+
<div class="tree-item-header">
225
+
<span>${group}</span>
226
+
<span class="tree-item-count">loading...</span>
227
+
</div>
228
+
</div>
229
+
`;
230
+
} else {
231
+
html += `<div style="margin-bottom: 0.75rem;">`;
232
+
html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${group}</div>`;
233
+
234
+
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {
235
+
const itemDisplayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
236
+
html += `
237
+
<div class="tree-item" data-lexicon="${item.lexicon}" style="margin-left: 0.75rem;">
238
+
<div class="tree-item-header">
239
+
<span>${itemDisplayName}</span>
240
+
<span class="tree-item-count">loading...</span>
241
+
</div>
242
+
</div>
243
+
`;
244
+
});
245
+
html += `</div>`;
246
+
}
247
+
});
248
+
} else {
249
+
html += `<div class="tree-item">no collections found</div>`;
250
+
}
251
+
252
+
detail.innerHTML = html;
253
+
detail.classList.add('visible');
254
+
255
+
document.getElementById('detailClose').addEventListener('click', (e) => {
256
+
e.stopPropagation();
257
+
detail.classList.remove('visible');
258
+
});
259
+
260
+
// Fetch record counts
261
+
if (collections) {
262
+
for (const lexicon of collections) {
263
+
const data = await listRecords(state.globalPds, state.did, lexicon, 1);
264
+
const item = detail.querySelector(`[data-lexicon="${lexicon}"]`);
265
+
if (item) {
266
+
const countSpan = item.querySelector('.tree-item-count');
267
+
countSpan.textContent = data?.records?.length > 0 ? 'has records' : 'empty';
268
+
}
269
+
}
270
+
}
271
+
272
+
// Add click handlers for expanding collections
273
+
detail.querySelectorAll('.tree-item[data-lexicon]').forEach(item => {
274
+
item.addEventListener('click', (e) => {
275
+
e.stopPropagation();
276
+
expandCollection(item);
277
+
});
278
+
});
279
+
}
280
+
281
+
async function expandCollection(item) {
282
+
const lexicon = item.dataset.lexicon;
283
+
const existingContent = item.querySelector('.collection-content');
284
+
285
+
if (existingContent) {
286
+
existingContent.remove();
287
+
return;
288
+
}
289
+
290
+
const contentDiv = document.createElement('div');
291
+
contentDiv.className = 'collection-content';
292
+
contentDiv.innerHTML = `
293
+
<div class="collection-view-content">
294
+
<div class="collection-view records-view active">
295
+
<div class="loading">loading records...</div>
296
+
</div>
297
+
<div class="collection-view structure-view">
298
+
<div class="loading">loading structure...</div>
299
+
</div>
300
+
</div>
301
+
`;
302
+
item.appendChild(contentDiv);
303
+
304
+
const recordsView = contentDiv.querySelector('.records-view');
305
+
const structureView = contentDiv.querySelector('.structure-view');
306
+
const data = await listRecords(state.globalPds, state.did, lexicon, 10);
307
+
308
+
// Add tabs if there are enough records for MST view
309
+
const hasEnoughRecords = data?.records?.length >= 5;
310
+
if (hasEnoughRecords) {
311
+
const tabsHtml = `
312
+
<div class="collection-tabs">
313
+
<button class="collection-tab active" data-tab="records">records</button>
314
+
<button class="collection-tab" data-tab="structure">mst</button>
315
+
</div>
316
+
`;
317
+
contentDiv.insertAdjacentHTML('afterbegin', tabsHtml);
318
+
319
+
// Tab switching logic
320
+
contentDiv.querySelectorAll('.collection-tab').forEach(tab => {
321
+
tab.addEventListener('click', (e) => {
322
+
e.stopPropagation();
323
+
const tabName = tab.dataset.tab;
324
+
325
+
contentDiv.querySelectorAll('.collection-tab').forEach(t => t.classList.remove('active'));
326
+
tab.classList.add('active');
327
+
328
+
contentDiv.querySelectorAll('.collection-view').forEach(v => v.classList.remove('active'));
329
+
if (tabName === 'records') {
330
+
recordsView.classList.add('active');
331
+
} else if (tabName === 'structure') {
332
+
structureView.classList.add('active');
333
+
if (structureView.querySelector('.loading')) {
334
+
loadMSTStructure(lexicon, structureView);
335
+
}
336
+
}
337
+
});
338
+
});
339
+
}
340
+
341
+
if (data?.records?.length > 0) {
342
+
let recordsHtml = data.records.map((record, idx) => {
343
+
const json = JSON.stringify(record.value, null, 2);
344
+
return `
345
+
<div class="record">
346
+
<div class="record-header">
347
+
<span class="record-label">record</span>
348
+
<button class="copy-btn" data-content="${encodeURIComponent(json)}">copy</button>
349
+
</div>
350
+
<div class="record-content">
351
+
<pre>${escapeHtml(json)}</pre>
352
+
</div>
353
+
</div>
354
+
`;
355
+
}).join('');
356
+
357
+
if (data.cursor) {
358
+
recordsHtml += `<button class="load-more" data-cursor="${data.cursor}" data-lexicon="${lexicon}">load more</button>`;
359
+
}
360
+
361
+
recordsView.innerHTML = recordsHtml;
362
+
363
+
// Event delegation for copy and load more
364
+
recordsView.addEventListener('click', async (e) => {
365
+
if (e.target.classList.contains('copy-btn')) {
366
+
e.stopPropagation();
367
+
const content = decodeURIComponent(e.target.dataset.content);
368
+
await navigator.clipboard.writeText(content);
369
+
e.target.textContent = 'copied!';
370
+
setTimeout(() => { e.target.textContent = 'copy'; }, 1500);
371
+
}
372
+
373
+
if (e.target.classList.contains('load-more')) {
374
+
e.stopPropagation();
375
+
const cursor = e.target.dataset.cursor;
376
+
const lex = e.target.dataset.lexicon;
377
+
e.target.textContent = 'loading...';
378
+
379
+
const moreData = await listRecords(state.globalPds, state.did, lex, 10, cursor);
380
+
if (moreData?.records) {
381
+
let moreHtml = moreData.records.map(record => {
382
+
const json = JSON.stringify(record.value, null, 2);
383
+
return `
384
+
<div class="record">
385
+
<div class="record-header">
386
+
<span class="record-label">record</span>
387
+
<button class="copy-btn" data-content="${encodeURIComponent(json)}">copy</button>
388
+
</div>
389
+
<div class="record-content">
390
+
<pre>${escapeHtml(json)}</pre>
391
+
</div>
392
+
</div>
393
+
`;
394
+
}).join('');
395
+
396
+
e.target.remove();
397
+
recordsView.insertAdjacentHTML('beforeend', moreHtml);
398
+
399
+
if (moreData.cursor) {
400
+
recordsView.insertAdjacentHTML('beforeend',
401
+
`<button class="load-more" data-cursor="${moreData.cursor}" data-lexicon="${lex}">load more</button>`
402
+
);
403
+
}
404
+
}
405
+
}
406
+
});
407
+
} else {
408
+
recordsView.innerHTML = '<div class="record">no records found</div>';
409
+
}
410
+
}
-4
static/favicon.svg
-4
static/favicon.svg
+168
view/index.html
+168
view/index.html
···
1
+
<!DOCTYPE html>
2
+
<html>
3
+
4
+
<head>
5
+
<meta charset="UTF-8">
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+
<title>@me - explore your atproto identity</title>
8
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
9
+
10
+
<!-- Open Graph / Facebook -->
11
+
<meta property="og:type" content="website">
12
+
<meta property="og:url" content="https://at-me.wisp.place/">
13
+
<meta property="og:title" content="@me - explore your atproto identity">
14
+
<meta property="og:description"
15
+
content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
16
+
<meta property="og:image" content="https://at-me.wisp.place/og-image.png">
17
+
18
+
<!-- Twitter -->
19
+
<meta property="twitter:card" content="summary_large_image">
20
+
<meta property="twitter:url" content="https://at-me.wisp.place/">
21
+
<meta property="twitter:title" content="@me - explore your atproto identity">
22
+
<meta property="twitter:description"
23
+
content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
24
+
<meta property="twitter:image" content="https://at-me.wisp.place/og-image.png">
25
+
</head>
26
+
27
+
<body>
28
+
<a href="/" class="home-btn" title="back to landing">
29
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
30
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
32
+
<polyline points="9 22 9 12 15 12 15 22" />
33
+
</svg>
34
+
</a>
35
+
<div class="info" id="infoBtn" title="learn about your data">
36
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
37
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+
<circle cx="12" cy="12" r="10" />
39
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
40
+
<path d="M12 17h.01" />
41
+
</svg>
42
+
</div>
43
+
<div class="top-right-buttons">
44
+
<button class="filter-btn" id="filterBtn">
45
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
46
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
48
+
</svg>
49
+
<span class="filter-label-text">filter</span>
50
+
<span class="filter-count" id="filterCount" style="display: none;"></span>
51
+
</button>
52
+
<button class="watch-live-btn" id="watchLiveBtn">
53
+
<span class="watch-indicator"></span>
54
+
<span class="watch-label">watch live</span>
55
+
</button>
56
+
</div>
57
+
<div class="filter-panel" id="filterPanel">
58
+
<div class="filter-panel-header">
59
+
<span class="filter-panel-title">show apps</span>
60
+
<div class="filter-panel-actions">
61
+
<button type="button" class="filter-action-btn" id="filterShowAll">all</button>
62
+
<button type="button" class="filter-action-btn" id="filterHideUnresolved">valid</button>
63
+
<button type="button" class="filter-action-btn" id="filterHideAll">none</button>
64
+
</div>
65
+
</div>
66
+
<div class="filter-list" id="filterList"></div>
67
+
</div>
68
+
<div class="pov-indicator">point of view of <a class="pov-handle" id="povHandle" href="#" target="_blank"
69
+
rel="noopener noreferrer"></a></div>
70
+
<div class="guestbook-sign">sign the guest list</div>
71
+
<div class="guestbook-buttons-container">
72
+
<button class="view-guestbook-btn" id="viewGuestbookBtn" title="view all signatures">
73
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
74
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
75
+
<line x1="8" x2="21" y1="6" y2="6" />
76
+
<line x1="8" x2="21" y1="12" y2="12" />
77
+
<line x1="8" x2="21" y1="18" y2="18" />
78
+
<line x1="3" x2="3.01" y1="6" y2="6" />
79
+
<line x1="3" x2="3.01" y1="12" y2="12" />
80
+
<line x1="3" x2="3.01" y1="18" y2="18" />
81
+
</svg>
82
+
</button>
83
+
<button class="sign-guestbook-btn" id="signGuestbookBtn" title="sign the guestbook">
84
+
<span class="guestbook-icon">
85
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
86
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
88
+
</svg>
89
+
</span>
90
+
<span class="guestbook-text">sign guestbook</span>
91
+
<img class="guestbook-avatar" id="guestbookAvatar" style="display: none;" />
92
+
</button>
93
+
</div>
94
+
95
+
<div class="firehose-toast" id="firehoseToast">
96
+
<div class="firehose-toast-action"></div>
97
+
<div class="firehose-toast-collection"></div>
98
+
<a class='firehose-toast-link' id='firehoseToastLink' href='#' target='_blank' rel='noopener noreferrer'>view
99
+
record</a>
100
+
</div>
101
+
102
+
<div class="overlay" id="overlay"></div>
103
+
<div class="info-modal" id="infoModal">
104
+
<h2>this is your data</h2>
105
+
<p>this visualization shows your <a href="https://atproto.com/guides/data-repos" target="_blank"
106
+
rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data
107
+
Server</a> - where your social data actually lives. unlike traditional platforms that lock everything in
108
+
their database, your posts, likes, and follows are stored here, on infrastructure you control.</p>
109
+
<p>each circle represents an app that writes to your space. <a href="https://bsky.app" target="_blank"
110
+
rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">bluesky</a> for
111
+
microblogging. <a href="https://whtwnd.com" target="_blank" rel="noopener noreferrer"
112
+
style="color: var(--text); text-decoration: underline;">whitewind</a> for long-form posts. <a
113
+
href="https://tangled.org" target="_blank" rel="noopener noreferrer"
114
+
style="color: var(--text); text-decoration: underline;">tangled.org</a> for code hosting. they're all
115
+
just different views of the same underlying data - <strong>your</strong> data.</p>
116
+
<p>this is what "<a href="https://overreacted.io/open-social/" target="_blank" rel="noopener noreferrer"
117
+
style="color: var(--text); text-decoration: underline;">open social</a>" means: your followers, your
118
+
content, your connections - they all belong to you, not the app. switch apps anytime and take everything
119
+
with you. no platform can hold your social graph hostage.</p>
120
+
<p style="margin-bottom: 1rem;"><strong>how to explore:</strong> click your avatar in the center to see the
121
+
details of your identity. click any app to browse the records it's created in your repository.</p>
122
+
<button id="closeInfo">got it</button>
123
+
<p
124
+
style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.7rem; color: var(--text-light); display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">
125
+
<span>view <a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer"
126
+
style="color: var(--text); text-decoration: underline;">the source code</a> on</span>
127
+
<a href="https://tangled.org" target="_blank" rel="noopener noreferrer"
128
+
style="color: var(--text); text-decoration: underline;">tangled.org</a>
129
+
</p>
130
+
</div>
131
+
132
+
<div class="guestbook-modal" id="guestbookModal">
133
+
<button class="guestbook-close" id="guestbookClose">x</button>
134
+
<div id="guestbookContent"></div>
135
+
</div>
136
+
137
+
<div class="canvas">
138
+
<div class="identity">
139
+
<img class="identity-avatar" id="identityAvatar" />
140
+
<div class="identity-handle" id="handleDisplay"></div>
141
+
</div>
142
+
<div id="field" class="loading">
143
+
<div class="loading-spinner"></div>
144
+
<div class="loading-text">loading your data</div>
145
+
<div class="loading-progress" id="status">resolving identity...</div>
146
+
</div>
147
+
</div>
148
+
<div id="detail" class="detail-panel"></div>
149
+
150
+
<script type="module" src="/src/view/main.js"></script>
151
+
<script>
152
+
// Info modal handlers (kept inline as they're simple UI toggles)
153
+
document.getElementById('infoBtn').addEventListener('click', () => {
154
+
document.getElementById('infoModal').classList.add('visible');
155
+
document.getElementById('overlay').classList.add('visible');
156
+
});
157
+
document.getElementById('closeInfo').addEventListener('click', () => {
158
+
document.getElementById('infoModal').classList.remove('visible');
159
+
document.getElementById('overlay').classList.remove('visible');
160
+
});
161
+
document.getElementById('overlay').addEventListener('click', () => {
162
+
document.getElementById('infoModal').classList.remove('visible');
163
+
document.getElementById('overlay').classList.remove('visible');
164
+
});
165
+
</script>
166
+
</body>
167
+
168
+
</html>
+168
view.html
+168
view.html
···
1
+
<!DOCTYPE html>
2
+
<html>
3
+
4
+
<head>
5
+
<meta charset="UTF-8">
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+
<title>@me - explore your atproto identity</title>
8
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
9
+
10
+
<!-- Open Graph / Facebook -->
11
+
<meta property="og:type" content="website">
12
+
<meta property="og:url" content="https://at-me.wisp.place/">
13
+
<meta property="og:title" content="@me - explore your atproto identity">
14
+
<meta property="og:description"
15
+
content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
16
+
<meta property="og:image" content="https://at-me.wisp.place/og-image.png">
17
+
18
+
<!-- Twitter -->
19
+
<meta property="twitter:card" content="summary_large_image">
20
+
<meta property="twitter:url" content="https://at-me.wisp.place/">
21
+
<meta property="twitter:title" content="@me - explore your atproto identity">
22
+
<meta property="twitter:description"
23
+
content="visualize your decentralized identity and see what apps have stored data in your Personal Data Server">
24
+
<meta property="twitter:image" content="https://at-me.wisp.place/og-image.png">
25
+
</head>
26
+
27
+
<body>
28
+
<a href="/" class="home-btn" title="back to landing">
29
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
30
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
32
+
<polyline points="9 22 9 12 15 12 15 22" />
33
+
</svg>
34
+
</a>
35
+
<div class="info" id="infoBtn" title="learn about your data">
36
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
37
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+
<circle cx="12" cy="12" r="10" />
39
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
40
+
<path d="M12 17h.01" />
41
+
</svg>
42
+
</div>
43
+
<div class="top-right-buttons">
44
+
<button class="filter-btn" id="filterBtn">
45
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
46
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
48
+
</svg>
49
+
<span class="filter-label-text">filter</span>
50
+
<span class="filter-count" id="filterCount" style="display: none;"></span>
51
+
</button>
52
+
<button class="watch-live-btn" id="watchLiveBtn">
53
+
<span class="watch-indicator"></span>
54
+
<span class="watch-label">watch live</span>
55
+
</button>
56
+
</div>
57
+
<div class="filter-panel" id="filterPanel">
58
+
<div class="filter-panel-header">
59
+
<span class="filter-panel-title">show apps</span>
60
+
<div class="filter-panel-actions">
61
+
<button type="button" class="filter-action-btn" id="filterShowAll">all</button>
62
+
<button type="button" class="filter-action-btn" id="filterHideUnresolved">valid</button>
63
+
<button type="button" class="filter-action-btn" id="filterHideAll">none</button>
64
+
</div>
65
+
</div>
66
+
<div class="filter-list" id="filterList"></div>
67
+
</div>
68
+
<div class="pov-indicator">point of view of <a class="pov-handle" id="povHandle" href="#" target="_blank"
69
+
rel="noopener noreferrer"></a></div>
70
+
<div class="guestbook-sign">sign the guest list</div>
71
+
<div class="guestbook-buttons-container">
72
+
<button class="view-guestbook-btn" id="viewGuestbookBtn" title="view all signatures">
73
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
74
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
75
+
<line x1="8" x2="21" y1="6" y2="6" />
76
+
<line x1="8" x2="21" y1="12" y2="12" />
77
+
<line x1="8" x2="21" y1="18" y2="18" />
78
+
<line x1="3" x2="3.01" y1="6" y2="6" />
79
+
<line x1="3" x2="3.01" y1="12" y2="12" />
80
+
<line x1="3" x2="3.01" y1="18" y2="18" />
81
+
</svg>
82
+
</button>
83
+
<button class="sign-guestbook-btn" id="signGuestbookBtn" title="sign the guestbook">
84
+
<span class="guestbook-icon">
85
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
86
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
88
+
</svg>
89
+
</span>
90
+
<span class="guestbook-text">sign guestbook</span>
91
+
<img class="guestbook-avatar" id="guestbookAvatar" style="display: none;" />
92
+
</button>
93
+
</div>
94
+
95
+
<div class="firehose-toast" id="firehoseToast">
96
+
<div class="firehose-toast-action"></div>
97
+
<div class="firehose-toast-collection"></div>
98
+
<a class='firehose-toast-link' id='firehoseToastLink' href='#' target='_blank' rel='noopener noreferrer'>view
99
+
record</a>
100
+
</div>
101
+
102
+
<div class="overlay" id="overlay"></div>
103
+
<div class="info-modal" id="infoModal">
104
+
<h2>this is your data</h2>
105
+
<p>this visualization shows your <a href="https://atproto.com/guides/data-repos" target="_blank"
106
+
rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">Personal Data
107
+
Server</a> - where your social data actually lives. unlike traditional platforms that lock everything in
108
+
their database, your posts, likes, and follows are stored here, on infrastructure you control.</p>
109
+
<p>each circle represents an app that writes to your space. <a href="https://bsky.app" target="_blank"
110
+
rel="noopener noreferrer" style="color: var(--text); text-decoration: underline;">bluesky</a> for
111
+
microblogging. <a href="https://whtwnd.com" target="_blank" rel="noopener noreferrer"
112
+
style="color: var(--text); text-decoration: underline;">whitewind</a> for long-form posts. <a
113
+
href="https://tangled.org" target="_blank" rel="noopener noreferrer"
114
+
style="color: var(--text); text-decoration: underline;">tangled.org</a> for code hosting. they're all
115
+
just different views of the same underlying data - <strong>your</strong> data.</p>
116
+
<p>this is what "<a href="https://overreacted.io/open-social/" target="_blank" rel="noopener noreferrer"
117
+
style="color: var(--text); text-decoration: underline;">open social</a>" means: your followers, your
118
+
content, your connections - they all belong to you, not the app. switch apps anytime and take everything
119
+
with you. no platform can hold your social graph hostage.</p>
120
+
<p style="margin-bottom: 1rem;"><strong>how to explore:</strong> click your avatar in the center to see the
121
+
details of your identity. click any app to browse the records it's created in your repository.</p>
122
+
<button id="closeInfo">got it</button>
123
+
<p
124
+
style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.7rem; color: var(--text-light); display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">
125
+
<span>view <a href="https://tangled.org/@zzstoatzz.io/at-me" target="_blank" rel="noopener noreferrer"
126
+
style="color: var(--text); text-decoration: underline;">the source code</a> on</span>
127
+
<a href="https://tangled.org" target="_blank" rel="noopener noreferrer"
128
+
style="color: var(--text); text-decoration: underline;">tangled.org</a>
129
+
</p>
130
+
</div>
131
+
132
+
<div class="guestbook-modal" id="guestbookModal">
133
+
<button class="guestbook-close" id="guestbookClose">x</button>
134
+
<div id="guestbookContent"></div>
135
+
</div>
136
+
137
+
<div class="canvas">
138
+
<div class="identity">
139
+
<img class="identity-avatar" id="identityAvatar" />
140
+
<div class="identity-handle" id="handleDisplay"></div>
141
+
</div>
142
+
<div id="field" class="loading">
143
+
<div class="loading-spinner"></div>
144
+
<div class="loading-text">loading your data</div>
145
+
<div class="loading-progress" id="status">resolving identity...</div>
146
+
</div>
147
+
</div>
148
+
<div id="detail" class="detail-panel"></div>
149
+
150
+
<script type="module" src="/src/view/main.js"></script>
151
+
<script>
152
+
// Info modal handlers (kept inline as they're simple UI toggles)
153
+
document.getElementById('infoBtn').addEventListener('click', () => {
154
+
document.getElementById('infoModal').classList.add('visible');
155
+
document.getElementById('overlay').classList.add('visible');
156
+
});
157
+
document.getElementById('closeInfo').addEventListener('click', () => {
158
+
document.getElementById('infoModal').classList.remove('visible');
159
+
document.getElementById('overlay').classList.remove('visible');
160
+
});
161
+
document.getElementById('overlay').addEventListener('click', () => {
162
+
document.getElementById('infoModal').classList.remove('visible');
163
+
document.getElementById('overlay').classList.remove('visible');
164
+
});
165
+
</script>
166
+
</body>
167
+
168
+
</html>
+34
vite.config.js
+34
vite.config.js
···
1
+
import { defineConfig } from 'vite';
2
+
3
+
export default defineConfig({
4
+
root: '.',
5
+
publicDir: 'public',
6
+
base: './',
7
+
build: {
8
+
outDir: 'dist',
9
+
rollupOptions: {
10
+
input: {
11
+
main: 'index.html',
12
+
view: 'view.html',
13
+
'view-dir': 'view/index.html'
14
+
}
15
+
}
16
+
},
17
+
server: {
18
+
port: 3030
19
+
},
20
+
appType: 'mpa',
21
+
plugins: [
22
+
{
23
+
name: 'rewrite-view',
24
+
configureServer(server) {
25
+
server.middlewares.use((req, res, next) => {
26
+
if (req.url.startsWith('/view') && !req.url.includes('.html')) {
27
+
req.url = req.url.replace('/view', '/view.html');
28
+
}
29
+
next();
30
+
});
31
+
}
32
+
}
33
+
]
34
+
});