feat: implement ATProto labeler for copyright violations (#385)

* feat: implement ATProto labeler for copyright violations

adds a full ATProto-compliant labeler service that emits signed
`copyright-violation` labels when tracks are flagged by copyright scans.

moderation service (rust):
- add Label struct with DAG-CBOR serialization and secp256k1 signing
- add Postgres database for label storage with sequence numbers
- implement com.atproto.label.queryLabels XRPC endpoint
- implement com.atproto.label.subscribeLabels WebSocket with backfill
- add /emit-label endpoint for backend integration
- add landing page at root URL
- fix: split migrations into separate statements (postgres requirement)
- fix: enable tls-rustls for Neon database connections

backend (python):
- add _emit_copyright_label() to POST labels when tracks flagged
- add labeler_url config setting
- emit label only when track has atproto_record_uri

includes regression tests for label emission behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update neon project names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 04900332 c72fceba

Changed files
+2431 -48
backend
src
backend
tests
docs
tools
moderation
+45 -2
backend/src/backend/_internal/moderation.py
··· 5 5 6 6 import httpx 7 7 import logfire 8 + from sqlalchemy import select 8 9 9 10 from backend.config import settings 10 - from backend.models import CopyrightScan 11 + from backend.models import CopyrightScan, Track 11 12 from backend.utilities.database import db_session 12 13 13 14 logger = logging.getLogger(__name__) ··· 87 88 result: scan result from moderation service 88 89 """ 89 90 async with db_session() as db: 91 + is_flagged = result.get("is_flagged", False) 92 + 90 93 scan = CopyrightScan( 91 94 track_id=track_id, 92 - is_flagged=result.get("is_flagged", False), 95 + is_flagged=is_flagged, 93 96 highest_score=result.get("highest_score", 0), 94 97 matches=result.get("matches", []), 95 98 raw_response=result.get("raw_response", {}), ··· 104 107 highest_score=scan.highest_score, 105 108 match_count=len(scan.matches), 106 109 ) 110 + 111 + # emit ATProto label if flagged 112 + if is_flagged: 113 + track = await db.scalar(select(Track).where(Track.id == track_id)) 114 + if track and track.atproto_record_uri: 115 + await _emit_copyright_label( 116 + uri=track.atproto_record_uri, 117 + cid=track.atproto_record_cid, 118 + ) 119 + 120 + 121 + async def _emit_copyright_label(uri: str, cid: str | None) -> None: 122 + """emit a copyright-violation label to the ATProto labeler service. 123 + 124 + this is fire-and-forget - failures are logged but don't affect the scan result. 125 + 126 + args: 127 + uri: AT URI of the track record 128 + cid: optional CID of the record 129 + """ 130 + try: 131 + async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client: 132 + response = await client.post( 133 + f"{settings.moderation.labeler_url}/emit-label", 134 + json={ 135 + "uri": uri, 136 + "val": "copyright-violation", 137 + "cid": cid, 138 + }, 139 + headers={"X-Moderation-Key": settings.moderation.auth_token}, 140 + ) 141 + response.raise_for_status() 142 + 143 + logfire.info( 144 + "copyright label emitted", 145 + uri=uri, 146 + cid=cid, 147 + ) 148 + except Exception as e: 149 + logger.warning("failed to emit copyright label for %s: %s", uri, e) 107 150 108 151 109 152 async def _store_scan_error(track_id: int, error: str) -> None:
+4
backend/src/backend/config.py
··· 399 399 default=300, 400 400 description="Timeout for moderation service requests", 401 401 ) 402 + labeler_url: str = Field( 403 + default="https://moderation.plyr.fm", 404 + description="URL of the ATProto labeler service for emitting labels", 405 + ) 402 406 403 407 404 408 class RateLimitSettings(AppSettingsSection):
+74
backend/tests/test_moderation.py
··· 122 122 assert scan.matches[0]["artist"] == "Test Artist" 123 123 124 124 125 + async def test_store_scan_result_flagged_emits_label( 126 + db_session: AsyncSession, 127 + mock_moderation_response: dict, 128 + ) -> None: 129 + """test that flagged scan result emits ATProto label.""" 130 + # create test artist and track with ATProto URI 131 + artist = Artist( 132 + did="did:plc:labelertest", 133 + handle="labeler.bsky.social", 134 + display_name="Labeler Test User", 135 + ) 136 + db_session.add(artist) 137 + await db_session.commit() 138 + 139 + track = Track( 140 + title="Labeler Test Track", 141 + file_id="labeler_test_file", 142 + file_type="mp3", 143 + artist_did=artist.did, 144 + r2_url="https://example.com/audio.mp3", 145 + atproto_record_uri="at://did:plc:labelertest/fm.plyr.track/abc123", 146 + atproto_record_cid="bafyreiabc123", 147 + ) 148 + db_session.add(track) 149 + await db_session.commit() 150 + 151 + with patch( 152 + "backend._internal.moderation._emit_copyright_label", 153 + new_callable=AsyncMock, 154 + ) as mock_emit: 155 + await _store_scan_result(track.id, mock_moderation_response) 156 + 157 + # verify label emission was called 158 + mock_emit.assert_called_once_with( 159 + uri="at://did:plc:labelertest/fm.plyr.track/abc123", 160 + cid="bafyreiabc123", 161 + ) 162 + 163 + 164 + async def test_store_scan_result_flagged_no_atproto_uri_skips_label( 165 + db_session: AsyncSession, 166 + mock_moderation_response: dict, 167 + ) -> None: 168 + """test that flagged scan without ATProto URI skips label emission.""" 169 + # create test artist and track without ATProto URI 170 + artist = Artist( 171 + did="did:plc:nouri", 172 + handle="nouri.bsky.social", 173 + display_name="No URI User", 174 + ) 175 + db_session.add(artist) 176 + await db_session.commit() 177 + 178 + track = Track( 179 + title="No URI Track", 180 + file_id="nouri_file", 181 + file_type="mp3", 182 + artist_did=artist.did, 183 + r2_url="https://example.com/audio.mp3", 184 + # no atproto_record_uri 185 + ) 186 + db_session.add(track) 187 + await db_session.commit() 188 + 189 + with patch( 190 + "backend._internal.moderation._emit_copyright_label", 191 + new_callable=AsyncMock, 192 + ) as mock_emit: 193 + await _store_scan_result(track.id, mock_moderation_response) 194 + 195 + # label emission should not be called 196 + mock_emit.assert_not_called() 197 + 198 + 125 199 async def test_store_scan_result_clear( 126 200 db_session: AsyncSession, 127 201 mock_clear_response: dict,
+2 -2
docs/tools/neon.md
··· 28 28 - storage size 29 29 30 30 **plyr.fm projects:** 31 - - `plyr` (cold-butterfly-11920742) - production (us-east-1) 31 + - `plyr-prd` (cold-butterfly-11920742) - production (us-east-1) 32 + - `plyr-stg` (frosty-math-37367092) - staging (us-west-2) 32 33 - `plyr-dev` (muddy-flower-98795112) - development (us-east-2) 33 - - `plyr-staging` (frosty-math-37367092) - staging (us-west-2) 34 34 35 35 ### get project details 36 36
+1422 -20
moderation/Cargo.lock
··· 12 12 ] 13 13 14 14 [[package]] 15 + name = "allocator-api2" 16 + version = "0.2.21" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 + 20 + [[package]] 21 + name = "android_system_properties" 22 + version = "0.1.5" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 + dependencies = [ 26 + "libc", 27 + ] 28 + 29 + [[package]] 15 30 name = "anyhow" 16 31 version = "1.0.100" 17 32 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 25 40 dependencies = [ 26 41 "proc-macro2", 27 42 "quote", 28 - "syn", 43 + "syn 2.0.111", 44 + ] 45 + 46 + [[package]] 47 + name = "atoi" 48 + version = "2.0.0" 49 + source = "registry+https://github.com/rust-lang/crates.io-index" 50 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 51 + dependencies = [ 52 + "num-traits", 29 53 ] 30 54 31 55 [[package]] ··· 35 59 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 36 60 37 61 [[package]] 62 + name = "autocfg" 63 + version = "1.5.0" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 66 + 67 + [[package]] 38 68 name = "axum" 39 69 version = "0.7.9" 40 70 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 43 73 "async-trait", 44 74 "axum-core", 45 75 "axum-macros", 76 + "base64", 46 77 "bytes", 47 78 "futures-util", 48 79 "http", ··· 61 92 "serde_json", 62 93 "serde_path_to_error", 63 94 "serde_urlencoded", 95 + "sha1", 64 96 "sync_wrapper", 65 97 "tokio", 98 + "tokio-tungstenite", 66 99 "tower", 67 100 "tower-layer", 68 101 "tower-service", ··· 98 131 dependencies = [ 99 132 "proc-macro2", 100 133 "quote", 101 - "syn", 134 + "syn 2.0.111", 135 + ] 136 + 137 + [[package]] 138 + name = "base-x" 139 + version = "0.2.11" 140 + source = "registry+https://github.com/rust-lang/crates.io-index" 141 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 142 + 143 + [[package]] 144 + name = "base16ct" 145 + version = "0.2.0" 146 + source = "registry+https://github.com/rust-lang/crates.io-index" 147 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 148 + 149 + [[package]] 150 + name = "base256emoji" 151 + version = "1.0.2" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 154 + dependencies = [ 155 + "const-str", 156 + "match-lookup", 102 157 ] 103 158 104 159 [[package]] ··· 108 163 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 109 164 110 165 [[package]] 166 + name = "base64ct" 167 + version = "1.8.0" 168 + source = "registry+https://github.com/rust-lang/crates.io-index" 169 + checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 170 + 171 + [[package]] 111 172 name = "bitflags" 112 173 version = "2.10.0" 113 174 source = "registry+https://github.com/rust-lang/crates.io-index" 114 175 checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 176 + dependencies = [ 177 + "serde_core", 178 + ] 179 + 180 + [[package]] 181 + name = "block-buffer" 182 + version = "0.10.4" 183 + source = "registry+https://github.com/rust-lang/crates.io-index" 184 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 185 + dependencies = [ 186 + "generic-array", 187 + ] 115 188 116 189 [[package]] 117 190 name = "bumpalo" ··· 120 193 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 121 194 122 195 [[package]] 196 + name = "byteorder" 197 + version = "1.5.0" 198 + source = "registry+https://github.com/rust-lang/crates.io-index" 199 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 200 + 201 + [[package]] 123 202 name = "bytes" 124 203 version = "1.11.0" 125 204 source = "registry+https://github.com/rust-lang/crates.io-index" 126 205 checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 127 206 128 207 [[package]] 208 + name = "cbor4ii" 209 + version = "0.2.14" 210 + source = "registry+https://github.com/rust-lang/crates.io-index" 211 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 212 + dependencies = [ 213 + "serde", 214 + ] 215 + 216 + [[package]] 129 217 name = "cc" 130 218 version = "1.2.47" 131 219 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 148 236 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 149 237 150 238 [[package]] 239 + name = "chrono" 240 + version = "0.4.42" 241 + source = "registry+https://github.com/rust-lang/crates.io-index" 242 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 243 + dependencies = [ 244 + "iana-time-zone", 245 + "js-sys", 246 + "num-traits", 247 + "serde", 248 + "wasm-bindgen", 249 + "windows-link", 250 + ] 251 + 252 + [[package]] 253 + name = "cid" 254 + version = "0.11.1" 255 + source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 257 + dependencies = [ 258 + "core2", 259 + "multibase", 260 + "multihash", 261 + "serde", 262 + "serde_bytes", 263 + "unsigned-varint", 264 + ] 265 + 266 + [[package]] 267 + name = "concurrent-queue" 268 + version = "2.5.0" 269 + source = "registry+https://github.com/rust-lang/crates.io-index" 270 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 271 + dependencies = [ 272 + "crossbeam-utils", 273 + ] 274 + 275 + [[package]] 276 + name = "const-oid" 277 + version = "0.9.6" 278 + source = "registry+https://github.com/rust-lang/crates.io-index" 279 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 280 + 281 + [[package]] 282 + name = "const-str" 283 + version = "0.4.3" 284 + source = "registry+https://github.com/rust-lang/crates.io-index" 285 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 286 + 287 + [[package]] 288 + name = "core-foundation-sys" 289 + version = "0.8.7" 290 + source = "registry+https://github.com/rust-lang/crates.io-index" 291 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 292 + 293 + [[package]] 294 + name = "core2" 295 + version = "0.4.0" 296 + source = "registry+https://github.com/rust-lang/crates.io-index" 297 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 298 + dependencies = [ 299 + "memchr", 300 + ] 301 + 302 + [[package]] 303 + name = "cpufeatures" 304 + version = "0.2.17" 305 + source = "registry+https://github.com/rust-lang/crates.io-index" 306 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 307 + dependencies = [ 308 + "libc", 309 + ] 310 + 311 + [[package]] 312 + name = "crc" 313 + version = "3.4.0" 314 + source = "registry+https://github.com/rust-lang/crates.io-index" 315 + checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" 316 + dependencies = [ 317 + "crc-catalog", 318 + ] 319 + 320 + [[package]] 321 + name = "crc-catalog" 322 + version = "2.4.0" 323 + source = "registry+https://github.com/rust-lang/crates.io-index" 324 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 325 + 326 + [[package]] 327 + name = "crossbeam-queue" 328 + version = "0.3.12" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 331 + dependencies = [ 332 + "crossbeam-utils", 333 + ] 334 + 335 + [[package]] 336 + name = "crossbeam-utils" 337 + version = "0.8.21" 338 + source = "registry+https://github.com/rust-lang/crates.io-index" 339 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 340 + 341 + [[package]] 342 + name = "crypto-bigint" 343 + version = "0.5.5" 344 + source = "registry+https://github.com/rust-lang/crates.io-index" 345 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 346 + dependencies = [ 347 + "generic-array", 348 + "rand_core 0.6.4", 349 + "subtle", 350 + "zeroize", 351 + ] 352 + 353 + [[package]] 354 + name = "crypto-common" 355 + version = "0.1.6" 356 + source = "registry+https://github.com/rust-lang/crates.io-index" 357 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 358 + dependencies = [ 359 + "generic-array", 360 + "typenum", 361 + ] 362 + 363 + [[package]] 364 + name = "data-encoding" 365 + version = "2.9.0" 366 + source = "registry+https://github.com/rust-lang/crates.io-index" 367 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 368 + 369 + [[package]] 370 + name = "data-encoding-macro" 371 + version = "0.1.18" 372 + source = "registry+https://github.com/rust-lang/crates.io-index" 373 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 374 + dependencies = [ 375 + "data-encoding", 376 + "data-encoding-macro-internal", 377 + ] 378 + 379 + [[package]] 380 + name = "data-encoding-macro-internal" 381 + version = "0.1.16" 382 + source = "registry+https://github.com/rust-lang/crates.io-index" 383 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 384 + dependencies = [ 385 + "data-encoding", 386 + "syn 2.0.111", 387 + ] 388 + 389 + [[package]] 390 + name = "der" 391 + version = "0.7.10" 392 + source = "registry+https://github.com/rust-lang/crates.io-index" 393 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 394 + dependencies = [ 395 + "const-oid", 396 + "pem-rfc7468", 397 + "zeroize", 398 + ] 399 + 400 + [[package]] 401 + name = "digest" 402 + version = "0.10.7" 403 + source = "registry+https://github.com/rust-lang/crates.io-index" 404 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 405 + dependencies = [ 406 + "block-buffer", 407 + "const-oid", 408 + "crypto-common", 409 + "subtle", 410 + ] 411 + 412 + [[package]] 151 413 name = "displaydoc" 152 414 version = "0.2.5" 153 415 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 155 417 dependencies = [ 156 418 "proc-macro2", 157 419 "quote", 158 - "syn", 420 + "syn 2.0.111", 421 + ] 422 + 423 + [[package]] 424 + name = "dotenvy" 425 + version = "0.15.7" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 428 + 429 + [[package]] 430 + name = "ecdsa" 431 + version = "0.16.9" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 434 + dependencies = [ 435 + "der", 436 + "digest", 437 + "elliptic-curve", 438 + "rfc6979", 439 + "signature", 440 + "spki", 441 + ] 442 + 443 + [[package]] 444 + name = "either" 445 + version = "1.15.0" 446 + source = "registry+https://github.com/rust-lang/crates.io-index" 447 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 448 + dependencies = [ 449 + "serde", 450 + ] 451 + 452 + [[package]] 453 + name = "elliptic-curve" 454 + version = "0.13.8" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 457 + dependencies = [ 458 + "base16ct", 459 + "crypto-bigint", 460 + "digest", 461 + "ff", 462 + "generic-array", 463 + "group", 464 + "pkcs8", 465 + "rand_core 0.6.4", 466 + "sec1", 467 + "subtle", 468 + "zeroize", 469 + ] 470 + 471 + [[package]] 472 + name = "equivalent" 473 + version = "1.0.2" 474 + source = "registry+https://github.com/rust-lang/crates.io-index" 475 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 476 + 477 + [[package]] 478 + name = "etcetera" 479 + version = "0.8.0" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 482 + dependencies = [ 483 + "cfg-if", 484 + "home", 485 + "windows-sys 0.48.0", 486 + ] 487 + 488 + [[package]] 489 + name = "event-listener" 490 + version = "5.4.1" 491 + source = "registry+https://github.com/rust-lang/crates.io-index" 492 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 493 + dependencies = [ 494 + "concurrent-queue", 495 + "parking", 496 + "pin-project-lite", 497 + ] 498 + 499 + [[package]] 500 + name = "ff" 501 + version = "0.13.1" 502 + source = "registry+https://github.com/rust-lang/crates.io-index" 503 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 504 + dependencies = [ 505 + "rand_core 0.6.4", 506 + "subtle", 159 507 ] 160 508 161 509 [[package]] ··· 165 513 checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 166 514 167 515 [[package]] 516 + name = "flume" 517 + version = "0.11.1" 518 + source = "registry+https://github.com/rust-lang/crates.io-index" 519 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 520 + dependencies = [ 521 + "futures-core", 522 + "futures-sink", 523 + "spin", 524 + ] 525 + 526 + [[package]] 527 + name = "foldhash" 528 + version = "0.1.5" 529 + source = "registry+https://github.com/rust-lang/crates.io-index" 530 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 531 + 532 + [[package]] 168 533 name = "form_urlencoded" 169 534 version = "1.2.2" 170 535 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 174 539 ] 175 540 176 541 [[package]] 542 + name = "futures" 543 + version = "0.3.31" 544 + source = "registry+https://github.com/rust-lang/crates.io-index" 545 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 546 + dependencies = [ 547 + "futures-channel", 548 + "futures-core", 549 + "futures-executor", 550 + "futures-io", 551 + "futures-sink", 552 + "futures-task", 553 + "futures-util", 554 + ] 555 + 556 + [[package]] 177 557 name = "futures-channel" 178 558 version = "0.3.31" 179 559 source = "registry+https://github.com/rust-lang/crates.io-index" 180 560 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 181 561 dependencies = [ 182 562 "futures-core", 563 + "futures-sink", 183 564 ] 184 565 185 566 [[package]] ··· 189 570 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 190 571 191 572 [[package]] 573 + name = "futures-executor" 574 + version = "0.3.31" 575 + source = "registry+https://github.com/rust-lang/crates.io-index" 576 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 577 + dependencies = [ 578 + "futures-core", 579 + "futures-task", 580 + "futures-util", 581 + ] 582 + 583 + [[package]] 584 + name = "futures-intrusive" 585 + version = "0.5.0" 586 + source = "registry+https://github.com/rust-lang/crates.io-index" 587 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 588 + dependencies = [ 589 + "futures-core", 590 + "lock_api", 591 + "parking_lot", 592 + ] 593 + 594 + [[package]] 595 + name = "futures-io" 596 + version = "0.3.31" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 599 + 600 + [[package]] 601 + name = "futures-macro" 602 + version = "0.3.31" 603 + source = "registry+https://github.com/rust-lang/crates.io-index" 604 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 605 + dependencies = [ 606 + "proc-macro2", 607 + "quote", 608 + "syn 2.0.111", 609 + ] 610 + 611 + [[package]] 612 + name = "futures-sink" 613 + version = "0.3.31" 614 + source = "registry+https://github.com/rust-lang/crates.io-index" 615 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 616 + 617 + [[package]] 192 618 name = "futures-task" 193 619 version = "0.3.31" 194 620 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 200 626 source = "registry+https://github.com/rust-lang/crates.io-index" 201 627 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 202 628 dependencies = [ 629 + "futures-channel", 203 630 "futures-core", 631 + "futures-io", 632 + "futures-macro", 633 + "futures-sink", 204 634 "futures-task", 635 + "memchr", 205 636 "pin-project-lite", 206 637 "pin-utils", 638 + "slab", 639 + ] 640 + 641 + [[package]] 642 + name = "generic-array" 643 + version = "0.14.9" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" 646 + dependencies = [ 647 + "typenum", 648 + "version_check", 649 + "zeroize", 207 650 ] 208 651 209 652 [[package]] ··· 234 677 ] 235 678 236 679 [[package]] 680 + name = "group" 681 + version = "0.13.0" 682 + source = "registry+https://github.com/rust-lang/crates.io-index" 683 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 684 + dependencies = [ 685 + "ff", 686 + "rand_core 0.6.4", 687 + "subtle", 688 + ] 689 + 690 + [[package]] 691 + name = "hashbrown" 692 + version = "0.15.5" 693 + source = "registry+https://github.com/rust-lang/crates.io-index" 694 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 695 + dependencies = [ 696 + "allocator-api2", 697 + "equivalent", 698 + "foldhash", 699 + ] 700 + 701 + [[package]] 702 + name = "hashbrown" 703 + version = "0.16.1" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 706 + 707 + [[package]] 708 + name = "hashlink" 709 + version = "0.10.0" 710 + source = "registry+https://github.com/rust-lang/crates.io-index" 711 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 712 + dependencies = [ 713 + "hashbrown 0.15.5", 714 + ] 715 + 716 + [[package]] 717 + name = "heck" 718 + version = "0.5.0" 719 + source = "registry+https://github.com/rust-lang/crates.io-index" 720 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 721 + 722 + [[package]] 723 + name = "hex" 724 + version = "0.4.3" 725 + source = "registry+https://github.com/rust-lang/crates.io-index" 726 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 727 + 728 + [[package]] 729 + name = "hkdf" 730 + version = "0.12.4" 731 + source = "registry+https://github.com/rust-lang/crates.io-index" 732 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 733 + dependencies = [ 734 + "hmac", 735 + ] 736 + 737 + [[package]] 738 + name = "hmac" 739 + version = "0.12.1" 740 + source = "registry+https://github.com/rust-lang/crates.io-index" 741 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 742 + dependencies = [ 743 + "digest", 744 + ] 745 + 746 + [[package]] 747 + name = "home" 748 + version = "0.5.9" 749 + source = "registry+https://github.com/rust-lang/crates.io-index" 750 + checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 751 + dependencies = [ 752 + "windows-sys 0.52.0", 753 + ] 754 + 755 + [[package]] 237 756 name = "http" 238 757 version = "1.4.0" 239 758 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 342 861 ] 343 862 344 863 [[package]] 864 + name = "iana-time-zone" 865 + version = "0.1.64" 866 + source = "registry+https://github.com/rust-lang/crates.io-index" 867 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 868 + dependencies = [ 869 + "android_system_properties", 870 + "core-foundation-sys", 871 + "iana-time-zone-haiku", 872 + "js-sys", 873 + "log", 874 + "wasm-bindgen", 875 + "windows-core", 876 + ] 877 + 878 + [[package]] 879 + name = "iana-time-zone-haiku" 880 + version = "0.1.2" 881 + source = "registry+https://github.com/rust-lang/crates.io-index" 882 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 883 + dependencies = [ 884 + "cc", 885 + ] 886 + 887 + [[package]] 345 888 name = "icu_collections" 346 889 version = "2.1.1" 347 890 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 444 987 ] 445 988 446 989 [[package]] 990 + name = "indexmap" 991 + version = "2.12.1" 992 + source = "registry+https://github.com/rust-lang/crates.io-index" 993 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 994 + dependencies = [ 995 + "equivalent", 996 + "hashbrown 0.16.1", 997 + ] 998 + 999 + [[package]] 1000 + name = "ipld-core" 1001 + version = "0.4.2" 1002 + source = "registry+https://github.com/rust-lang/crates.io-index" 1003 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1004 + dependencies = [ 1005 + "cid", 1006 + "serde", 1007 + "serde_bytes", 1008 + ] 1009 + 1010 + [[package]] 447 1011 name = "ipnet" 448 1012 version = "2.11.0" 449 1013 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 476 1040 ] 477 1041 478 1042 [[package]] 1043 + name = "k256" 1044 + version = "0.13.4" 1045 + source = "registry+https://github.com/rust-lang/crates.io-index" 1046 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1047 + dependencies = [ 1048 + "cfg-if", 1049 + "ecdsa", 1050 + "elliptic-curve", 1051 + "once_cell", 1052 + "sha2", 1053 + "signature", 1054 + ] 1055 + 1056 + [[package]] 479 1057 name = "lazy_static" 480 1058 version = "1.5.0" 481 1059 source = "registry+https://github.com/rust-lang/crates.io-index" 482 1060 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1061 + dependencies = [ 1062 + "spin", 1063 + ] 483 1064 484 1065 [[package]] 485 1066 name = "libc" ··· 488 1069 checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 489 1070 490 1071 [[package]] 1072 + name = "libm" 1073 + version = "0.2.15" 1074 + source = "registry+https://github.com/rust-lang/crates.io-index" 1075 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1076 + 1077 + [[package]] 1078 + name = "libredox" 1079 + version = "0.1.10" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 1082 + dependencies = [ 1083 + "bitflags", 1084 + "libc", 1085 + "redox_syscall", 1086 + ] 1087 + 1088 + [[package]] 1089 + name = "libsqlite3-sys" 1090 + version = "0.30.1" 1091 + source = "registry+https://github.com/rust-lang/crates.io-index" 1092 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1093 + dependencies = [ 1094 + "pkg-config", 1095 + "vcpkg", 1096 + ] 1097 + 1098 + [[package]] 491 1099 name = "litemap" 492 1100 version = "0.8.1" 493 1101 source = "registry+https://github.com/rust-lang/crates.io-index" 494 1102 checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 495 1103 496 1104 [[package]] 1105 + name = "lock_api" 1106 + version = "0.4.14" 1107 + source = "registry+https://github.com/rust-lang/crates.io-index" 1108 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1109 + dependencies = [ 1110 + "scopeguard", 1111 + ] 1112 + 1113 + [[package]] 497 1114 name = "log" 498 1115 version = "0.4.28" 499 1116 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 506 1123 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 507 1124 508 1125 [[package]] 1126 + name = "match-lookup" 1127 + version = "0.1.1" 1128 + source = "registry+https://github.com/rust-lang/crates.io-index" 1129 + checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 1130 + dependencies = [ 1131 + "proc-macro2", 1132 + "quote", 1133 + "syn 1.0.109", 1134 + ] 1135 + 1136 + [[package]] 509 1137 name = "matchers" 510 1138 version = "0.2.0" 511 1139 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 521 1149 checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 522 1150 523 1151 [[package]] 1152 + name = "md-5" 1153 + version = "0.10.6" 1154 + source = "registry+https://github.com/rust-lang/crates.io-index" 1155 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1156 + dependencies = [ 1157 + "cfg-if", 1158 + "digest", 1159 + ] 1160 + 1161 + [[package]] 524 1162 name = "memchr" 525 1163 version = "2.7.6" 526 1164 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 549 1187 dependencies = [ 550 1188 "anyhow", 551 1189 "axum", 1190 + "bytes", 1191 + "chrono", 1192 + "futures", 1193 + "hex", 1194 + "k256", 1195 + "rand 0.8.5", 552 1196 "reqwest", 553 1197 "serde", 1198 + "serde_bytes", 1199 + "serde_ipld_dagcbor", 554 1200 "serde_json", 555 - "thiserror 1.0.69", 1201 + "sqlx", 1202 + "thiserror 2.0.17", 556 1203 "tokio", 1204 + "tokio-stream", 557 1205 "tracing", 558 1206 "tracing-subscriber", 559 1207 ] 560 1208 561 1209 [[package]] 1210 + name = "multibase" 1211 + version = "0.9.2" 1212 + source = "registry+https://github.com/rust-lang/crates.io-index" 1213 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1214 + dependencies = [ 1215 + "base-x", 1216 + "base256emoji", 1217 + "data-encoding", 1218 + "data-encoding-macro", 1219 + ] 1220 + 1221 + [[package]] 1222 + name = "multihash" 1223 + version = "0.19.3" 1224 + source = "registry+https://github.com/rust-lang/crates.io-index" 1225 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1226 + dependencies = [ 1227 + "core2", 1228 + "serde", 1229 + "unsigned-varint", 1230 + ] 1231 + 1232 + [[package]] 562 1233 name = "nu-ansi-term" 563 1234 version = "0.50.3" 564 1235 source = "registry+https://github.com/rust-lang/crates.io-index" 565 1236 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 566 1237 dependencies = [ 567 - "windows-sys 0.61.2", 1238 + "windows-sys 0.60.2", 1239 + ] 1240 + 1241 + [[package]] 1242 + name = "num-bigint-dig" 1243 + version = "0.8.6" 1244 + source = "registry+https://github.com/rust-lang/crates.io-index" 1245 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 1246 + dependencies = [ 1247 + "lazy_static", 1248 + "libm", 1249 + "num-integer", 1250 + "num-iter", 1251 + "num-traits", 1252 + "rand 0.8.5", 1253 + "smallvec", 1254 + "zeroize", 1255 + ] 1256 + 1257 + [[package]] 1258 + name = "num-integer" 1259 + version = "0.1.46" 1260 + source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1262 + dependencies = [ 1263 + "num-traits", 1264 + ] 1265 + 1266 + [[package]] 1267 + name = "num-iter" 1268 + version = "0.1.45" 1269 + source = "registry+https://github.com/rust-lang/crates.io-index" 1270 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1271 + dependencies = [ 1272 + "autocfg", 1273 + "num-integer", 1274 + "num-traits", 1275 + ] 1276 + 1277 + [[package]] 1278 + name = "num-traits" 1279 + version = "0.2.19" 1280 + source = "registry+https://github.com/rust-lang/crates.io-index" 1281 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1282 + dependencies = [ 1283 + "autocfg", 1284 + "libm", 568 1285 ] 569 1286 570 1287 [[package]] ··· 574 1291 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 575 1292 576 1293 [[package]] 1294 + name = "parking" 1295 + version = "2.2.1" 1296 + source = "registry+https://github.com/rust-lang/crates.io-index" 1297 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1298 + 1299 + [[package]] 1300 + name = "parking_lot" 1301 + version = "0.12.5" 1302 + source = "registry+https://github.com/rust-lang/crates.io-index" 1303 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1304 + dependencies = [ 1305 + "lock_api", 1306 + "parking_lot_core", 1307 + ] 1308 + 1309 + [[package]] 1310 + name = "parking_lot_core" 1311 + version = "0.9.12" 1312 + source = "registry+https://github.com/rust-lang/crates.io-index" 1313 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1314 + dependencies = [ 1315 + "cfg-if", 1316 + "libc", 1317 + "redox_syscall", 1318 + "smallvec", 1319 + "windows-link", 1320 + ] 1321 + 1322 + [[package]] 1323 + name = "pem-rfc7468" 1324 + version = "0.7.0" 1325 + source = "registry+https://github.com/rust-lang/crates.io-index" 1326 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1327 + dependencies = [ 1328 + "base64ct", 1329 + ] 1330 + 1331 + [[package]] 577 1332 name = "percent-encoding" 578 1333 version = "2.3.2" 579 1334 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 592 1347 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 593 1348 594 1349 [[package]] 1350 + name = "pkcs1" 1351 + version = "0.7.5" 1352 + source = "registry+https://github.com/rust-lang/crates.io-index" 1353 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1354 + dependencies = [ 1355 + "der", 1356 + "pkcs8", 1357 + "spki", 1358 + ] 1359 + 1360 + [[package]] 1361 + name = "pkcs8" 1362 + version = "0.10.2" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1365 + dependencies = [ 1366 + "der", 1367 + "spki", 1368 + ] 1369 + 1370 + [[package]] 1371 + name = "pkg-config" 1372 + version = "0.3.32" 1373 + source = "registry+https://github.com/rust-lang/crates.io-index" 1374 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1375 + 1376 + [[package]] 595 1377 name = "potential_utf" 596 1378 version = "0.1.4" 597 1379 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 647 1429 "bytes", 648 1430 "getrandom 0.3.4", 649 1431 "lru-slab", 650 - "rand", 1432 + "rand 0.9.2", 651 1433 "ring", 652 1434 "rustc-hash", 653 1435 "rustls", ··· 690 1472 691 1473 [[package]] 692 1474 name = "rand" 1475 + version = "0.8.5" 1476 + source = "registry+https://github.com/rust-lang/crates.io-index" 1477 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1478 + dependencies = [ 1479 + "libc", 1480 + "rand_chacha 0.3.1", 1481 + "rand_core 0.6.4", 1482 + ] 1483 + 1484 + [[package]] 1485 + name = "rand" 693 1486 version = "0.9.2" 694 1487 source = "registry+https://github.com/rust-lang/crates.io-index" 695 1488 checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 696 1489 dependencies = [ 697 - "rand_chacha", 698 - "rand_core", 1490 + "rand_chacha 0.9.0", 1491 + "rand_core 0.9.3", 1492 + ] 1493 + 1494 + [[package]] 1495 + name = "rand_chacha" 1496 + version = "0.3.1" 1497 + source = "registry+https://github.com/rust-lang/crates.io-index" 1498 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1499 + dependencies = [ 1500 + "ppv-lite86", 1501 + "rand_core 0.6.4", 699 1502 ] 700 1503 701 1504 [[package]] ··· 705 1508 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 706 1509 dependencies = [ 707 1510 "ppv-lite86", 708 - "rand_core", 1511 + "rand_core 0.9.3", 1512 + ] 1513 + 1514 + [[package]] 1515 + name = "rand_core" 1516 + version = "0.6.4" 1517 + source = "registry+https://github.com/rust-lang/crates.io-index" 1518 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1519 + dependencies = [ 1520 + "getrandom 0.2.16", 709 1521 ] 710 1522 711 1523 [[package]] ··· 718 1530 ] 719 1531 720 1532 [[package]] 1533 + name = "redox_syscall" 1534 + version = "0.5.18" 1535 + source = "registry+https://github.com/rust-lang/crates.io-index" 1536 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1537 + dependencies = [ 1538 + "bitflags", 1539 + ] 1540 + 1541 + [[package]] 721 1542 name = "regex-automata" 722 1543 version = "0.4.13" 723 1544 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 773 1594 ] 774 1595 775 1596 [[package]] 1597 + name = "rfc6979" 1598 + version = "0.4.0" 1599 + source = "registry+https://github.com/rust-lang/crates.io-index" 1600 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 1601 + dependencies = [ 1602 + "hmac", 1603 + "subtle", 1604 + ] 1605 + 1606 + [[package]] 776 1607 name = "ring" 777 1608 version = "0.17.14" 778 1609 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 787 1618 ] 788 1619 789 1620 [[package]] 1621 + name = "rsa" 1622 + version = "0.9.9" 1623 + source = "registry+https://github.com/rust-lang/crates.io-index" 1624 + checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" 1625 + dependencies = [ 1626 + "const-oid", 1627 + "digest", 1628 + "num-bigint-dig", 1629 + "num-integer", 1630 + "num-traits", 1631 + "pkcs1", 1632 + "pkcs8", 1633 + "rand_core 0.6.4", 1634 + "signature", 1635 + "spki", 1636 + "subtle", 1637 + "zeroize", 1638 + ] 1639 + 1640 + [[package]] 790 1641 name = "rustc-hash" 791 1642 version = "2.1.1" 792 1643 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 840 1691 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 841 1692 842 1693 [[package]] 1694 + name = "scopeguard" 1695 + version = "1.2.0" 1696 + source = "registry+https://github.com/rust-lang/crates.io-index" 1697 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1698 + 1699 + [[package]] 1700 + name = "sec1" 1701 + version = "0.7.3" 1702 + source = "registry+https://github.com/rust-lang/crates.io-index" 1703 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 1704 + dependencies = [ 1705 + "base16ct", 1706 + "der", 1707 + "generic-array", 1708 + "pkcs8", 1709 + "subtle", 1710 + "zeroize", 1711 + ] 1712 + 1713 + [[package]] 843 1714 name = "serde" 844 1715 version = "1.0.228" 845 1716 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 850 1721 ] 851 1722 852 1723 [[package]] 1724 + name = "serde_bytes" 1725 + version = "0.11.19" 1726 + source = "registry+https://github.com/rust-lang/crates.io-index" 1727 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 1728 + dependencies = [ 1729 + "serde", 1730 + "serde_core", 1731 + ] 1732 + 1733 + [[package]] 853 1734 name = "serde_core" 854 1735 version = "1.0.228" 855 1736 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 866 1747 dependencies = [ 867 1748 "proc-macro2", 868 1749 "quote", 869 - "syn", 1750 + "syn 2.0.111", 1751 + ] 1752 + 1753 + [[package]] 1754 + name = "serde_ipld_dagcbor" 1755 + version = "0.6.4" 1756 + source = "registry+https://github.com/rust-lang/crates.io-index" 1757 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 1758 + dependencies = [ 1759 + "cbor4ii", 1760 + "ipld-core", 1761 + "scopeguard", 1762 + "serde", 870 1763 ] 871 1764 872 1765 [[package]] ··· 906 1799 ] 907 1800 908 1801 [[package]] 1802 + name = "sha1" 1803 + version = "0.10.6" 1804 + source = "registry+https://github.com/rust-lang/crates.io-index" 1805 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1806 + dependencies = [ 1807 + "cfg-if", 1808 + "cpufeatures", 1809 + "digest", 1810 + ] 1811 + 1812 + [[package]] 1813 + name = "sha2" 1814 + version = "0.10.9" 1815 + source = "registry+https://github.com/rust-lang/crates.io-index" 1816 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1817 + dependencies = [ 1818 + "cfg-if", 1819 + "cpufeatures", 1820 + "digest", 1821 + ] 1822 + 1823 + [[package]] 909 1824 name = "sharded-slab" 910 1825 version = "0.1.7" 911 1826 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 930 1845 ] 931 1846 932 1847 [[package]] 1848 + name = "signature" 1849 + version = "2.2.0" 1850 + source = "registry+https://github.com/rust-lang/crates.io-index" 1851 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1852 + dependencies = [ 1853 + "digest", 1854 + "rand_core 0.6.4", 1855 + ] 1856 + 1857 + [[package]] 933 1858 name = "slab" 934 1859 version = "0.4.11" 935 1860 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 940 1865 version = "1.15.1" 941 1866 source = "registry+https://github.com/rust-lang/crates.io-index" 942 1867 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1868 + dependencies = [ 1869 + "serde", 1870 + ] 943 1871 944 1872 [[package]] 945 1873 name = "socket2" ··· 952 1880 ] 953 1881 954 1882 [[package]] 1883 + name = "spin" 1884 + version = "0.9.8" 1885 + source = "registry+https://github.com/rust-lang/crates.io-index" 1886 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1887 + dependencies = [ 1888 + "lock_api", 1889 + ] 1890 + 1891 + [[package]] 1892 + name = "spki" 1893 + version = "0.7.3" 1894 + source = "registry+https://github.com/rust-lang/crates.io-index" 1895 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1896 + dependencies = [ 1897 + "base64ct", 1898 + "der", 1899 + ] 1900 + 1901 + [[package]] 1902 + name = "sqlx" 1903 + version = "0.8.6" 1904 + source = "registry+https://github.com/rust-lang/crates.io-index" 1905 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 1906 + dependencies = [ 1907 + "sqlx-core", 1908 + "sqlx-macros", 1909 + "sqlx-mysql", 1910 + "sqlx-postgres", 1911 + "sqlx-sqlite", 1912 + ] 1913 + 1914 + [[package]] 1915 + name = "sqlx-core" 1916 + version = "0.8.6" 1917 + source = "registry+https://github.com/rust-lang/crates.io-index" 1918 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 1919 + dependencies = [ 1920 + "base64", 1921 + "bytes", 1922 + "chrono", 1923 + "crc", 1924 + "crossbeam-queue", 1925 + "either", 1926 + "event-listener", 1927 + "futures-core", 1928 + "futures-intrusive", 1929 + "futures-io", 1930 + "futures-util", 1931 + "hashbrown 0.15.5", 1932 + "hashlink", 1933 + "indexmap", 1934 + "log", 1935 + "memchr", 1936 + "once_cell", 1937 + "percent-encoding", 1938 + "serde", 1939 + "serde_json", 1940 + "sha2", 1941 + "smallvec", 1942 + "thiserror 2.0.17", 1943 + "tokio", 1944 + "tokio-stream", 1945 + "tracing", 1946 + "url", 1947 + ] 1948 + 1949 + [[package]] 1950 + name = "sqlx-macros" 1951 + version = "0.8.6" 1952 + source = "registry+https://github.com/rust-lang/crates.io-index" 1953 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 1954 + dependencies = [ 1955 + "proc-macro2", 1956 + "quote", 1957 + "sqlx-core", 1958 + "sqlx-macros-core", 1959 + "syn 2.0.111", 1960 + ] 1961 + 1962 + [[package]] 1963 + name = "sqlx-macros-core" 1964 + version = "0.8.6" 1965 + source = "registry+https://github.com/rust-lang/crates.io-index" 1966 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 1967 + dependencies = [ 1968 + "dotenvy", 1969 + "either", 1970 + "heck", 1971 + "hex", 1972 + "once_cell", 1973 + "proc-macro2", 1974 + "quote", 1975 + "serde", 1976 + "serde_json", 1977 + "sha2", 1978 + "sqlx-core", 1979 + "sqlx-mysql", 1980 + "sqlx-postgres", 1981 + "sqlx-sqlite", 1982 + "syn 2.0.111", 1983 + "tokio", 1984 + "url", 1985 + ] 1986 + 1987 + [[package]] 1988 + name = "sqlx-mysql" 1989 + version = "0.8.6" 1990 + source = "registry+https://github.com/rust-lang/crates.io-index" 1991 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 1992 + dependencies = [ 1993 + "atoi", 1994 + "base64", 1995 + "bitflags", 1996 + "byteorder", 1997 + "bytes", 1998 + "chrono", 1999 + "crc", 2000 + "digest", 2001 + "dotenvy", 2002 + "either", 2003 + "futures-channel", 2004 + "futures-core", 2005 + "futures-io", 2006 + "futures-util", 2007 + "generic-array", 2008 + "hex", 2009 + "hkdf", 2010 + "hmac", 2011 + "itoa", 2012 + "log", 2013 + "md-5", 2014 + "memchr", 2015 + "once_cell", 2016 + "percent-encoding", 2017 + "rand 0.8.5", 2018 + "rsa", 2019 + "serde", 2020 + "sha1", 2021 + "sha2", 2022 + "smallvec", 2023 + "sqlx-core", 2024 + "stringprep", 2025 + "thiserror 2.0.17", 2026 + "tracing", 2027 + "whoami", 2028 + ] 2029 + 2030 + [[package]] 2031 + name = "sqlx-postgres" 2032 + version = "0.8.6" 2033 + source = "registry+https://github.com/rust-lang/crates.io-index" 2034 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 2035 + dependencies = [ 2036 + "atoi", 2037 + "base64", 2038 + "bitflags", 2039 + "byteorder", 2040 + "chrono", 2041 + "crc", 2042 + "dotenvy", 2043 + "etcetera", 2044 + "futures-channel", 2045 + "futures-core", 2046 + "futures-util", 2047 + "hex", 2048 + "hkdf", 2049 + "hmac", 2050 + "home", 2051 + "itoa", 2052 + "log", 2053 + "md-5", 2054 + "memchr", 2055 + "once_cell", 2056 + "rand 0.8.5", 2057 + "serde", 2058 + "serde_json", 2059 + "sha2", 2060 + "smallvec", 2061 + "sqlx-core", 2062 + "stringprep", 2063 + "thiserror 2.0.17", 2064 + "tracing", 2065 + "whoami", 2066 + ] 2067 + 2068 + [[package]] 2069 + name = "sqlx-sqlite" 2070 + version = "0.8.6" 2071 + source = "registry+https://github.com/rust-lang/crates.io-index" 2072 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 2073 + dependencies = [ 2074 + "atoi", 2075 + "chrono", 2076 + "flume", 2077 + "futures-channel", 2078 + "futures-core", 2079 + "futures-executor", 2080 + "futures-intrusive", 2081 + "futures-util", 2082 + "libsqlite3-sys", 2083 + "log", 2084 + "percent-encoding", 2085 + "serde", 2086 + "serde_urlencoded", 2087 + "sqlx-core", 2088 + "thiserror 2.0.17", 2089 + "tracing", 2090 + "url", 2091 + ] 2092 + 2093 + [[package]] 955 2094 name = "stable_deref_trait" 956 2095 version = "1.2.1" 957 2096 source = "registry+https://github.com/rust-lang/crates.io-index" 958 2097 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 959 2098 960 2099 [[package]] 2100 + name = "stringprep" 2101 + version = "0.1.5" 2102 + source = "registry+https://github.com/rust-lang/crates.io-index" 2103 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 2104 + dependencies = [ 2105 + "unicode-bidi", 2106 + "unicode-normalization", 2107 + "unicode-properties", 2108 + ] 2109 + 2110 + [[package]] 961 2111 name = "subtle" 962 2112 version = "2.6.1" 963 2113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 965 2115 966 2116 [[package]] 967 2117 name = "syn" 2118 + version = "1.0.109" 2119 + source = "registry+https://github.com/rust-lang/crates.io-index" 2120 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 2121 + dependencies = [ 2122 + "proc-macro2", 2123 + "quote", 2124 + "unicode-ident", 2125 + ] 2126 + 2127 + [[package]] 2128 + name = "syn" 968 2129 version = "2.0.111" 969 2130 source = "registry+https://github.com/rust-lang/crates.io-index" 970 2131 checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" ··· 991 2152 dependencies = [ 992 2153 "proc-macro2", 993 2154 "quote", 994 - "syn", 2155 + "syn 2.0.111", 995 2156 ] 996 2157 997 2158 [[package]] ··· 1020 2181 dependencies = [ 1021 2182 "proc-macro2", 1022 2183 "quote", 1023 - "syn", 2184 + "syn 2.0.111", 1024 2185 ] 1025 2186 1026 2187 [[package]] ··· 1031 2192 dependencies = [ 1032 2193 "proc-macro2", 1033 2194 "quote", 1034 - "syn", 2195 + "syn 2.0.111", 1035 2196 ] 1036 2197 1037 2198 [[package]] ··· 1092 2253 dependencies = [ 1093 2254 "proc-macro2", 1094 2255 "quote", 1095 - "syn", 2256 + "syn 2.0.111", 1096 2257 ] 1097 2258 1098 2259 [[package]] ··· 1106 2267 ] 1107 2268 1108 2269 [[package]] 2270 + name = "tokio-stream" 2271 + version = "0.1.17" 2272 + source = "registry+https://github.com/rust-lang/crates.io-index" 2273 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2274 + dependencies = [ 2275 + "futures-core", 2276 + "pin-project-lite", 2277 + "tokio", 2278 + "tokio-util", 2279 + ] 2280 + 2281 + [[package]] 2282 + name = "tokio-tungstenite" 2283 + version = "0.24.0" 2284 + source = "registry+https://github.com/rust-lang/crates.io-index" 2285 + checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" 2286 + dependencies = [ 2287 + "futures-util", 2288 + "log", 2289 + "tokio", 2290 + "tungstenite", 2291 + ] 2292 + 2293 + [[package]] 2294 + name = "tokio-util" 2295 + version = "0.7.17" 2296 + source = "registry+https://github.com/rust-lang/crates.io-index" 2297 + checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 2298 + dependencies = [ 2299 + "bytes", 2300 + "futures-core", 2301 + "futures-sink", 2302 + "pin-project-lite", 2303 + "tokio", 2304 + ] 2305 + 2306 + [[package]] 1109 2307 name = "tower" 1110 2308 version = "0.5.2" 1111 2309 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1171 2369 dependencies = [ 1172 2370 "proc-macro2", 1173 2371 "quote", 1174 - "syn", 2372 + "syn 2.0.111", 1175 2373 ] 1176 2374 1177 2375 [[package]] ··· 1220 2418 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1221 2419 1222 2420 [[package]] 2421 + name = "tungstenite" 2422 + version = "0.24.0" 2423 + source = "registry+https://github.com/rust-lang/crates.io-index" 2424 + checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" 2425 + dependencies = [ 2426 + "byteorder", 2427 + "bytes", 2428 + "data-encoding", 2429 + "http", 2430 + "httparse", 2431 + "log", 2432 + "rand 0.8.5", 2433 + "sha1", 2434 + "thiserror 1.0.69", 2435 + "utf-8", 2436 + ] 2437 + 2438 + [[package]] 2439 + name = "typenum" 2440 + version = "1.19.0" 2441 + source = "registry+https://github.com/rust-lang/crates.io-index" 2442 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 2443 + 2444 + [[package]] 2445 + name = "unicode-bidi" 2446 + version = "0.3.18" 2447 + source = "registry+https://github.com/rust-lang/crates.io-index" 2448 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 2449 + 2450 + [[package]] 1223 2451 name = "unicode-ident" 1224 2452 version = "1.0.22" 1225 2453 source = "registry+https://github.com/rust-lang/crates.io-index" 1226 2454 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1227 2455 1228 2456 [[package]] 2457 + name = "unicode-normalization" 2458 + version = "0.1.25" 2459 + source = "registry+https://github.com/rust-lang/crates.io-index" 2460 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 2461 + dependencies = [ 2462 + "tinyvec", 2463 + ] 2464 + 2465 + [[package]] 2466 + name = "unicode-properties" 2467 + version = "0.1.4" 2468 + source = "registry+https://github.com/rust-lang/crates.io-index" 2469 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 2470 + 2471 + [[package]] 2472 + name = "unsigned-varint" 2473 + version = "0.8.0" 2474 + source = "registry+https://github.com/rust-lang/crates.io-index" 2475 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2476 + 2477 + [[package]] 1229 2478 name = "untrusted" 1230 2479 version = "0.9.0" 1231 2480 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1242 2491 "percent-encoding", 1243 2492 "serde", 1244 2493 ] 2494 + 2495 + [[package]] 2496 + name = "utf-8" 2497 + version = "0.7.6" 2498 + source = "registry+https://github.com/rust-lang/crates.io-index" 2499 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1245 2500 1246 2501 [[package]] 1247 2502 name = "utf8_iter" ··· 1256 2511 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1257 2512 1258 2513 [[package]] 2514 + name = "vcpkg" 2515 + version = "0.2.15" 2516 + source = "registry+https://github.com/rust-lang/crates.io-index" 2517 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2518 + 2519 + [[package]] 2520 + name = "version_check" 2521 + version = "0.9.5" 2522 + source = "registry+https://github.com/rust-lang/crates.io-index" 2523 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2524 + 2525 + [[package]] 1259 2526 name = "want" 1260 2527 version = "0.3.1" 1261 2528 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1280 2547 ] 1281 2548 1282 2549 [[package]] 2550 + name = "wasite" 2551 + version = "0.1.0" 2552 + source = "registry+https://github.com/rust-lang/crates.io-index" 2553 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2554 + 2555 + [[package]] 1283 2556 name = "wasm-bindgen" 1284 2557 version = "0.2.105" 1285 2558 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1324 2597 "bumpalo", 1325 2598 "proc-macro2", 1326 2599 "quote", 1327 - "syn", 2600 + "syn 2.0.111", 1328 2601 "wasm-bindgen-shared", 1329 2602 ] 1330 2603 ··· 1367 2640 ] 1368 2641 1369 2642 [[package]] 2643 + name = "whoami" 2644 + version = "1.6.1" 2645 + source = "registry+https://github.com/rust-lang/crates.io-index" 2646 + checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 2647 + dependencies = [ 2648 + "libredox", 2649 + "wasite", 2650 + ] 2651 + 2652 + [[package]] 2653 + name = "windows-core" 2654 + version = "0.62.2" 2655 + source = "registry+https://github.com/rust-lang/crates.io-index" 2656 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 2657 + dependencies = [ 2658 + "windows-implement", 2659 + "windows-interface", 2660 + "windows-link", 2661 + "windows-result", 2662 + "windows-strings", 2663 + ] 2664 + 2665 + [[package]] 2666 + name = "windows-implement" 2667 + version = "0.60.2" 2668 + source = "registry+https://github.com/rust-lang/crates.io-index" 2669 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 2670 + dependencies = [ 2671 + "proc-macro2", 2672 + "quote", 2673 + "syn 2.0.111", 2674 + ] 2675 + 2676 + [[package]] 2677 + name = "windows-interface" 2678 + version = "0.59.3" 2679 + source = "registry+https://github.com/rust-lang/crates.io-index" 2680 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 2681 + dependencies = [ 2682 + "proc-macro2", 2683 + "quote", 2684 + "syn 2.0.111", 2685 + ] 2686 + 2687 + [[package]] 1370 2688 name = "windows-link" 1371 2689 version = "0.2.1" 1372 2690 source = "registry+https://github.com/rust-lang/crates.io-index" 1373 2691 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1374 2692 1375 2693 [[package]] 2694 + name = "windows-result" 2695 + version = "0.4.1" 2696 + source = "registry+https://github.com/rust-lang/crates.io-index" 2697 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 2698 + dependencies = [ 2699 + "windows-link", 2700 + ] 2701 + 2702 + [[package]] 2703 + name = "windows-strings" 2704 + version = "0.5.1" 2705 + source = "registry+https://github.com/rust-lang/crates.io-index" 2706 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 2707 + dependencies = [ 2708 + "windows-link", 2709 + ] 2710 + 2711 + [[package]] 2712 + name = "windows-sys" 2713 + version = "0.48.0" 2714 + source = "registry+https://github.com/rust-lang/crates.io-index" 2715 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2716 + dependencies = [ 2717 + "windows-targets 0.48.5", 2718 + ] 2719 + 2720 + [[package]] 1376 2721 name = "windows-sys" 1377 2722 version = "0.52.0" 1378 2723 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1401 2746 1402 2747 [[package]] 1403 2748 name = "windows-targets" 2749 + version = "0.48.5" 2750 + source = "registry+https://github.com/rust-lang/crates.io-index" 2751 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2752 + dependencies = [ 2753 + "windows_aarch64_gnullvm 0.48.5", 2754 + "windows_aarch64_msvc 0.48.5", 2755 + "windows_i686_gnu 0.48.5", 2756 + "windows_i686_msvc 0.48.5", 2757 + "windows_x86_64_gnu 0.48.5", 2758 + "windows_x86_64_gnullvm 0.48.5", 2759 + "windows_x86_64_msvc 0.48.5", 2760 + ] 2761 + 2762 + [[package]] 2763 + name = "windows-targets" 1404 2764 version = "0.52.6" 1405 2765 source = "registry+https://github.com/rust-lang/crates.io-index" 1406 2766 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" ··· 1434 2794 1435 2795 [[package]] 1436 2796 name = "windows_aarch64_gnullvm" 2797 + version = "0.48.5" 2798 + source = "registry+https://github.com/rust-lang/crates.io-index" 2799 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2800 + 2801 + [[package]] 2802 + name = "windows_aarch64_gnullvm" 1437 2803 version = "0.52.6" 1438 2804 source = "registry+https://github.com/rust-lang/crates.io-index" 1439 2805 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" ··· 1446 2812 1447 2813 [[package]] 1448 2814 name = "windows_aarch64_msvc" 2815 + version = "0.48.5" 2816 + source = "registry+https://github.com/rust-lang/crates.io-index" 2817 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2818 + 2819 + [[package]] 2820 + name = "windows_aarch64_msvc" 1449 2821 version = "0.52.6" 1450 2822 source = "registry+https://github.com/rust-lang/crates.io-index" 1451 2823 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" ··· 1458 2830 1459 2831 [[package]] 1460 2832 name = "windows_i686_gnu" 2833 + version = "0.48.5" 2834 + source = "registry+https://github.com/rust-lang/crates.io-index" 2835 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2836 + 2837 + [[package]] 2838 + name = "windows_i686_gnu" 1461 2839 version = "0.52.6" 1462 2840 source = "registry+https://github.com/rust-lang/crates.io-index" 1463 2841 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" ··· 1482 2860 1483 2861 [[package]] 1484 2862 name = "windows_i686_msvc" 2863 + version = "0.48.5" 2864 + source = "registry+https://github.com/rust-lang/crates.io-index" 2865 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2866 + 2867 + [[package]] 2868 + name = "windows_i686_msvc" 1485 2869 version = "0.52.6" 1486 2870 source = "registry+https://github.com/rust-lang/crates.io-index" 1487 2871 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" ··· 1494 2878 1495 2879 [[package]] 1496 2880 name = "windows_x86_64_gnu" 2881 + version = "0.48.5" 2882 + source = "registry+https://github.com/rust-lang/crates.io-index" 2883 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2884 + 2885 + [[package]] 2886 + name = "windows_x86_64_gnu" 1497 2887 version = "0.52.6" 1498 2888 source = "registry+https://github.com/rust-lang/crates.io-index" 1499 2889 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" ··· 1506 2896 1507 2897 [[package]] 1508 2898 name = "windows_x86_64_gnullvm" 2899 + version = "0.48.5" 2900 + source = "registry+https://github.com/rust-lang/crates.io-index" 2901 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2902 + 2903 + [[package]] 2904 + name = "windows_x86_64_gnullvm" 1509 2905 version = "0.52.6" 1510 2906 source = "registry+https://github.com/rust-lang/crates.io-index" 1511 2907 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" ··· 1518 2914 1519 2915 [[package]] 1520 2916 name = "windows_x86_64_msvc" 2917 + version = "0.48.5" 2918 + source = "registry+https://github.com/rust-lang/crates.io-index" 2919 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2920 + 2921 + [[package]] 2922 + name = "windows_x86_64_msvc" 1521 2923 version = "0.52.6" 1522 2924 source = "registry+https://github.com/rust-lang/crates.io-index" 1523 2925 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" ··· 1559 2961 dependencies = [ 1560 2962 "proc-macro2", 1561 2963 "quote", 1562 - "syn", 2964 + "syn 2.0.111", 1563 2965 "synstructure", 1564 2966 ] 1565 2967 ··· 1580 2982 dependencies = [ 1581 2983 "proc-macro2", 1582 2984 "quote", 1583 - "syn", 2985 + "syn 2.0.111", 1584 2986 ] 1585 2987 1586 2988 [[package]] ··· 1600 3002 dependencies = [ 1601 3003 "proc-macro2", 1602 3004 "quote", 1603 - "syn", 3005 + "syn 2.0.111", 1604 3006 "synstructure", 1605 3007 ] 1606 3008 ··· 1640 3042 dependencies = [ 1641 3043 "proc-macro2", 1642 3044 "quote", 1643 - "syn", 3045 + "syn 2.0.111", 1644 3046 ]
+15 -3
moderation/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 anyhow = "1.0" 8 - axum = { version = "0.7", features = ["macros", "json"] } 8 + axum = { version = "0.7", features = ["macros", "json", "ws"] } 9 + bytes = "1.0" 10 + chrono = { version = "0.4", features = ["serde"] } 11 + futures = "0.3" 12 + hex = "0.4" 13 + k256 = { version = "0.13", features = ["ecdsa"] } 9 14 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 10 15 serde = { version = "1.0", features = ["derive"] } 16 + serde_bytes = "0.11" 17 + serde_ipld_dagcbor = "0.6" 11 18 serde_json = "1.0" 12 - thiserror = "1.0" 13 - tokio = { version = "1.40", features = ["rt-multi-thread", "macros", "signal"] } 19 + sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "tls-rustls"] } 20 + thiserror = "2.0" 21 + tokio = { version = "1.40", features = ["rt-multi-thread", "macros", "signal", "sync"] } 22 + tokio-stream = { version = "0.1", features = ["sync"] } 14 23 tracing = "0.1" 15 24 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 25 + 26 + [dev-dependencies] 27 + rand = "0.8"
+1 -1
moderation/Dockerfile
··· 1 - FROM rust:1.83-slim as builder 1 + FROM rust:1.85-slim as builder 2 2 3 3 WORKDIR /app 4 4 COPY Cargo.toml Cargo.lock* ./
+252
moderation/src/db.rs
··· 1 + //! Database operations for the labeler. 2 + 3 + use chrono::{DateTime, Utc}; 4 + use sqlx::{postgres::PgPoolOptions, PgPool}; 5 + 6 + use crate::labels::Label; 7 + 8 + /// Database connection pool and operations. 9 + #[derive(Clone)] 10 + pub struct LabelDb { 11 + pool: PgPool, 12 + } 13 + 14 + /// Stored label row from the database. 15 + #[derive(Debug, Clone, sqlx::FromRow)] 16 + pub struct LabelRow { 17 + pub seq: i64, 18 + pub src: String, 19 + pub uri: String, 20 + pub cid: Option<String>, 21 + pub val: String, 22 + pub neg: bool, 23 + pub cts: DateTime<Utc>, 24 + pub exp: Option<DateTime<Utc>>, 25 + pub sig: Vec<u8>, 26 + } 27 + 28 + impl LabelDb { 29 + /// Connect to the database. 30 + pub async fn connect(database_url: &str) -> Result<Self, sqlx::Error> { 31 + let pool = PgPoolOptions::new() 32 + .max_connections(5) 33 + .connect(database_url) 34 + .await?; 35 + Ok(Self { pool }) 36 + } 37 + 38 + /// Run database migrations. 39 + pub async fn migrate(&self) -> Result<(), sqlx::Error> { 40 + sqlx::query( 41 + r#" 42 + CREATE TABLE IF NOT EXISTS labels ( 43 + id BIGSERIAL PRIMARY KEY, 44 + seq BIGSERIAL UNIQUE NOT NULL, 45 + src TEXT NOT NULL, 46 + uri TEXT NOT NULL, 47 + cid TEXT, 48 + val TEXT NOT NULL, 49 + neg BOOLEAN NOT NULL DEFAULT FALSE, 50 + cts TIMESTAMPTZ NOT NULL, 51 + exp TIMESTAMPTZ, 52 + sig BYTEA NOT NULL, 53 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 54 + ) 55 + "#, 56 + ) 57 + .execute(&self.pool) 58 + .await?; 59 + 60 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_labels_uri ON labels(uri)") 61 + .execute(&self.pool) 62 + .await?; 63 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_labels_src ON labels(src)") 64 + .execute(&self.pool) 65 + .await?; 66 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_labels_seq ON labels(seq)") 67 + .execute(&self.pool) 68 + .await?; 69 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_labels_val ON labels(val)") 70 + .execute(&self.pool) 71 + .await?; 72 + 73 + Ok(()) 74 + } 75 + 76 + /// Store a signed label and return its sequence number. 77 + pub async fn store_label(&self, label: &Label) -> Result<i64, sqlx::Error> { 78 + let sig = label.sig.as_ref().map(|b| b.to_vec()).unwrap_or_default(); 79 + let cts: DateTime<Utc> = label 80 + .cts 81 + .parse() 82 + .unwrap_or_else(|_| Utc::now()); 83 + let exp: Option<DateTime<Utc>> = label.exp.as_ref().and_then(|e| e.parse().ok()); 84 + 85 + let row = sqlx::query_scalar::<_, i64>( 86 + r#" 87 + INSERT INTO labels (src, uri, cid, val, neg, cts, exp, sig) 88 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 89 + RETURNING seq 90 + "#, 91 + ) 92 + .bind(&label.src) 93 + .bind(&label.uri) 94 + .bind(&label.cid) 95 + .bind(&label.val) 96 + .bind(label.neg.unwrap_or(false)) 97 + .bind(cts) 98 + .bind(exp) 99 + .bind(sig) 100 + .fetch_one(&self.pool) 101 + .await?; 102 + 103 + Ok(row) 104 + } 105 + 106 + /// Query labels matching URI patterns. 107 + /// 108 + /// Patterns can contain `*` as a wildcard (e.g., `at://did:plc:*`). 109 + pub async fn query_labels( 110 + &self, 111 + uri_patterns: &[String], 112 + sources: Option<&[String]>, 113 + cursor: Option<&str>, 114 + limit: i64, 115 + ) -> Result<(Vec<LabelRow>, Option<String>), sqlx::Error> { 116 + // Build dynamic query 117 + let mut conditions = Vec::new(); 118 + let mut param_idx = 1; 119 + 120 + // URI pattern matching 121 + let uri_conditions: Vec<String> = uri_patterns 122 + .iter() 123 + .map(|p| { 124 + let idx = param_idx; 125 + param_idx += 1; 126 + if p.contains('*') { 127 + format!("uri LIKE ${}", idx) 128 + } else { 129 + format!("uri = ${}", idx) 130 + } 131 + }) 132 + .collect(); 133 + 134 + if !uri_conditions.is_empty() { 135 + conditions.push(format!("({})", uri_conditions.join(" OR "))); 136 + } 137 + 138 + // Source filtering 139 + if let Some(srcs) = sources { 140 + if !srcs.is_empty() { 141 + let placeholders: Vec<String> = srcs 142 + .iter() 143 + .map(|_| { 144 + let idx = param_idx; 145 + param_idx += 1; 146 + format!("${}", idx) 147 + }) 148 + .collect(); 149 + conditions.push(format!("src IN ({})", placeholders.join(", "))); 150 + } 151 + } 152 + 153 + // Cursor for pagination 154 + if cursor.is_some() { 155 + conditions.push(format!("seq > ${}", param_idx)); 156 + } 157 + 158 + let where_clause = if conditions.is_empty() { 159 + String::new() 160 + } else { 161 + format!("WHERE {}", conditions.join(" AND ")) 162 + }; 163 + 164 + let query = format!( 165 + r#" 166 + SELECT seq, src, uri, cid, val, neg, cts, exp, sig 167 + FROM labels 168 + {} 169 + ORDER BY seq ASC 170 + LIMIT {} 171 + "#, 172 + where_clause, 173 + limit + 1 // Fetch one extra to determine if there's more 174 + ); 175 + 176 + // Build query with parameters 177 + let mut q = sqlx::query_as::<_, LabelRow>(&query); 178 + 179 + // Bind URI patterns (converting * to %) 180 + for pattern in uri_patterns { 181 + let sql_pattern = pattern.replace('*', "%"); 182 + q = q.bind(sql_pattern); 183 + } 184 + 185 + // Bind sources 186 + if let Some(srcs) = sources { 187 + for src in srcs { 188 + q = q.bind(src); 189 + } 190 + } 191 + 192 + // Bind cursor 193 + if let Some(c) = cursor { 194 + let cursor_seq: i64 = c.parse().unwrap_or(0); 195 + q = q.bind(cursor_seq); 196 + } 197 + 198 + let mut rows: Vec<LabelRow> = q.fetch_all(&self.pool).await?; 199 + 200 + // Determine next cursor 201 + let next_cursor = if rows.len() > limit as usize { 202 + rows.pop(); // Remove the extra row 203 + rows.last().map(|r| r.seq.to_string()) 204 + } else { 205 + None 206 + }; 207 + 208 + Ok((rows, next_cursor)) 209 + } 210 + 211 + /// Get labels since a sequence number (for subscribeLabels). 212 + pub async fn get_labels_since(&self, cursor: i64, limit: i64) -> Result<Vec<LabelRow>, sqlx::Error> { 213 + sqlx::query_as::<_, LabelRow>( 214 + r#" 215 + SELECT seq, src, uri, cid, val, neg, cts, exp, sig 216 + FROM labels 217 + WHERE seq > $1 218 + ORDER BY seq ASC 219 + LIMIT $2 220 + "#, 221 + ) 222 + .bind(cursor) 223 + .bind(limit) 224 + .fetch_all(&self.pool) 225 + .await 226 + } 227 + 228 + /// Get the latest sequence number. 229 + pub async fn get_latest_seq(&self) -> Result<i64, sqlx::Error> { 230 + sqlx::query_scalar::<_, Option<i64>>("SELECT MAX(seq) FROM labels") 231 + .fetch_one(&self.pool) 232 + .await 233 + .map(|s| s.unwrap_or(0)) 234 + } 235 + } 236 + 237 + impl LabelRow { 238 + /// Convert database row to Label struct. 239 + pub fn to_label(&self) -> Label { 240 + Label { 241 + ver: Some(1), 242 + src: self.src.clone(), 243 + uri: self.uri.clone(), 244 + cid: self.cid.clone(), 245 + val: self.val.clone(), 246 + neg: if self.neg { Some(true) } else { None }, 247 + cts: self.cts.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), 248 + exp: self.exp.map(|e| e.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()), 249 + sig: Some(bytes::Bytes::from(self.sig.clone())), 250 + } 251 + } 252 + }
+228
moderation/src/labels.rs
··· 1 + //! ATProto label types and signing. 2 + //! 3 + //! Labels are signed metadata tags that can be applied to ATProto resources. 4 + //! This module implements the com.atproto.label.defs#label schema. 5 + 6 + use bytes::Bytes; 7 + use chrono::Utc; 8 + use k256::ecdsa::{signature::Signer, Signature, SigningKey}; 9 + use serde::{Deserialize, Serialize}; 10 + 11 + /// ATProto label as defined in com.atproto.label.defs#label. 12 + /// 13 + /// Labels are signed by the labeler's `#atproto_label` key. 14 + #[derive(Debug, Clone, Serialize, Deserialize)] 15 + #[serde(rename_all = "camelCase")] 16 + pub struct Label { 17 + /// Version of the label format (currently 1). 18 + #[serde(skip_serializing_if = "Option::is_none")] 19 + pub ver: Option<i64>, 20 + 21 + /// DID of the labeler that created this label. 22 + pub src: String, 23 + 24 + /// AT URI of the resource this label applies to. 25 + pub uri: String, 26 + 27 + /// CID of the specific version (optional). 28 + #[serde(skip_serializing_if = "Option::is_none")] 29 + pub cid: Option<String>, 30 + 31 + /// The label value (e.g., "copyright-violation"). 32 + pub val: String, 33 + 34 + /// If true, this negates a previous label. 35 + #[serde(skip_serializing_if = "Option::is_none")] 36 + pub neg: Option<bool>, 37 + 38 + /// Timestamp when label was created (ISO 8601). 39 + pub cts: String, 40 + 41 + /// Expiration timestamp (optional). 42 + #[serde(skip_serializing_if = "Option::is_none")] 43 + pub exp: Option<String>, 44 + 45 + /// DAG-CBOR signature of the label. 46 + #[serde(skip_serializing_if = "Option::is_none")] 47 + #[serde(with = "serde_bytes_opt")] 48 + pub sig: Option<Bytes>, 49 + } 50 + 51 + mod serde_bytes_opt { 52 + use bytes::Bytes; 53 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 54 + 55 + pub fn serialize<S>(value: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error> 56 + where 57 + S: Serializer, 58 + { 59 + match value { 60 + Some(bytes) => serde_bytes::Bytes::new(bytes.as_ref()).serialize(serializer), 61 + None => serializer.serialize_none(), 62 + } 63 + } 64 + 65 + pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error> 66 + where 67 + D: Deserializer<'de>, 68 + { 69 + let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?; 70 + Ok(opt.map(|b| Bytes::from(b.into_vec()))) 71 + } 72 + } 73 + 74 + impl Label { 75 + /// Create a new unsigned label. 76 + pub fn new(src: impl Into<String>, uri: impl Into<String>, val: impl Into<String>) -> Self { 77 + Self { 78 + ver: Some(1), 79 + src: src.into(), 80 + uri: uri.into(), 81 + cid: None, 82 + val: val.into(), 83 + neg: None, 84 + cts: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), 85 + exp: None, 86 + sig: None, 87 + } 88 + } 89 + 90 + /// Set the CID for a specific version of the resource. 91 + pub fn with_cid(mut self, cid: impl Into<String>) -> Self { 92 + self.cid = Some(cid.into()); 93 + self 94 + } 95 + 96 + /// Set this as a negation label. 97 + pub fn negated(mut self) -> Self { 98 + self.neg = Some(true); 99 + self 100 + } 101 + 102 + /// Sign this label with a secp256k1 key. 103 + /// 104 + /// The signing process: 105 + /// 1. Serialize the label without the `sig` field to DAG-CBOR 106 + /// 2. Sign the bytes with the secp256k1 key 107 + /// 3. Attach the signature 108 + pub fn sign(mut self, signing_key: &SigningKey) -> Result<Self, LabelError> { 109 + // Create unsigned version for signing 110 + let unsigned = UnsignedLabel { 111 + ver: self.ver, 112 + src: &self.src, 113 + uri: &self.uri, 114 + cid: self.cid.as_deref(), 115 + val: &self.val, 116 + neg: self.neg, 117 + cts: &self.cts, 118 + exp: self.exp.as_deref(), 119 + }; 120 + 121 + // Encode to DAG-CBOR 122 + let cbor_bytes = 123 + serde_ipld_dagcbor::to_vec(&unsigned).map_err(|e| LabelError::Serialization(e))?; 124 + 125 + // Sign with secp256k1 126 + let signature: Signature = signing_key.sign(&cbor_bytes); 127 + self.sig = Some(Bytes::copy_from_slice(&signature.to_bytes())); 128 + 129 + Ok(self) 130 + } 131 + } 132 + 133 + /// Unsigned label for serialization during signing. 134 + #[derive(Serialize)] 135 + #[serde(rename_all = "camelCase")] 136 + struct UnsignedLabel<'a> { 137 + #[serde(skip_serializing_if = "Option::is_none")] 138 + ver: Option<i64>, 139 + src: &'a str, 140 + uri: &'a str, 141 + #[serde(skip_serializing_if = "Option::is_none")] 142 + cid: Option<&'a str>, 143 + val: &'a str, 144 + #[serde(skip_serializing_if = "Option::is_none")] 145 + neg: Option<bool>, 146 + cts: &'a str, 147 + #[serde(skip_serializing_if = "Option::is_none")] 148 + exp: Option<&'a str>, 149 + } 150 + 151 + /// Label-related errors. 152 + #[derive(Debug, thiserror::Error)] 153 + pub enum LabelError { 154 + #[error("failed to serialize label: {0}")] 155 + Serialization(#[from] serde_ipld_dagcbor::EncodeError<std::collections::TryReserveError>), 156 + 157 + #[error("invalid signing key: {0}")] 158 + InvalidKey(String), 159 + 160 + #[error("database error: {0}")] 161 + Database(#[from] sqlx::Error), 162 + } 163 + 164 + /// Label signer that holds the signing key and labeler DID. 165 + #[derive(Clone)] 166 + pub struct LabelSigner { 167 + signing_key: SigningKey, 168 + labeler_did: String, 169 + } 170 + 171 + impl LabelSigner { 172 + /// Create a new label signer from a hex-encoded private key. 173 + pub fn from_hex(hex_key: &str, labeler_did: impl Into<String>) -> Result<Self, LabelError> { 174 + let key_bytes = hex::decode(hex_key) 175 + .map_err(|e| LabelError::InvalidKey(format!("invalid hex: {e}")))?; 176 + let signing_key = SigningKey::from_slice(&key_bytes) 177 + .map_err(|e| LabelError::InvalidKey(format!("invalid key: {e}")))?; 178 + Ok(Self { 179 + signing_key, 180 + labeler_did: labeler_did.into(), 181 + }) 182 + } 183 + 184 + /// Get the labeler DID. 185 + pub fn did(&self) -> &str { 186 + &self.labeler_did 187 + } 188 + 189 + /// Sign an arbitrary label. 190 + pub fn sign_label(&self, label: Label) -> Result<Label, LabelError> { 191 + label.sign(&self.signing_key) 192 + } 193 + } 194 + 195 + #[cfg(test)] 196 + mod tests { 197 + use super::*; 198 + 199 + #[test] 200 + fn test_label_creation() { 201 + let label = Label::new( 202 + "did:plc:test", 203 + "at://did:plc:user/fm.plyr.track/abc123", 204 + "copyright-violation", 205 + ); 206 + 207 + assert_eq!(label.src, "did:plc:test"); 208 + assert_eq!(label.val, "copyright-violation"); 209 + assert!(label.sig.is_none()); 210 + } 211 + 212 + #[test] 213 + fn test_label_signing() { 214 + // Generate a test key 215 + let signing_key = SigningKey::random(&mut rand::thread_rng()); 216 + 217 + let label = Label::new( 218 + "did:plc:test", 219 + "at://did:plc:user/fm.plyr.track/abc123", 220 + "copyright-violation", 221 + ) 222 + .sign(&signing_key) 223 + .unwrap(); 224 + 225 + assert!(label.sig.is_some()); 226 + assert_eq!(label.sig.as_ref().unwrap().len(), 64); // secp256k1 signature is 64 bytes 227 + } 228 + }
+388 -20
moderation/src/main.rs
··· 1 - use std::{env, net::SocketAddr}; 1 + //! plyr.fm moderation service 2 + //! 3 + //! Provides: 4 + //! - AuDD audio fingerprinting for copyright detection 5 + //! - ATProto labeler endpoints (queryLabels, subscribeLabels) 6 + //! - Label emission for copyright violations 7 + 8 + use std::{env, net::SocketAddr, sync::Arc}; 2 9 3 10 use anyhow::anyhow; 4 11 use axum::{ 5 - extract::Request, 12 + extract::{ 13 + ws::{Message, WebSocket, WebSocketUpgrade}, 14 + Query, Request, State, 15 + }, 6 16 http::StatusCode, 7 17 middleware::{self, Next}, 8 18 response::{IntoResponse, Response}, 9 19 routing::{get, post}, 10 20 Json, Router, 11 21 }; 22 + use futures::StreamExt; 12 23 use serde::{Deserialize, Serialize}; 13 - use tokio::net::TcpListener; 24 + use tokio::{net::TcpListener, sync::broadcast}; 25 + use tokio_stream::wrappers::BroadcastStream; 14 26 use tracing::{error, info, warn}; 15 27 28 + mod db; 29 + mod labels; 30 + 31 + use db::LabelDb; 32 + use labels::{Label, LabelSigner}; 33 + 16 34 // --- config --- 17 35 18 36 struct Config { ··· 21 39 auth_token: Option<String>, 22 40 audd_api_token: String, 23 41 audd_api_url: String, 42 + database_url: Option<String>, 43 + labeler_did: Option<String>, 44 + labeler_signing_key: Option<String>, 24 45 } 25 46 26 47 impl Config { 27 48 fn from_env() -> anyhow::Result<Self> { 28 49 Ok(Self { 29 - host: env::var("MODERATION_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), 50 + host: env::var("MODERATION_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), 30 51 port: env::var("MODERATION_PORT") 31 52 .ok() 32 53 .and_then(|v| v.parse().ok()) ··· 36 57 .map_err(|_| anyhow!("MODERATION_AUDD_API_TOKEN is required"))?, 37 58 audd_api_url: env::var("MODERATION_AUDD_API_URL") 38 59 .unwrap_or_else(|_| "https://enterprise.audd.io/".to_string()), 60 + database_url: env::var("MODERATION_DATABASE_URL").ok(), 61 + labeler_did: env::var("MODERATION_LABELER_DID").ok(), 62 + labeler_signing_key: env::var("MODERATION_LABELER_SIGNING_KEY").ok(), 39 63 }) 64 + } 65 + 66 + fn labeler_enabled(&self) -> bool { 67 + self.database_url.is_some() 68 + && self.labeler_did.is_some() 69 + && self.labeler_signing_key.is_some() 40 70 } 41 71 } 42 72 ··· 73 103 #[derive(Debug, Serialize)] 74 104 struct HealthResponse { 75 105 status: &'static str, 106 + labeler_enabled: bool, 107 + } 108 + 109 + // --- emit label request --- 110 + 111 + #[derive(Debug, Deserialize)] 112 + struct EmitLabelRequest { 113 + /// AT URI of the resource to label (e.g., at://did:plc:xxx/fm.plyr.track/abc123) 114 + uri: String, 115 + /// Label value (e.g., "copyright-violation") 116 + #[serde(default = "default_label_val")] 117 + val: String, 118 + /// Optional CID of specific version 119 + cid: Option<String>, 120 + /// If true, negate an existing label 121 + #[serde(default)] 122 + neg: bool, 123 + } 124 + 125 + fn default_label_val() -> String { 126 + "copyright-violation".to_string() 127 + } 128 + 129 + #[derive(Debug, Serialize)] 130 + struct EmitLabelResponse { 131 + seq: i64, 132 + label: Label, 133 + } 134 + 135 + // --- xrpc types --- 136 + 137 + #[derive(Debug, Deserialize)] 138 + #[serde(rename_all = "camelCase")] 139 + struct QueryLabelsParams { 140 + uri_patterns: String, // comma-separated 141 + sources: Option<String>, 142 + cursor: Option<String>, 143 + limit: Option<i64>, 144 + } 145 + 146 + #[derive(Debug, Serialize)] 147 + struct QueryLabelsResponse { 148 + cursor: Option<String>, 149 + labels: Vec<Label>, 150 + } 151 + 152 + #[derive(Debug, Deserialize)] 153 + struct SubscribeLabelsParams { 154 + cursor: Option<i64>, 76 155 } 77 156 78 157 // --- audd api types --- ··· 92 171 93 172 #[derive(Debug, Deserialize)] 94 173 struct AuddGroup { 95 - offset: Option<serde_json::Value>, // can be i64 (ms) or string like "00:00" 174 + offset: Option<serde_json::Value>, 96 175 songs: Option<Vec<AuddSong>>, 97 176 } 98 177 99 178 #[derive(Debug, Deserialize)] 100 - #[allow(dead_code)] // fields needed for deserialization 179 + #[allow(dead_code)] 101 180 struct AuddSong { 102 181 artist: Option<String>, 103 182 title: Option<String>, ··· 122 201 let config = Config::from_env()?; 123 202 let auth_token = config.auth_token.clone(); 124 203 204 + // Initialize labeler components if configured 205 + let (db, signer, label_tx) = if config.labeler_enabled() { 206 + let db = LabelDb::connect(config.database_url.as_ref().unwrap()).await?; 207 + db.migrate().await?; 208 + info!("labeler database connected and migrated"); 209 + 210 + let signer = LabelSigner::from_hex( 211 + config.labeler_signing_key.as_ref().unwrap(), 212 + config.labeler_did.as_ref().unwrap(), 213 + )?; 214 + info!(did = %signer.did(), "labeler signer initialized"); 215 + 216 + let (tx, _) = broadcast::channel::<(i64, Label)>(1024); 217 + (Some(db), Some(signer), Some(tx)) 218 + } else { 219 + warn!("labeler not configured - XRPC endpoints will return 503"); 220 + (None, None, None) 221 + }; 222 + 223 + let state = AppState { 224 + audd_api_token: config.audd_api_token, 225 + audd_api_url: config.audd_api_url, 226 + db: db.map(Arc::new), 227 + signer: signer.map(Arc::new), 228 + label_tx, 229 + }; 230 + 125 231 let app = Router::new() 232 + // Landing page 233 + .route("/", get(landing)) 234 + // Health check 126 235 .route("/health", get(health)) 236 + // AuDD scanning (existing) 127 237 .route("/scan", post(scan)) 238 + // Label emission (internal API) 239 + .route("/emit-label", post(emit_label)) 240 + // ATProto XRPC endpoints (public) 241 + .route("/xrpc/com.atproto.label.queryLabels", get(query_labels)) 242 + .route( 243 + "/xrpc/com.atproto.label.subscribeLabels", 244 + get(subscribe_labels), 245 + ) 128 246 .layer(middleware::from_fn(move |req, next| { 129 247 auth_middleware(req, next, auth_token.clone()) 130 248 })) 131 - .with_state(AppState { 132 - audd_api_token: config.audd_api_token, 133 - audd_api_url: config.audd_api_url, 134 - }); 249 + .with_state(state); 135 250 136 251 let addr: SocketAddr = format!("{}:{}", config.host, config.port) 137 252 .parse() ··· 149 264 struct AppState { 150 265 audd_api_token: String, 151 266 audd_api_url: String, 267 + db: Option<Arc<LabelDb>>, 268 + signer: Option<Arc<LabelSigner>>, 269 + label_tx: Option<broadcast::Sender<(i64, Label)>>, 152 270 } 153 271 154 272 // --- middleware --- ··· 158 276 next: Next, 159 277 auth_token: Option<String>, 160 278 ) -> Result<Response, StatusCode> { 161 - if req.uri().path() == "/health" { 279 + let path = req.uri().path(); 280 + 281 + // Public endpoints - no auth required 282 + if path == "/" 283 + || path == "/health" 284 + || path.starts_with("/xrpc/com.atproto.label.") 285 + { 162 286 return Ok(next.run(req).await); 163 287 } 164 288 ··· 187 311 188 312 // --- handlers --- 189 313 190 - async fn health() -> Json<HealthResponse> { 191 - Json(HealthResponse { status: "ok" }) 314 + async fn health(State(state): State<AppState>) -> Json<HealthResponse> { 315 + Json(HealthResponse { 316 + status: "ok", 317 + labeler_enabled: state.db.is_some(), 318 + }) 319 + } 320 + 321 + async fn landing(State(state): State<AppState>) -> axum::response::Html<String> { 322 + let labeler_did = state 323 + .signer 324 + .as_ref() 325 + .map(|s| s.did().to_string()) 326 + .unwrap_or_else(|| "not configured".to_string()); 327 + 328 + axum::response::Html(format!( 329 + r#"<!DOCTYPE html> 330 + <html> 331 + <head> 332 + <meta charset="utf-8"> 333 + <meta name="viewport" content="width=device-width, initial-scale=1"> 334 + <title>plyr.fm moderation</title> 335 + <style> 336 + body {{ 337 + font-family: system-ui, -apple-system, sans-serif; 338 + background: #0a0a0a; 339 + color: #e5e5e5; 340 + max-width: 600px; 341 + margin: 80px auto; 342 + padding: 20px; 343 + line-height: 1.6; 344 + }} 345 + h1 {{ color: #fff; margin-bottom: 8px; }} 346 + .subtitle {{ color: #888; margin-bottom: 32px; }} 347 + a {{ color: #3b82f6; }} 348 + code {{ 349 + background: #1a1a1a; 350 + padding: 2px 6px; 351 + border-radius: 4px; 352 + font-size: 0.9em; 353 + }} 354 + .endpoint {{ 355 + background: #111; 356 + border: 1px solid #222; 357 + border-radius: 8px; 358 + padding: 16px; 359 + margin: 12px 0; 360 + }} 361 + .endpoint-name {{ color: #10b981; font-family: monospace; }} 362 + </style> 363 + </head> 364 + <body> 365 + <h1>plyr.fm moderation</h1> 366 + <p class="subtitle">ATProto labeler for audio content moderation</p> 367 + 368 + <p>This service provides content labels for <a href="https://plyr.fm">plyr.fm</a>, 369 + the music streaming platform on ATProto.</p> 370 + 371 + <p><strong>Labeler DID:</strong> <code>{}</code></p> 372 + 373 + <h2>Endpoints</h2> 374 + 375 + <div class="endpoint"> 376 + <div class="endpoint-name">GET /xrpc/com.atproto.label.queryLabels</div> 377 + <p>Query labels by URI pattern</p> 378 + </div> 379 + 380 + <div class="endpoint"> 381 + <div class="endpoint-name">GET /xrpc/com.atproto.label.subscribeLabels</div> 382 + <p>WebSocket subscription for real-time label updates</p> 383 + </div> 384 + 385 + <p style="margin-top: 32px; color: #666;"> 386 + <a href="https://bsky.app/profile/moderation.plyr.fm">@moderation.plyr.fm</a> 387 + </p> 388 + </body> 389 + </html>"#, 390 + labeler_did 391 + )) 192 392 } 193 393 194 394 async fn scan( 195 - axum::extract::State(state): axum::extract::State<AppState>, 395 + State(state): State<AppState>, 196 396 Json(request): Json<ScanRequest>, 197 397 ) -> Result<Json<ScanResponse>, AppError> { 198 398 info!(audio_url = %request.audio_url, "scanning audio"); ··· 226 426 227 427 let matches = extract_matches(&audd_response); 228 428 let highest_score = matches.iter().map(|m| m.score).max().unwrap_or(0); 229 - // flag if any matches are found - audd enterprise doesn't return confidence scores 230 429 let is_flagged = !matches.is_empty(); 231 430 232 431 info!( ··· 244 443 })) 245 444 } 246 445 446 + async fn emit_label( 447 + State(state): State<AppState>, 448 + Json(request): Json<EmitLabelRequest>, 449 + ) -> Result<Json<EmitLabelResponse>, AppError> { 450 + let db = state.db.as_ref().ok_or(AppError::LabelerNotConfigured)?; 451 + let signer = state.signer.as_ref().ok_or(AppError::LabelerNotConfigured)?; 452 + 453 + info!(uri = %request.uri, val = %request.val, neg = request.neg, "emitting label"); 454 + 455 + // Create and sign the label 456 + let mut label = Label::new(signer.did(), &request.uri, &request.val); 457 + if let Some(cid) = request.cid { 458 + label = label.with_cid(cid); 459 + } 460 + if request.neg { 461 + label = label.negated(); 462 + } 463 + let label = signer.sign_label(label)?; 464 + 465 + // Store in database 466 + let seq = db.store_label(&label).await?; 467 + info!(seq, uri = %request.uri, "label stored"); 468 + 469 + // Broadcast to subscribers 470 + if let Some(tx) = &state.label_tx { 471 + let _ = tx.send((seq, label.clone())); 472 + } 473 + 474 + Ok(Json(EmitLabelResponse { seq, label })) 475 + } 476 + 477 + async fn query_labels( 478 + State(state): State<AppState>, 479 + Query(params): Query<QueryLabelsParams>, 480 + ) -> Result<Json<QueryLabelsResponse>, AppError> { 481 + let db = state.db.as_ref().ok_or(AppError::LabelerNotConfigured)?; 482 + 483 + let uri_patterns: Vec<String> = params 484 + .uri_patterns 485 + .split(',') 486 + .map(|s| s.trim().to_string()) 487 + .collect(); 488 + let sources: Option<Vec<String>> = params 489 + .sources 490 + .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); 491 + let limit = params.limit.unwrap_or(50).min(250).max(1); 492 + 493 + let (rows, cursor) = db 494 + .query_labels( 495 + &uri_patterns, 496 + sources.as_deref(), 497 + params.cursor.as_deref(), 498 + limit, 499 + ) 500 + .await?; 501 + 502 + let labels: Vec<Label> = rows.iter().map(|r| r.to_label()).collect(); 503 + 504 + Ok(Json(QueryLabelsResponse { cursor, labels })) 505 + } 506 + 507 + async fn subscribe_labels( 508 + State(state): State<AppState>, 509 + Query(params): Query<SubscribeLabelsParams>, 510 + ws: WebSocketUpgrade, 511 + ) -> Result<Response, AppError> { 512 + let db = state.db.clone().ok_or(AppError::LabelerNotConfigured)?; 513 + let label_tx = state.label_tx.clone().ok_or(AppError::LabelerNotConfigured)?; 514 + 515 + Ok(ws.on_upgrade(move |socket| handle_subscribe(socket, db, label_tx, params.cursor))) 516 + } 517 + 518 + async fn handle_subscribe( 519 + mut socket: WebSocket, 520 + db: Arc<LabelDb>, 521 + label_tx: broadcast::Sender<(i64, Label)>, 522 + cursor: Option<i64>, 523 + ) { 524 + // If cursor provided, backfill from that point 525 + let start_seq = if let Some(c) = cursor { 526 + // Send historical labels first 527 + match db.get_labels_since(c, 1000).await { 528 + Ok(rows) => { 529 + for row in &rows { 530 + let msg = SubscribeLabelsMessage { 531 + seq: row.seq, 532 + labels: vec![row.to_label()], 533 + }; 534 + if let Ok(json) = serde_json::to_string(&msg) { 535 + if socket.send(Message::Text(json.into())).await.is_err() { 536 + return; 537 + } 538 + } 539 + } 540 + rows.last().map(|r| r.seq).unwrap_or(c) 541 + } 542 + Err(e) => { 543 + error!(error = %e, "failed to backfill labels"); 544 + return; 545 + } 546 + } 547 + } else { 548 + // Start from current position 549 + db.get_latest_seq().await.unwrap_or(0) 550 + }; 551 + 552 + // Subscribe to live updates 553 + let rx = label_tx.subscribe(); 554 + let mut stream = BroadcastStream::new(rx); 555 + 556 + let mut last_seq = start_seq; 557 + 558 + loop { 559 + tokio::select! { 560 + // Receive from broadcast 561 + Some(result) = stream.next() => { 562 + match result { 563 + Ok((seq, label)) => { 564 + if seq > last_seq { 565 + let msg = SubscribeLabelsMessage { 566 + seq, 567 + labels: vec![label], 568 + }; 569 + if let Ok(json) = serde_json::to_string(&msg) { 570 + if socket.send(Message::Text(json.into())).await.is_err() { 571 + break; 572 + } 573 + } 574 + last_seq = seq; 575 + } 576 + } 577 + Err(_) => continue, // Lagged, skip 578 + } 579 + } 580 + // Check for client disconnect 581 + msg = socket.recv() => { 582 + match msg { 583 + Some(Ok(Message::Close(_))) | None => break, 584 + Some(Ok(Message::Ping(data))) => { 585 + if socket.send(Message::Pong(data)).await.is_err() { 586 + break; 587 + } 588 + } 589 + _ => {} 590 + } 591 + } 592 + } 593 + } 594 + } 595 + 596 + #[derive(Serialize)] 597 + struct SubscribeLabelsMessage { 598 + seq: i64, 599 + labels: Vec<Label>, 600 + } 601 + 247 602 fn extract_matches(response: &AuddResponse) -> Vec<AuddMatch> { 248 603 let Some(result) = &response.result else { 249 604 return vec![]; ··· 270 625 } 271 626 272 627 fn parse_song(song: &AuddSong, offset: Option<&serde_json::Value>) -> AuddMatch { 273 - // convert offset to ms - can be i64 or string like "00:00" or "01:24" 274 628 let offset_ms = offset.and_then(|v| match v { 275 629 serde_json::Value::Number(n) => n.as_i64(), 276 630 serde_json::Value::String(s) => parse_timecode_to_ms(s), ··· 289 643 } 290 644 291 645 fn parse_timecode_to_ms(timecode: &str) -> Option<i64> { 292 - // parse "MM:SS" or "HH:MM:SS" to milliseconds 293 646 let parts: Vec<&str> = timecode.split(':').collect(); 294 647 match parts.len() { 295 648 2 => { ··· 313 666 enum AppError { 314 667 #[error("audd error: {0}")] 315 668 Audd(String), 669 + 670 + #[error("labeler not configured")] 671 + LabelerNotConfigured, 672 + 673 + #[error("label error: {0}")] 674 + Label(#[from] labels::LabelError), 675 + 676 + #[error("database error: {0}")] 677 + Database(#[from] sqlx::Error), 316 678 } 317 679 318 680 impl IntoResponse for AppError { 319 681 fn into_response(self) -> Response { 320 682 error!(error = %self, "request failed"); 321 - let status = match self { 322 - AppError::Audd(_) => StatusCode::BAD_GATEWAY, 683 + let (status, error_type) = match &self { 684 + AppError::Audd(_) => (StatusCode::BAD_GATEWAY, "AuddError"), 685 + AppError::LabelerNotConfigured => (StatusCode::SERVICE_UNAVAILABLE, "LabelerNotConfigured"), 686 + AppError::Label(_) => (StatusCode::INTERNAL_SERVER_ERROR, "LabelError"), 687 + AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "DatabaseError"), 323 688 }; 324 - let body = serde_json::json!({ "error": self.to_string() }); 689 + let body = serde_json::json!({ 690 + "error": error_type, 691 + "message": self.to_string() 692 + }); 325 693 (status, Json(body)).into_response() 326 694 } 327 695 }