A better Rust ATProto crate
103
fork

Configure Feed

Select the types of activity you want to include in your feed.

repo and oauth migrated

+4082 -1488
+2120 -33
Cargo.lock
··· 12 12 ] 13 13 14 14 [[package]] 15 + name = "adler" 16 + version = "1.0.2" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 + 20 + [[package]] 15 21 name = "adler2" 16 22 version = "2.0.1" 17 23 source = "registry+https://github.com/rust-lang/crates.io-index" 18 24 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 25 20 26 [[package]] 27 + name = "adler32" 28 + version = "1.2.0" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 31 + 32 + [[package]] 21 33 name = "aho-corasick" 22 34 version = "1.1.4" 23 35 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 33 45 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 34 46 35 47 [[package]] 48 + name = "aligned" 49 + version = "0.4.3" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" 52 + dependencies = [ 53 + "as-slice", 54 + ] 55 + 56 + [[package]] 57 + name = "aligned-vec" 58 + version = "0.6.4" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" 61 + dependencies = [ 62 + "equator", 63 + ] 64 + 65 + [[package]] 66 + name = "alloc-no-stdlib" 67 + version = "2.0.4" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 70 + 71 + [[package]] 72 + name = "alloc-stdlib" 73 + version = "0.2.2" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 76 + dependencies = [ 77 + "alloc-no-stdlib", 78 + ] 79 + 80 + [[package]] 36 81 name = "allocator-api2" 37 82 version = "0.2.21" 38 83 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 45 90 checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 46 91 dependencies = [ 47 92 "libc", 93 + ] 94 + 95 + [[package]] 96 + name = "ansi_colours" 97 + version = "1.2.3" 98 + source = "registry+https://github.com/rust-lang/crates.io-index" 99 + checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe" 100 + dependencies = [ 101 + "rgb", 48 102 ] 49 103 50 104 [[package]] ··· 104 158 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 105 159 106 160 [[package]] 161 + name = "arbitrary" 162 + version = "1.4.2" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 165 + 166 + [[package]] 167 + name = "arg_enum_proc_macro" 168 + version = "0.3.4" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 171 + dependencies = [ 172 + "proc-macro2", 173 + "quote", 174 + "syn", 175 + ] 176 + 177 + [[package]] 178 + name = "arrayvec" 179 + version = "0.5.2" 180 + source = "registry+https://github.com/rust-lang/crates.io-index" 181 + checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 182 + 183 + [[package]] 184 + name = "arrayvec" 185 + version = "0.7.6" 186 + source = "registry+https://github.com/rust-lang/crates.io-index" 187 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 188 + 189 + [[package]] 190 + name = "as-slice" 191 + version = "0.2.1" 192 + source = "registry+https://github.com/rust-lang/crates.io-index" 193 + checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" 194 + dependencies = [ 195 + "stable_deref_trait", 196 + ] 197 + 198 + [[package]] 199 + name = "ascii" 200 + version = "1.1.0" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" 203 + 204 + [[package]] 107 205 name = "async-compression" 108 206 version = "0.4.41" 109 207 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 148 246 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 149 247 150 248 [[package]] 249 + name = "av-scenechange" 250 + version = "0.14.1" 251 + source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" 253 + dependencies = [ 254 + "aligned", 255 + "anyhow", 256 + "arg_enum_proc_macro", 257 + "arrayvec 0.7.6", 258 + "log", 259 + "num-rational", 260 + "num-traits", 261 + "pastey", 262 + "rayon", 263 + "thiserror 2.0.18", 264 + "v_frame", 265 + "y4m", 266 + ] 267 + 268 + [[package]] 269 + name = "av1-grain" 270 + version = "0.2.5" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" 273 + dependencies = [ 274 + "anyhow", 275 + "arrayvec 0.7.6", 276 + "log", 277 + "nom", 278 + "num-rational", 279 + "v_frame", 280 + ] 281 + 282 + [[package]] 283 + name = "avif-serialize" 284 + version = "0.8.8" 285 + source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" 287 + dependencies = [ 288 + "arrayvec 0.7.6", 289 + ] 290 + 291 + [[package]] 292 + name = "axum" 293 + version = "0.8.8" 294 + source = "registry+https://github.com/rust-lang/crates.io-index" 295 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 296 + dependencies = [ 297 + "axum-core", 298 + "bytes", 299 + "form_urlencoded", 300 + "futures-util", 301 + "http", 302 + "http-body", 303 + "http-body-util", 304 + "hyper", 305 + "hyper-util", 306 + "itoa", 307 + "matchit", 308 + "memchr", 309 + "mime", 310 + "percent-encoding", 311 + "pin-project-lite", 312 + "serde_core", 313 + "serde_json", 314 + "serde_path_to_error", 315 + "serde_urlencoded", 316 + "sync_wrapper", 317 + "tokio", 318 + "tower", 319 + "tower-layer", 320 + "tower-service", 321 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 322 + ] 323 + 324 + [[package]] 325 + name = "axum-core" 326 + version = "0.5.6" 327 + source = "registry+https://github.com/rust-lang/crates.io-index" 328 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 329 + dependencies = [ 330 + "bytes", 331 + "futures-core", 332 + "http", 333 + "http-body", 334 + "http-body-util", 335 + "mime", 336 + "pin-project-lite", 337 + "sync_wrapper", 338 + "tower-layer", 339 + "tower-service", 340 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 341 + ] 342 + 343 + [[package]] 344 + name = "axum-macros" 345 + version = "0.5.0" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 348 + dependencies = [ 349 + "proc-macro2", 350 + "quote", 351 + "syn", 352 + ] 353 + 354 + [[package]] 355 + name = "axum-test" 356 + version = "18.7.0" 357 + source = "registry+https://github.com/rust-lang/crates.io-index" 358 + checksum = "0ce2a8627e8d8851f894696b39f2b67807d6375c177361d376173ace306a21e2" 359 + dependencies = [ 360 + "anyhow", 361 + "axum", 362 + "bytes", 363 + "bytesize", 364 + "cookie", 365 + "expect-json", 366 + "http", 367 + "http-body-util", 368 + "hyper", 369 + "hyper-util", 370 + "mime", 371 + "pretty_assertions", 372 + "reserve-port", 373 + "rust-multipart-rfc7578_2", 374 + "serde", 375 + "serde_json", 376 + "serde_urlencoded", 377 + "smallvec", 378 + "tokio", 379 + "tower", 380 + "url", 381 + ] 382 + 383 + [[package]] 151 384 name = "backtrace" 152 385 version = "0.3.76" 153 386 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 156 389 "addr2line", 157 390 "cfg-if", 158 391 "libc", 159 - "miniz_oxide", 392 + "miniz_oxide 0.8.9", 160 393 "object", 161 394 "rustc-demangle", 162 395 "windows-link", ··· 195 428 196 429 [[package]] 197 430 name = "base64" 431 + version = "0.13.1" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 434 + 435 + [[package]] 436 + name = "base64" 198 437 version = "0.22.1" 199 438 source = "registry+https://github.com/rust-lang/crates.io-index" 200 439 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 206 445 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 207 446 208 447 [[package]] 448 + name = "bit-set" 449 + version = "0.8.0" 450 + source = "registry+https://github.com/rust-lang/crates.io-index" 451 + checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 452 + dependencies = [ 453 + "bit-vec", 454 + ] 455 + 456 + [[package]] 457 + name = "bit-vec" 458 + version = "0.8.0" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 461 + 462 + [[package]] 463 + name = "bit_field" 464 + version = "0.10.3" 465 + source = "registry+https://github.com/rust-lang/crates.io-index" 466 + checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" 467 + 468 + [[package]] 209 469 name = "bitflags" 210 470 version = "2.11.0" 211 471 source = "registry+https://github.com/rust-lang/crates.io-index" 212 472 checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 213 473 214 474 [[package]] 475 + name = "bitstream-io" 476 + version = "4.9.0" 477 + source = "registry+https://github.com/rust-lang/crates.io-index" 478 + checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" 479 + dependencies = [ 480 + "core2", 481 + ] 482 + 483 + [[package]] 215 484 name = "block-buffer" 216 485 version = "0.10.4" 217 486 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 262 531 ] 263 532 264 533 [[package]] 534 + name = "brotli" 535 + version = "3.5.0" 536 + source = "registry+https://github.com/rust-lang/crates.io-index" 537 + checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" 538 + dependencies = [ 539 + "alloc-no-stdlib", 540 + "alloc-stdlib", 541 + "brotli-decompressor", 542 + ] 543 + 544 + [[package]] 545 + name = "brotli-decompressor" 546 + version = "2.5.1" 547 + source = "registry+https://github.com/rust-lang/crates.io-index" 548 + checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" 549 + dependencies = [ 550 + "alloc-no-stdlib", 551 + "alloc-stdlib", 552 + ] 553 + 554 + [[package]] 555 + name = "buf_redux" 556 + version = "0.8.4" 557 + source = "registry+https://github.com/rust-lang/crates.io-index" 558 + checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 559 + dependencies = [ 560 + "memchr", 561 + "safemem", 562 + ] 563 + 564 + [[package]] 565 + name = "buffer" 566 + version = "0.1.9" 567 + source = "registry+https://github.com/rust-lang/crates.io-index" 568 + checksum = "aab7228d32b5d95be40adeba1d9461c8547b5dadf5f9cbfba09b6d578991df28" 569 + dependencies = [ 570 + "arrayvec 0.5.2", 571 + "mac 0.0.2", 572 + ] 573 + 574 + [[package]] 575 + name = "built" 576 + version = "0.8.0" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" 579 + 580 + [[package]] 265 581 name = "bumpalo" 266 582 version = "3.20.2" 267 583 source = "registry+https://github.com/rust-lang/crates.io-index" 268 584 checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 269 585 270 586 [[package]] 587 + name = "bytemuck" 588 + version = "1.25.0" 589 + source = "registry+https://github.com/rust-lang/crates.io-index" 590 + checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" 591 + 592 + [[package]] 271 593 name = "byteorder" 272 594 version = "1.5.0" 273 595 source = "registry+https://github.com/rust-lang/crates.io-index" 274 596 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 275 597 276 598 [[package]] 599 + name = "byteorder-lite" 600 + version = "0.1.0" 601 + source = "registry+https://github.com/rust-lang/crates.io-index" 602 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 603 + 604 + [[package]] 277 605 name = "bytes" 278 606 version = "1.11.1" 279 607 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 281 609 dependencies = [ 282 610 "serde", 283 611 ] 612 + 613 + [[package]] 614 + name = "bytesize" 615 + version = "2.3.1" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" 284 618 285 619 [[package]] 286 620 name = "cast" ··· 336 670 ] 337 671 338 672 [[package]] 673 + name = "chunked_transfer" 674 + version = "1.5.0" 675 + source = "registry+https://github.com/rust-lang/crates.io-index" 676 + checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" 677 + 678 + [[package]] 339 679 name = "ciborium" 340 680 version = "0.2.2" 341 681 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 373 713 "multihash", 374 714 "serde", 375 715 "serde_bytes", 376 - "unsigned-varint", 716 + "unsigned-varint 0.8.0", 377 717 ] 378 718 379 719 [[package]] ··· 445 785 ] 446 786 447 787 [[package]] 788 + name = "color_quant" 789 + version = "1.1.0" 790 + source = "registry+https://github.com/rust-lang/crates.io-index" 791 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 792 + 793 + [[package]] 448 794 name = "colorchoice" 449 795 version = "1.0.5" 450 796 source = "registry+https://github.com/rust-lang/crates.io-index" 451 797 checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" 452 798 453 799 [[package]] 800 + name = "combine" 801 + version = "4.6.7" 802 + source = "registry+https://github.com/rust-lang/crates.io-index" 803 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 804 + dependencies = [ 805 + "bytes", 806 + "memchr", 807 + ] 808 + 809 + [[package]] 454 810 name = "compression-codecs" 455 811 version = "0.4.37" 456 812 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 466 822 version = "0.4.31" 467 823 source = "registry+https://github.com/rust-lang/crates.io-index" 468 824 checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 825 + 826 + [[package]] 827 + name = "console" 828 + version = "0.15.11" 829 + source = "registry+https://github.com/rust-lang/crates.io-index" 830 + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 831 + dependencies = [ 832 + "encode_unicode", 833 + "libc", 834 + "once_cell", 835 + "windows-sys 0.59.0", 836 + ] 469 837 470 838 [[package]] 471 839 name = "const-oid" ··· 480 848 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 481 849 482 850 [[package]] 851 + name = "cookie" 852 + version = "0.18.1" 853 + source = "registry+https://github.com/rust-lang/crates.io-index" 854 + checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 855 + dependencies = [ 856 + "time", 857 + "version_check", 858 + ] 859 + 860 + [[package]] 483 861 name = "cordyceps" 484 862 version = "0.3.4" 485 863 source = "registry+https://github.com/rust-lang/crates.io-index" 486 864 checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" 487 865 dependencies = [ 488 866 "loom", 489 - "tracing", 867 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 490 868 ] 491 869 492 870 [[package]] ··· 558 936 ] 559 937 560 938 [[package]] 939 + name = "crossbeam-deque" 940 + version = "0.8.6" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 943 + dependencies = [ 944 + "crossbeam-epoch", 945 + "crossbeam-utils", 946 + ] 947 + 948 + [[package]] 949 + name = "crossbeam-epoch" 950 + version = "0.9.18" 951 + source = "registry+https://github.com/rust-lang/crates.io-index" 952 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 953 + dependencies = [ 954 + "crossbeam-utils", 955 + ] 956 + 957 + [[package]] 561 958 name = "crossbeam-utils" 562 959 version = "0.8.21" 563 960 source = "registry+https://github.com/rust-lang/crates.io-index" 564 961 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 565 962 566 963 [[package]] 964 + name = "crossterm" 965 + version = "0.28.1" 966 + source = "registry+https://github.com/rust-lang/crates.io-index" 967 + checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 968 + dependencies = [ 969 + "bitflags", 970 + "crossterm_winapi", 971 + "parking_lot", 972 + "rustix 0.38.44", 973 + "winapi", 974 + ] 975 + 976 + [[package]] 977 + name = "crossterm_winapi" 978 + version = "0.9.1" 979 + source = "registry+https://github.com/rust-lang/crates.io-index" 980 + checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 981 + dependencies = [ 982 + "winapi", 983 + ] 984 + 985 + [[package]] 567 986 name = "crunchy" 568 987 version = "0.2.4" 569 988 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 604 1023 "fiat-crypto", 605 1024 "rustc_version", 606 1025 "subtle", 1026 + "zeroize", 607 1027 ] 608 1028 609 1029 [[package]] ··· 692 1112 ] 693 1113 694 1114 [[package]] 1115 + name = "deflate" 1116 + version = "1.0.0" 1117 + source = "registry+https://github.com/rust-lang/crates.io-index" 1118 + checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" 1119 + dependencies = [ 1120 + "adler32", 1121 + "gzip-header", 1122 + ] 1123 + 1124 + [[package]] 695 1125 name = "der" 696 1126 version = "0.7.10" 697 1127 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 739 1169 checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" 740 1170 741 1171 [[package]] 1172 + name = "diff" 1173 + version = "0.1.13" 1174 + source = "registry+https://github.com/rust-lang/crates.io-index" 1175 + checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 1176 + 1177 + [[package]] 742 1178 name = "digest" 743 1179 version = "0.10.7" 744 1180 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 793 1229 dependencies = [ 794 1230 "curve25519-dalek", 795 1231 "ed25519", 1232 + "rand_core 0.6.4", 1233 + "serde", 796 1234 "sha2", 797 1235 "subtle", 1236 + "zeroize", 798 1237 ] 799 1238 800 1239 [[package]] 1240 + name = "either" 1241 + version = "1.15.0" 1242 + source = "registry+https://github.com/rust-lang/crates.io-index" 1243 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 1244 + 1245 + [[package]] 801 1246 name = "elliptic-curve" 802 1247 version = "0.13.8" 803 1248 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 809 1254 "ff", 810 1255 "generic-array", 811 1256 "group", 1257 + "hkdf", 812 1258 "pem-rfc7468", 813 1259 "pkcs8", 814 1260 "rand_core 0.6.4", ··· 818 1264 ] 819 1265 820 1266 [[package]] 1267 + name = "email_address" 1268 + version = "0.2.9" 1269 + source = "registry+https://github.com/rust-lang/crates.io-index" 1270 + checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 1271 + dependencies = [ 1272 + "serde", 1273 + ] 1274 + 1275 + [[package]] 821 1276 name = "embedded-io" 822 1277 version = "0.4.0" 823 1278 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 830 1285 checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 831 1286 832 1287 [[package]] 1288 + name = "embedded-io-adapters" 1289 + version = "0.6.2" 1290 + source = "registry+https://github.com/rust-lang/crates.io-index" 1291 + checksum = "90ccf22c3feffc79593914c0b4be9a2ed6b11e44cf1f84fd6b77d2ee92de0077" 1292 + dependencies = [ 1293 + "embedded-io 0.6.1", 1294 + "embedded-io-async", 1295 + "tokio", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "embedded-io-async" 1300 + version = "0.6.1" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" 1303 + dependencies = [ 1304 + "embedded-io 0.6.1", 1305 + ] 1306 + 1307 + [[package]] 1308 + name = "encode_unicode" 1309 + version = "1.0.0" 1310 + source = "registry+https://github.com/rust-lang/crates.io-index" 1311 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 1312 + 1313 + [[package]] 833 1314 name = "encoding_rs" 834 1315 version = "0.8.35" 835 1316 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 851 1332 ] 852 1333 853 1334 [[package]] 1335 + name = "equator" 1336 + version = "0.4.2" 1337 + source = "registry+https://github.com/rust-lang/crates.io-index" 1338 + checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" 1339 + dependencies = [ 1340 + "equator-macro", 1341 + ] 1342 + 1343 + [[package]] 1344 + name = "equator-macro" 1345 + version = "0.4.2" 1346 + source = "registry+https://github.com/rust-lang/crates.io-index" 1347 + checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" 1348 + dependencies = [ 1349 + "proc-macro2", 1350 + "quote", 1351 + "syn", 1352 + ] 1353 + 1354 + [[package]] 854 1355 name = "equivalent" 855 1356 version = "1.0.2" 856 1357 source = "registry+https://github.com/rust-lang/crates.io-index" 857 1358 checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 858 1359 859 1360 [[package]] 1361 + name = "erased-serde" 1362 + version = "0.4.10" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" 1365 + dependencies = [ 1366 + "serde", 1367 + "serde_core", 1368 + "typeid", 1369 + ] 1370 + 1371 + [[package]] 860 1372 name = "errno" 861 1373 version = "0.3.14" 862 1374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 867 1379 ] 868 1380 869 1381 [[package]] 1382 + name = "expect-json" 1383 + version = "1.10.1" 1384 + source = "registry+https://github.com/rust-lang/crates.io-index" 1385 + checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4" 1386 + dependencies = [ 1387 + "chrono", 1388 + "email_address", 1389 + "expect-json-macros", 1390 + "num", 1391 + "regex", 1392 + "serde", 1393 + "serde_json", 1394 + "thiserror 2.0.18", 1395 + "typetag", 1396 + "uuid", 1397 + ] 1398 + 1399 + [[package]] 1400 + name = "expect-json-macros" 1401 + version = "1.10.1" 1402 + source = "registry+https://github.com/rust-lang/crates.io-index" 1403 + checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9" 1404 + dependencies = [ 1405 + "proc-macro2", 1406 + "quote", 1407 + "syn", 1408 + ] 1409 + 1410 + [[package]] 1411 + name = "exr" 1412 + version = "1.74.0" 1413 + source = "registry+https://github.com/rust-lang/crates.io-index" 1414 + checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" 1415 + dependencies = [ 1416 + "bit_field", 1417 + "half", 1418 + "lebe", 1419 + "miniz_oxide 0.8.9", 1420 + "rayon-core", 1421 + "smallvec", 1422 + "zune-inflate", 1423 + ] 1424 + 1425 + [[package]] 870 1426 name = "fastrand" 871 1427 version = "2.3.0" 872 1428 source = "registry+https://github.com/rust-lang/crates.io-index" 873 1429 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 874 1430 875 1431 [[package]] 1432 + name = "fax" 1433 + version = "0.2.6" 1434 + source = "registry+https://github.com/rust-lang/crates.io-index" 1435 + checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" 1436 + dependencies = [ 1437 + "fax_derive", 1438 + ] 1439 + 1440 + [[package]] 1441 + name = "fax_derive" 1442 + version = "0.2.0" 1443 + source = "registry+https://github.com/rust-lang/crates.io-index" 1444 + checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" 1445 + dependencies = [ 1446 + "proc-macro2", 1447 + "quote", 1448 + "syn", 1449 + ] 1450 + 1451 + [[package]] 1452 + name = "fdeflate" 1453 + version = "0.3.7" 1454 + source = "registry+https://github.com/rust-lang/crates.io-index" 1455 + checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 1456 + dependencies = [ 1457 + "simd-adler32", 1458 + ] 1459 + 1460 + [[package]] 876 1461 name = "ff" 877 1462 version = "0.13.1" 878 1463 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 889 1474 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 890 1475 891 1476 [[package]] 1477 + name = "filetime" 1478 + version = "0.2.27" 1479 + source = "registry+https://github.com/rust-lang/crates.io-index" 1480 + checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" 1481 + dependencies = [ 1482 + "cfg-if", 1483 + "libc", 1484 + "libredox", 1485 + ] 1486 + 1487 + [[package]] 892 1488 name = "find-msvc-tools" 893 1489 version = "0.1.9" 894 1490 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 901 1497 checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" 902 1498 dependencies = [ 903 1499 "crc32fast", 904 - "miniz_oxide", 1500 + "miniz_oxide 0.8.9", 905 1501 ] 906 1502 907 1503 [[package]] ··· 937 1533 ] 938 1534 939 1535 [[package]] 1536 + name = "futf" 1537 + version = "0.1.5" 1538 + source = "registry+https://github.com/rust-lang/crates.io-index" 1539 + checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 1540 + dependencies = [ 1541 + "mac 0.1.1", 1542 + "new_debug_unreachable", 1543 + ] 1544 + 1545 + [[package]] 940 1546 name = "futures" 941 1547 version = "0.3.32" 942 1548 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 944 1550 dependencies = [ 945 1551 "futures-channel", 946 1552 "futures-core", 1553 + "futures-executor", 947 1554 "futures-io", 948 1555 "futures-sink", 949 1556 "futures-task", ··· 980 1587 checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 981 1588 982 1589 [[package]] 1590 + name = "futures-executor" 1591 + version = "0.3.32" 1592 + source = "registry+https://github.com/rust-lang/crates.io-index" 1593 + checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" 1594 + dependencies = [ 1595 + "futures-core", 1596 + "futures-task", 1597 + "futures-util", 1598 + ] 1599 + 1600 + [[package]] 983 1601 name = "futures-io" 984 1602 version = "0.3.32" 985 1603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1027 1645 source = "registry+https://github.com/rust-lang/crates.io-index" 1028 1646 checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 1029 1647 dependencies = [ 1648 + "futures-channel", 1030 1649 "futures-core", 1031 1650 "futures-io", 1032 1651 "futures-macro", ··· 1091 1710 ] 1092 1711 1093 1712 [[package]] 1713 + name = "gif" 1714 + version = "0.14.1" 1715 + source = "registry+https://github.com/rust-lang/crates.io-index" 1716 + checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" 1717 + dependencies = [ 1718 + "color_quant", 1719 + "weezl", 1720 + ] 1721 + 1722 + [[package]] 1094 1723 name = "gimli" 1095 1724 version = "0.32.3" 1096 1725 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1103 1732 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 1104 1733 1105 1734 [[package]] 1735 + name = "gloo-storage" 1736 + version = "0.3.0" 1737 + source = "registry+https://github.com/rust-lang/crates.io-index" 1738 + checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 1739 + dependencies = [ 1740 + "gloo-utils", 1741 + "js-sys", 1742 + "serde", 1743 + "serde_json", 1744 + "thiserror 1.0.69", 1745 + "wasm-bindgen", 1746 + "web-sys", 1747 + ] 1748 + 1749 + [[package]] 1750 + name = "gloo-utils" 1751 + version = "0.2.0" 1752 + source = "registry+https://github.com/rust-lang/crates.io-index" 1753 + checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 1754 + dependencies = [ 1755 + "js-sys", 1756 + "serde", 1757 + "serde_json", 1758 + "wasm-bindgen", 1759 + "web-sys", 1760 + ] 1761 + 1762 + [[package]] 1106 1763 name = "group" 1107 1764 version = "0.13.0" 1108 1765 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1111 1768 "ff", 1112 1769 "rand_core 0.6.4", 1113 1770 "subtle", 1771 + ] 1772 + 1773 + [[package]] 1774 + name = "gzip-header" 1775 + version = "1.0.0" 1776 + source = "registry+https://github.com/rust-lang/crates.io-index" 1777 + checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" 1778 + dependencies = [ 1779 + "crc32fast", 1114 1780 ] 1115 1781 1116 1782 [[package]] ··· 1129 1795 "slab", 1130 1796 "tokio", 1131 1797 "tokio-util", 1132 - "tracing", 1798 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1133 1799 ] 1134 1800 1135 1801 [[package]] ··· 1153 1819 ] 1154 1820 1155 1821 [[package]] 1822 + name = "hash32" 1823 + version = "0.3.1" 1824 + source = "registry+https://github.com/rust-lang/crates.io-index" 1825 + checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 1826 + dependencies = [ 1827 + "byteorder", 1828 + ] 1829 + 1830 + [[package]] 1156 1831 name = "hashbrown" 1157 1832 version = "0.14.5" 1158 1833 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1182 1857 checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1183 1858 dependencies = [ 1184 1859 "atomic-polyfill", 1185 - "hash32", 1860 + "hash32 0.2.1", 1186 1861 "rustc_version", 1187 1862 "serde", 1188 1863 "spin 0.9.8", ··· 1190 1865 ] 1191 1866 1192 1867 [[package]] 1868 + name = "heapless" 1869 + version = "0.9.2" 1870 + source = "registry+https://github.com/rust-lang/crates.io-index" 1871 + checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" 1872 + dependencies = [ 1873 + "hash32 0.3.1", 1874 + "stable_deref_trait", 1875 + ] 1876 + 1877 + [[package]] 1193 1878 name = "heck" 1194 1879 version = "0.4.1" 1195 1880 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1202 1887 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1203 1888 1204 1889 [[package]] 1890 + name = "hermit-abi" 1891 + version = "0.5.2" 1892 + source = "registry+https://github.com/rust-lang/crates.io-index" 1893 + checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 1894 + 1895 + [[package]] 1205 1896 name = "hex" 1206 1897 version = "0.4.3" 1207 1898 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1227 1918 "thiserror 1.0.69", 1228 1919 "tinyvec", 1229 1920 "tokio", 1230 - "tracing", 1921 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1231 1922 "url", 1232 1923 ] 1233 1924 ··· 1249 1940 "smallvec", 1250 1941 "thiserror 1.0.69", 1251 1942 "tokio", 1252 - "tracing", 1943 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1944 + ] 1945 + 1946 + [[package]] 1947 + name = "hkdf" 1948 + version = "0.12.4" 1949 + source = "registry+https://github.com/rust-lang/crates.io-index" 1950 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1951 + dependencies = [ 1952 + "hmac", 1253 1953 ] 1254 1954 1255 1955 [[package]] ··· 1262 1962 ] 1263 1963 1264 1964 [[package]] 1965 + name = "html5ever" 1966 + version = "0.27.0" 1967 + source = "registry+https://github.com/rust-lang/crates.io-index" 1968 + checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" 1969 + dependencies = [ 1970 + "log", 1971 + "mac 0.1.1", 1972 + "markup5ever", 1973 + "proc-macro2", 1974 + "quote", 1975 + "syn", 1976 + ] 1977 + 1978 + [[package]] 1265 1979 name = "http" 1266 1980 version = "1.4.0" 1267 1981 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1301 2015 checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1302 2016 1303 2017 [[package]] 2018 + name = "httpdate" 2019 + version = "1.0.3" 2020 + source = "registry+https://github.com/rust-lang/crates.io-index" 2021 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 2022 + 2023 + [[package]] 1304 2024 name = "hyper" 1305 2025 version = "1.8.1" 1306 2026 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1314 2034 "http", 1315 2035 "http-body", 1316 2036 "httparse", 2037 + "httpdate", 1317 2038 "itoa", 1318 2039 "pin-project-lite", 1319 2040 "pin-utils", ··· 1345 2066 source = "registry+https://github.com/rust-lang/crates.io-index" 1346 2067 checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 1347 2068 dependencies = [ 1348 - "base64", 2069 + "base64 0.22.1", 1349 2070 "bytes", 1350 2071 "futures-channel", 1351 2072 "futures-util", ··· 1360 2081 "system-configuration", 1361 2082 "tokio", 1362 2083 "tower-service", 1363 - "tracing", 2084 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1364 2085 "windows-registry", 1365 2086 ] 1366 2087 ··· 1497 2218 ] 1498 2219 1499 2220 [[package]] 2221 + name = "image" 2222 + version = "0.25.10" 2223 + source = "registry+https://github.com/rust-lang/crates.io-index" 2224 + checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" 2225 + dependencies = [ 2226 + "bytemuck", 2227 + "byteorder-lite", 2228 + "color_quant", 2229 + "exr", 2230 + "gif", 2231 + "image-webp", 2232 + "moxcms", 2233 + "num-traits", 2234 + "png", 2235 + "qoi", 2236 + "ravif", 2237 + "rayon", 2238 + "rgb", 2239 + "tiff 0.11.3", 2240 + "zune-core", 2241 + "zune-jpeg", 2242 + ] 2243 + 2244 + [[package]] 2245 + name = "image-webp" 2246 + version = "0.2.4" 2247 + source = "registry+https://github.com/rust-lang/crates.io-index" 2248 + checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" 2249 + dependencies = [ 2250 + "byteorder-lite", 2251 + "quick-error 2.0.1", 2252 + ] 2253 + 2254 + [[package]] 2255 + name = "imgref" 2256 + version = "1.12.0" 2257 + source = "registry+https://github.com/rust-lang/crates.io-index" 2258 + checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" 2259 + 2260 + [[package]] 1500 2261 name = "indexmap" 1501 2262 version = "2.13.0" 1502 2263 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1504 2265 dependencies = [ 1505 2266 "equivalent", 1506 2267 "hashbrown 0.16.1", 2268 + ] 2269 + 2270 + [[package]] 2271 + name = "interpolate_name" 2272 + version = "0.2.4" 2273 + source = "registry+https://github.com/rust-lang/crates.io-index" 2274 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 2275 + dependencies = [ 2276 + "proc-macro2", 2277 + "quote", 2278 + "syn", 1507 2279 ] 1508 2280 1509 2281 [[package]] ··· 1552 2324 dependencies = [ 1553 2325 "memchr", 1554 2326 "serde", 2327 + ] 2328 + 2329 + [[package]] 2330 + name = "iroh-car" 2331 + version = "0.5.1" 2332 + source = "registry+https://github.com/rust-lang/crates.io-index" 2333 + checksum = "cb7f8cd4cb9aa083fba8b52e921764252d0b4dcb1cd6d120b809dbfe1106e81a" 2334 + dependencies = [ 2335 + "anyhow", 2336 + "cid", 2337 + "futures", 2338 + "serde", 2339 + "serde_ipld_dagcbor", 2340 + "thiserror 1.0.69", 2341 + "tokio", 2342 + "unsigned-varint 0.7.2", 1555 2343 ] 1556 2344 1557 2345 [[package]] ··· 1567 2355 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 1568 2356 1569 2357 [[package]] 2358 + name = "itertools" 2359 + version = "0.14.0" 2360 + source = "registry+https://github.com/rust-lang/crates.io-index" 2361 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 2362 + dependencies = [ 2363 + "either", 2364 + ] 2365 + 2366 + [[package]] 1570 2367 name = "itoa" 1571 2368 version = "1.0.18" 1572 2369 source = "registry+https://github.com/rust-lang/crates.io-index" 1573 2370 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 1574 2371 1575 2372 [[package]] 2373 + name = "jacquard" 2374 + version = "0.11.0" 2375 + dependencies = [ 2376 + "bytes", 2377 + "clap", 2378 + "getrandom 0.2.17", 2379 + "gloo-storage", 2380 + "http", 2381 + "image", 2382 + "jacquard-api", 2383 + "jacquard-common", 2384 + "jacquard-derive", 2385 + "jacquard-identity", 2386 + "jacquard-oauth", 2387 + "jose-jwk", 2388 + "miette", 2389 + "n0-future", 2390 + "regex", 2391 + "regex-lite", 2392 + "reqwest", 2393 + "serde", 2394 + "serde_html_form", 2395 + "serde_json", 2396 + "smol_str", 2397 + "thiserror 2.0.18", 2398 + "tiff 0.6.1", 2399 + "tokio", 2400 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2401 + "trait-variant", 2402 + "viuer", 2403 + "webpage", 2404 + ] 2405 + 2406 + [[package]] 1576 2407 name = "jacquard-api" 1577 2408 version = "0.11.1" 1578 2409 dependencies = [ ··· 1585 2416 ] 1586 2417 1587 2418 [[package]] 2419 + name = "jacquard-axum" 2420 + version = "0.11.0" 2421 + dependencies = [ 2422 + "axum", 2423 + "axum-macros", 2424 + "axum-test", 2425 + "base64 0.22.1", 2426 + "bytes", 2427 + "chrono", 2428 + "jacquard", 2429 + "jacquard-common", 2430 + "jacquard-derive", 2431 + "jacquard-identity", 2432 + "k256", 2433 + "miette", 2434 + "multibase", 2435 + "rand 0.8.5", 2436 + "reqwest", 2437 + "serde", 2438 + "serde_html_form", 2439 + "serde_json", 2440 + "thiserror 2.0.18", 2441 + "tokio", 2442 + "tower", 2443 + "tower-http", 2444 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2445 + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 2446 + ] 2447 + 2448 + [[package]] 2449 + name = "jacquard-codegen-tests" 2450 + version = "0.0.0" 2451 + dependencies = [ 2452 + "jacquard-common", 2453 + "jacquard-derive", 2454 + "jacquard-lexicon", 2455 + "miette", 2456 + "serde", 2457 + "serde_ipld_dagcbor", 2458 + "serde_json", 2459 + "smol_str", 2460 + "thiserror 2.0.18", 2461 + ] 2462 + 2463 + [[package]] 1588 2464 name = "jacquard-common" 1589 2465 version = "0.11.0" 1590 2466 dependencies = [ 1591 - "base64", 2467 + "base64 0.22.1", 1592 2468 "bon", 1593 2469 "bytes", 1594 2470 "chrono", ··· 1605 2481 "http", 1606 2482 "ipld-core", 1607 2483 "k256", 1608 - "maitake-sync", 2484 + "maitake-sync 0.1.2", 1609 2485 "miette", 1610 2486 "multibase", 1611 2487 "multihash", ··· 1633 2509 "tokio", 1634 2510 "tokio-tungstenite-wasm", 1635 2511 "tokio-util", 1636 - "tracing", 2512 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1637 2513 "trait-variant", 1638 2514 "unicode-segmentation", 1639 2515 "zstd", ··· 1674 2550 "serde_json", 1675 2551 "thiserror 2.0.18", 1676 2552 "tokio", 1677 - "tracing", 2553 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 1678 2554 "trait-variant", 1679 2555 ] 1680 2556 ··· 1734 2610 ] 1735 2611 1736 2612 [[package]] 2613 + name = "jacquard-oauth" 2614 + version = "0.11.0" 2615 + dependencies = [ 2616 + "base64 0.22.1", 2617 + "bytes", 2618 + "chrono", 2619 + "dashmap", 2620 + "ed25519-dalek", 2621 + "elliptic-curve", 2622 + "http", 2623 + "jacquard-common", 2624 + "jacquard-identity", 2625 + "jose-jwa", 2626 + "jose-jwk", 2627 + "k256", 2628 + "miette", 2629 + "n0-future", 2630 + "p256", 2631 + "p384", 2632 + "rand 0.8.5", 2633 + "rouille", 2634 + "serde", 2635 + "serde_html_form", 2636 + "serde_json", 2637 + "sha2", 2638 + "smol_str", 2639 + "thiserror 2.0.18", 2640 + "tokio", 2641 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2642 + "trait-variant", 2643 + "webbrowser", 2644 + ] 2645 + 2646 + [[package]] 2647 + name = "jacquard-repo" 2648 + version = "0.11.0" 2649 + dependencies = [ 2650 + "anyhow", 2651 + "bytes", 2652 + "cid", 2653 + "ed25519-dalek", 2654 + "hex", 2655 + "iroh-car", 2656 + "jacquard-api", 2657 + "jacquard-common", 2658 + "jacquard-derive", 2659 + "k256", 2660 + "miette", 2661 + "multihash", 2662 + "n0-future", 2663 + "p256", 2664 + "rand 0.8.5", 2665 + "serde", 2666 + "serde_bytes", 2667 + "serde_ipld_dagcbor", 2668 + "serde_ipld_dagjson", 2669 + "serde_json", 2670 + "sha2", 2671 + "smol_str", 2672 + "tempfile", 2673 + "thiserror 2.0.18", 2674 + "tokio", 2675 + "trait-variant", 2676 + ] 2677 + 2678 + [[package]] 2679 + name = "jni" 2680 + version = "0.22.4" 2681 + source = "registry+https://github.com/rust-lang/crates.io-index" 2682 + checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" 2683 + dependencies = [ 2684 + "cfg-if", 2685 + "combine", 2686 + "jni-macros", 2687 + "jni-sys", 2688 + "log", 2689 + "simd_cesu8", 2690 + "thiserror 2.0.18", 2691 + "walkdir", 2692 + "windows-link", 2693 + ] 2694 + 2695 + [[package]] 2696 + name = "jni-macros" 2697 + version = "0.22.4" 2698 + source = "registry+https://github.com/rust-lang/crates.io-index" 2699 + checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" 2700 + dependencies = [ 2701 + "proc-macro2", 2702 + "quote", 2703 + "rustc_version", 2704 + "simd_cesu8", 2705 + "syn", 2706 + ] 2707 + 2708 + [[package]] 2709 + name = "jni-sys" 2710 + version = "0.4.1" 2711 + source = "registry+https://github.com/rust-lang/crates.io-index" 2712 + checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" 2713 + dependencies = [ 2714 + "jni-sys-macros", 2715 + ] 2716 + 2717 + [[package]] 2718 + name = "jni-sys-macros" 2719 + version = "0.4.1" 2720 + source = "registry+https://github.com/rust-lang/crates.io-index" 2721 + checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" 2722 + dependencies = [ 2723 + "quote", 2724 + "syn", 2725 + ] 2726 + 2727 + [[package]] 1737 2728 name = "jobserver" 1738 2729 version = "0.1.34" 1739 2730 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1744 2735 ] 1745 2736 1746 2737 [[package]] 2738 + name = "jose-b64" 2739 + version = "0.1.2" 2740 + source = "registry+https://github.com/rust-lang/crates.io-index" 2741 + checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" 2742 + dependencies = [ 2743 + "base64ct", 2744 + "serde", 2745 + "subtle", 2746 + "zeroize", 2747 + ] 2748 + 2749 + [[package]] 2750 + name = "jose-jwa" 2751 + version = "0.1.2" 2752 + source = "registry+https://github.com/rust-lang/crates.io-index" 2753 + checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" 2754 + dependencies = [ 2755 + "serde", 2756 + ] 2757 + 2758 + [[package]] 2759 + name = "jose-jwk" 2760 + version = "0.1.2" 2761 + source = "registry+https://github.com/rust-lang/crates.io-index" 2762 + checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" 2763 + dependencies = [ 2764 + "jose-b64", 2765 + "jose-jwa", 2766 + "p256", 2767 + "p384", 2768 + "rsa", 2769 + "serde", 2770 + "zeroize", 2771 + ] 2772 + 2773 + [[package]] 2774 + name = "jpeg-decoder" 2775 + version = "0.1.22" 2776 + source = "registry+https://github.com/rust-lang/crates.io-index" 2777 + checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 2778 + 2779 + [[package]] 1747 2780 name = "js-sys" 1748 2781 version = "0.3.91" 1749 2782 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1762 2795 "cfg-if", 1763 2796 "ecdsa", 1764 2797 "elliptic-curve", 2798 + "once_cell", 1765 2799 "sha2", 2800 + "signature", 1766 2801 ] 1767 2802 1768 2803 [[package]] ··· 1777 2812 ] 1778 2813 1779 2814 [[package]] 2815 + name = "lazy-collections" 2816 + version = "0.11.0" 2817 + dependencies = [ 2818 + "buffer", 2819 + "bytes", 2820 + "chrono", 2821 + "embedded-io 0.6.1", 2822 + "embedded-io-adapters", 2823 + "embedded-io-async", 2824 + "heapless 0.9.2", 2825 + "lock_api", 2826 + "loom", 2827 + "maitake-sync 0.2.2", 2828 + "managed", 2829 + "miette", 2830 + "n0-future", 2831 + "pin-project", 2832 + "postcard", 2833 + "proptest", 2834 + "serde", 2835 + "serde_bytes", 2836 + "tokio", 2837 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2838 + "tracing 0.1.44 (git+https://github.com/tokio-rs/tracing)", 2839 + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 2840 + "tracing-subscriber 0.3.23 (git+https://github.com/tokio-rs/tracing)", 2841 + ] 2842 + 2843 + [[package]] 1780 2844 name = "lazy_static" 1781 2845 version = "1.5.0" 1782 2846 source = "registry+https://github.com/rust-lang/crates.io-index" 1783 2847 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2848 + dependencies = [ 2849 + "spin 0.9.8", 2850 + ] 2851 + 2852 + [[package]] 2853 + name = "lebe" 2854 + version = "0.5.3" 2855 + source = "registry+https://github.com/rust-lang/crates.io-index" 2856 + checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" 1784 2857 1785 2858 [[package]] 1786 2859 name = "libc" 1787 2860 version = "0.2.183" 1788 2861 source = "registry+https://github.com/rust-lang/crates.io-index" 1789 2862 checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 2863 + 2864 + [[package]] 2865 + name = "libfuzzer-sys" 2866 + version = "0.4.12" 2867 + source = "registry+https://github.com/rust-lang/crates.io-index" 2868 + checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" 2869 + dependencies = [ 2870 + "arbitrary", 2871 + "cc", 2872 + ] 1790 2873 1791 2874 [[package]] 1792 2875 name = "libm" ··· 1795 2878 checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 1796 2879 1797 2880 [[package]] 2881 + name = "libredox" 2882 + version = "0.1.14" 2883 + source = "registry+https://github.com/rust-lang/crates.io-index" 2884 + checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" 2885 + dependencies = [ 2886 + "bitflags", 2887 + "libc", 2888 + "plain", 2889 + "redox_syscall 0.7.3", 2890 + ] 2891 + 2892 + [[package]] 1798 2893 name = "linked-hash-map" 1799 2894 version = "0.5.6" 1800 2895 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1802 2897 1803 2898 [[package]] 1804 2899 name = "linux-raw-sys" 2900 + version = "0.4.15" 2901 + source = "registry+https://github.com/rust-lang/crates.io-index" 2902 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 2903 + 2904 + [[package]] 2905 + name = "linux-raw-sys" 1805 2906 version = "0.12.1" 1806 2907 source = "registry+https://github.com/rust-lang/crates.io-index" 1807 2908 checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" ··· 1836 2937 "cfg-if", 1837 2938 "generator", 1838 2939 "scoped-tls", 1839 - "tracing", 1840 - "tracing-subscriber", 2940 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2941 + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", 2942 + ] 2943 + 2944 + [[package]] 2945 + name = "loop9" 2946 + version = "0.1.5" 2947 + source = "registry+https://github.com/rust-lang/crates.io-index" 2948 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 2949 + dependencies = [ 2950 + "imgref", 1841 2951 ] 1842 2952 1843 2953 [[package]] ··· 1856 2966 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1857 2967 1858 2968 [[package]] 2969 + name = "mac" 2970 + version = "0.0.2" 2971 + source = "registry+https://github.com/rust-lang/crates.io-index" 2972 + checksum = "1b1db08c0d0ddbb591e65f1da58d1cefccc94a2faa0c55bf979ce215a3e04d5e" 2973 + 2974 + [[package]] 2975 + name = "mac" 2976 + version = "0.1.1" 2977 + source = "registry+https://github.com/rust-lang/crates.io-index" 2978 + checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 2979 + 2980 + [[package]] 1859 2981 name = "maitake-sync" 1860 2982 version = "0.1.2" 1861 2983 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1869 2991 ] 1870 2992 1871 2993 [[package]] 2994 + name = "maitake-sync" 2995 + version = "0.2.2" 2996 + source = "registry+https://github.com/rust-lang/crates.io-index" 2997 + checksum = "748f86d9befd480b602c3bebc9ef30dbf2f3dfc8acc4a73d07b90f0117e6de3f" 2998 + dependencies = [ 2999 + "cordyceps", 3000 + "loom", 3001 + "mutex-traits", 3002 + "mycelium-bitfield", 3003 + "pin-project", 3004 + "portable-atomic", 3005 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 3006 + ] 3007 + 3008 + [[package]] 3009 + name = "managed" 3010 + version = "0.8.0" 3011 + source = "registry+https://github.com/rust-lang/crates.io-index" 3012 + checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" 3013 + 3014 + [[package]] 3015 + name = "markup5ever" 3016 + version = "0.12.1" 3017 + source = "registry+https://github.com/rust-lang/crates.io-index" 3018 + checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" 3019 + dependencies = [ 3020 + "log", 3021 + "phf", 3022 + "phf_codegen", 3023 + "string_cache", 3024 + "string_cache_codegen", 3025 + "tendril", 3026 + ] 3027 + 3028 + [[package]] 3029 + name = "markup5ever_rcdom" 3030 + version = "0.3.0" 3031 + source = "registry+https://github.com/rust-lang/crates.io-index" 3032 + checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" 3033 + dependencies = [ 3034 + "html5ever", 3035 + "markup5ever", 3036 + "tendril", 3037 + "xml5ever", 3038 + ] 3039 + 3040 + [[package]] 1872 3041 name = "match-lookup" 1873 3042 version = "0.1.2" 1874 3043 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1889 3058 ] 1890 3059 1891 3060 [[package]] 3061 + name = "matchit" 3062 + version = "0.8.4" 3063 + source = "registry+https://github.com/rust-lang/crates.io-index" 3064 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 3065 + 3066 + [[package]] 3067 + name = "maybe-rayon" 3068 + version = "0.1.1" 3069 + source = "registry+https://github.com/rust-lang/crates.io-index" 3070 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 3071 + dependencies = [ 3072 + "cfg-if", 3073 + "rayon", 3074 + ] 3075 + 3076 + [[package]] 1892 3077 name = "memchr" 1893 3078 version = "2.8.0" 1894 3079 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1931 3116 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1932 3117 1933 3118 [[package]] 3119 + name = "mime_guess" 3120 + version = "2.0.5" 3121 + source = "registry+https://github.com/rust-lang/crates.io-index" 3122 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 3123 + dependencies = [ 3124 + "mime", 3125 + "unicase", 3126 + ] 3127 + 3128 + [[package]] 1934 3129 name = "mini-moka-wasm" 1935 3130 version = "0.10.99" 1936 3131 dependencies = [ ··· 1960 3155 1961 3156 [[package]] 1962 3157 name = "miniz_oxide" 3158 + version = "0.4.4" 3159 + source = "registry+https://github.com/rust-lang/crates.io-index" 3160 + checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 3161 + dependencies = [ 3162 + "adler", 3163 + "autocfg", 3164 + ] 3165 + 3166 + [[package]] 3167 + name = "miniz_oxide" 1963 3168 version = "0.8.9" 1964 3169 source = "registry+https://github.com/rust-lang/crates.io-index" 1965 3170 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" ··· 1980 3185 ] 1981 3186 1982 3187 [[package]] 3188 + name = "moxcms" 3189 + version = "0.8.1" 3190 + source = "registry+https://github.com/rust-lang/crates.io-index" 3191 + checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" 3192 + dependencies = [ 3193 + "num-traits", 3194 + "pxfm", 3195 + ] 3196 + 3197 + [[package]] 1983 3198 name = "multibase" 1984 3199 version = "0.9.2" 1985 3200 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1999 3214 dependencies = [ 2000 3215 "core2", 2001 3216 "serde", 2002 - "unsigned-varint", 3217 + "unsigned-varint 0.8.0", 3218 + ] 3219 + 3220 + [[package]] 3221 + name = "multipart" 3222 + version = "0.18.0" 3223 + source = "registry+https://github.com/rust-lang/crates.io-index" 3224 + checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" 3225 + dependencies = [ 3226 + "buf_redux", 3227 + "httparse", 3228 + "log", 3229 + "mime", 3230 + "mime_guess", 3231 + "quick-error 1.2.3", 3232 + "rand 0.8.5", 3233 + "safemem", 3234 + "tempfile", 3235 + "twoway", 2003 3236 ] 3237 + 3238 + [[package]] 3239 + name = "mutex-traits" 3240 + version = "1.0.1" 3241 + source = "registry+https://github.com/rust-lang/crates.io-index" 3242 + checksum = "3929f2b5633d29cf7b6624992e5f3c1e9334f1193423e12d17be4faf678cde3f" 2004 3243 2005 3244 [[package]] 2006 3245 name = "mycelium-bitfield" ··· 2030 3269 ] 2031 3270 2032 3271 [[package]] 3272 + name = "ndk-context" 3273 + version = "0.1.1" 3274 + source = "registry+https://github.com/rust-lang/crates.io-index" 3275 + checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 3276 + 3277 + [[package]] 3278 + name = "new_debug_unreachable" 3279 + version = "1.0.6" 3280 + source = "registry+https://github.com/rust-lang/crates.io-index" 3281 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 3282 + 3283 + [[package]] 3284 + name = "nom" 3285 + version = "8.0.0" 3286 + source = "registry+https://github.com/rust-lang/crates.io-index" 3287 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 3288 + dependencies = [ 3289 + "memchr", 3290 + ] 3291 + 3292 + [[package]] 3293 + name = "noop_proc_macro" 3294 + version = "0.3.0" 3295 + source = "registry+https://github.com/rust-lang/crates.io-index" 3296 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 3297 + 3298 + [[package]] 2033 3299 name = "nu-ansi-term" 2034 3300 version = "0.50.3" 2035 3301 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2063 3329 ] 2064 3330 2065 3331 [[package]] 3332 + name = "num-bigint-dig" 3333 + version = "0.8.6" 3334 + source = "registry+https://github.com/rust-lang/crates.io-index" 3335 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 3336 + dependencies = [ 3337 + "lazy_static", 3338 + "libm", 3339 + "num-integer", 3340 + "num-iter", 3341 + "num-traits", 3342 + "rand 0.8.5", 3343 + "smallvec", 3344 + "zeroize", 3345 + ] 3346 + 3347 + [[package]] 2066 3348 name = "num-complex" 2067 3349 version = "0.4.6" 2068 3350 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2076 3358 version = "0.2.0" 2077 3359 source = "registry+https://github.com/rust-lang/crates.io-index" 2078 3360 checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" 3361 + 3362 + [[package]] 3363 + name = "num-derive" 3364 + version = "0.4.2" 3365 + source = "registry+https://github.com/rust-lang/crates.io-index" 3366 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 3367 + dependencies = [ 3368 + "proc-macro2", 3369 + "quote", 3370 + "syn", 3371 + ] 2079 3372 2080 3373 [[package]] 2081 3374 name = "num-integer" ··· 2119 3412 ] 2120 3413 2121 3414 [[package]] 3415 + name = "num_cpus" 3416 + version = "1.17.0" 3417 + source = "registry+https://github.com/rust-lang/crates.io-index" 3418 + checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 3419 + dependencies = [ 3420 + "hermit-abi", 3421 + "libc", 3422 + ] 3423 + 3424 + [[package]] 3425 + name = "num_threads" 3426 + version = "0.1.7" 3427 + source = "registry+https://github.com/rust-lang/crates.io-index" 3428 + checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 3429 + dependencies = [ 3430 + "libc", 3431 + ] 3432 + 3433 + [[package]] 3434 + name = "objc2" 3435 + version = "0.6.4" 3436 + source = "registry+https://github.com/rust-lang/crates.io-index" 3437 + checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" 3438 + dependencies = [ 3439 + "objc2-encode", 3440 + ] 3441 + 3442 + [[package]] 3443 + name = "objc2-encode" 3444 + version = "4.1.0" 3445 + source = "registry+https://github.com/rust-lang/crates.io-index" 3446 + checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 3447 + 3448 + [[package]] 3449 + name = "objc2-foundation" 3450 + version = "0.3.2" 3451 + source = "registry+https://github.com/rust-lang/crates.io-index" 3452 + checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" 3453 + dependencies = [ 3454 + "bitflags", 3455 + "objc2", 3456 + ] 3457 + 3458 + [[package]] 2122 3459 name = "object" 2123 3460 version = "0.37.3" 2124 3461 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2203 3540 ] 2204 3541 2205 3542 [[package]] 3543 + name = "p384" 3544 + version = "0.13.1" 3545 + source = "registry+https://github.com/rust-lang/crates.io-index" 3546 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 3547 + dependencies = [ 3548 + "ecdsa", 3549 + "elliptic-curve", 3550 + "primeorder", 3551 + "sha2", 3552 + ] 3553 + 3554 + [[package]] 2206 3555 name = "parking" 2207 3556 version = "2.2.1" 2208 3557 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2226 3575 dependencies = [ 2227 3576 "cfg-if", 2228 3577 "libc", 2229 - "redox_syscall", 3578 + "redox_syscall 0.5.18", 2230 3579 "smallvec", 2231 3580 "windows-link", 2232 3581 ] 3582 + 3583 + [[package]] 3584 + name = "paste" 3585 + version = "1.0.15" 3586 + source = "registry+https://github.com/rust-lang/crates.io-index" 3587 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 3588 + 3589 + [[package]] 3590 + name = "pastey" 3591 + version = "0.1.1" 3592 + source = "registry+https://github.com/rust-lang/crates.io-index" 3593 + checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 2233 3594 2234 3595 [[package]] 2235 3596 name = "pem-rfc7468" ··· 2257 3618 ] 2258 3619 2259 3620 [[package]] 3621 + name = "phf_codegen" 3622 + version = "0.11.3" 3623 + source = "registry+https://github.com/rust-lang/crates.io-index" 3624 + checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 3625 + dependencies = [ 3626 + "phf_generator", 3627 + "phf_shared", 3628 + ] 3629 + 3630 + [[package]] 2260 3631 name = "phf_generator" 2261 3632 version = "0.11.3" 2262 3633 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2321 3692 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2322 3693 2323 3694 [[package]] 3695 + name = "pkcs1" 3696 + version = "0.7.5" 3697 + source = "registry+https://github.com/rust-lang/crates.io-index" 3698 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 3699 + dependencies = [ 3700 + "der", 3701 + "pkcs8", 3702 + "spki", 3703 + ] 3704 + 3705 + [[package]] 2324 3706 name = "pkcs8" 2325 3707 version = "0.10.2" 2326 3708 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2337 3719 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2338 3720 2339 3721 [[package]] 3722 + name = "plain" 3723 + version = "0.2.3" 3724 + source = "registry+https://github.com/rust-lang/crates.io-index" 3725 + checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 3726 + 3727 + [[package]] 3728 + name = "png" 3729 + version = "0.18.1" 3730 + source = "registry+https://github.com/rust-lang/crates.io-index" 3731 + checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" 3732 + dependencies = [ 3733 + "bitflags", 3734 + "crc32fast", 3735 + "fdeflate", 3736 + "flate2", 3737 + "miniz_oxide 0.8.9", 3738 + ] 3739 + 3740 + [[package]] 2340 3741 name = "portable-atomic" 2341 3742 version = "1.13.1" 2342 3743 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2351 3752 "cobs", 2352 3753 "embedded-io 0.4.0", 2353 3754 "embedded-io 0.6.1", 2354 - "heapless", 3755 + "heapless 0.7.17", 2355 3756 "serde", 2356 3757 ] 2357 3758 ··· 2380 3781 ] 2381 3782 2382 3783 [[package]] 3784 + name = "precomputed-hash" 3785 + version = "0.1.1" 3786 + source = "registry+https://github.com/rust-lang/crates.io-index" 3787 + checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 3788 + 3789 + [[package]] 3790 + name = "pretty_assertions" 3791 + version = "1.4.1" 3792 + source = "registry+https://github.com/rust-lang/crates.io-index" 3793 + checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 3794 + dependencies = [ 3795 + "diff", 3796 + "yansi", 3797 + ] 3798 + 3799 + [[package]] 2383 3800 name = "prettyplease" 2384 3801 version = "0.2.37" 2385 3802 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2421 3838 ] 2422 3839 2423 3840 [[package]] 3841 + name = "profiling" 3842 + version = "1.0.17" 3843 + source = "registry+https://github.com/rust-lang/crates.io-index" 3844 + checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" 3845 + dependencies = [ 3846 + "profiling-procmacros", 3847 + ] 3848 + 3849 + [[package]] 3850 + name = "profiling-procmacros" 3851 + version = "1.0.17" 3852 + source = "registry+https://github.com/rust-lang/crates.io-index" 3853 + checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" 3854 + dependencies = [ 3855 + "quote", 3856 + "syn", 3857 + ] 3858 + 3859 + [[package]] 3860 + name = "proptest" 3861 + version = "1.10.0" 3862 + source = "registry+https://github.com/rust-lang/crates.io-index" 3863 + checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" 3864 + dependencies = [ 3865 + "bit-set", 3866 + "bit-vec", 3867 + "bitflags", 3868 + "num-traits", 3869 + "rand 0.9.2", 3870 + "rand_chacha 0.9.0", 3871 + "rand_xorshift", 3872 + "regex-syntax", 3873 + "rusty-fork", 3874 + "tempfile", 3875 + "unarray", 3876 + ] 3877 + 3878 + [[package]] 3879 + name = "pxfm" 3880 + version = "0.1.28" 3881 + source = "registry+https://github.com/rust-lang/crates.io-index" 3882 + checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" 3883 + 3884 + [[package]] 3885 + name = "qoi" 3886 + version = "0.4.1" 3887 + source = "registry+https://github.com/rust-lang/crates.io-index" 3888 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 3889 + dependencies = [ 3890 + "bytemuck", 3891 + ] 3892 + 3893 + [[package]] 3894 + name = "quick-error" 3895 + version = "1.2.3" 3896 + source = "registry+https://github.com/rust-lang/crates.io-index" 3897 + checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 3898 + 3899 + [[package]] 3900 + name = "quick-error" 3901 + version = "2.0.1" 3902 + source = "registry+https://github.com/rust-lang/crates.io-index" 3903 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 3904 + 3905 + [[package]] 2424 3906 name = "quinn" 2425 3907 version = "0.11.9" 2426 3908 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2436 3918 "socket2 0.6.3", 2437 3919 "thiserror 2.0.18", 2438 3920 "tokio", 2439 - "tracing", 3921 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2440 3922 "web-time", 2441 3923 ] 2442 3924 ··· 2457 3939 "slab", 2458 3940 "thiserror 2.0.18", 2459 3941 "tinyvec", 2460 - "tracing", 3942 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2461 3943 "web-time", 2462 3944 ] 2463 3945 ··· 2471 3953 "libc", 2472 3954 "once_cell", 2473 3955 "socket2 0.6.3", 2474 - "tracing", 3956 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 2475 3957 "windows-sys 0.60.2", 2476 3958 ] 2477 3959 ··· 2550 4032 ] 2551 4033 2552 4034 [[package]] 4035 + name = "rand_xorshift" 4036 + version = "0.4.0" 4037 + source = "registry+https://github.com/rust-lang/crates.io-index" 4038 + checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" 4039 + dependencies = [ 4040 + "rand_core 0.9.5", 4041 + ] 4042 + 4043 + [[package]] 4044 + name = "rav1e" 4045 + version = "0.8.1" 4046 + source = "registry+https://github.com/rust-lang/crates.io-index" 4047 + checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" 4048 + dependencies = [ 4049 + "aligned-vec", 4050 + "arbitrary", 4051 + "arg_enum_proc_macro", 4052 + "arrayvec 0.7.6", 4053 + "av-scenechange", 4054 + "av1-grain", 4055 + "bitstream-io", 4056 + "built", 4057 + "cfg-if", 4058 + "interpolate_name", 4059 + "itertools", 4060 + "libc", 4061 + "libfuzzer-sys", 4062 + "log", 4063 + "maybe-rayon", 4064 + "new_debug_unreachable", 4065 + "noop_proc_macro", 4066 + "num-derive", 4067 + "num-traits", 4068 + "paste", 4069 + "profiling", 4070 + "rand 0.9.2", 4071 + "rand_chacha 0.9.0", 4072 + "simd_helpers", 4073 + "thiserror 2.0.18", 4074 + "v_frame", 4075 + "wasm-bindgen", 4076 + ] 4077 + 4078 + [[package]] 4079 + name = "ravif" 4080 + version = "0.13.0" 4081 + source = "registry+https://github.com/rust-lang/crates.io-index" 4082 + checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" 4083 + dependencies = [ 4084 + "avif-serialize", 4085 + "imgref", 4086 + "loop9", 4087 + "quick-error 2.0.1", 4088 + "rav1e", 4089 + "rayon", 4090 + "rgb", 4091 + ] 4092 + 4093 + [[package]] 4094 + name = "rayon" 4095 + version = "1.11.0" 4096 + source = "registry+https://github.com/rust-lang/crates.io-index" 4097 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 4098 + dependencies = [ 4099 + "either", 4100 + "rayon-core", 4101 + ] 4102 + 4103 + [[package]] 4104 + name = "rayon-core" 4105 + version = "1.13.0" 4106 + source = "registry+https://github.com/rust-lang/crates.io-index" 4107 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 4108 + dependencies = [ 4109 + "crossbeam-deque", 4110 + "crossbeam-utils", 4111 + ] 4112 + 4113 + [[package]] 2553 4114 name = "redox_syscall" 2554 4115 version = "0.5.18" 2555 4116 source = "registry+https://github.com/rust-lang/crates.io-index" 2556 4117 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 4118 + dependencies = [ 4119 + "bitflags", 4120 + ] 4121 + 4122 + [[package]] 4123 + name = "redox_syscall" 4124 + version = "0.7.3" 4125 + source = "registry+https://github.com/rust-lang/crates.io-index" 4126 + checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" 2557 4127 dependencies = [ 2558 4128 "bitflags", 2559 4129 ] ··· 2619 4189 source = "registry+https://github.com/rust-lang/crates.io-index" 2620 4190 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 2621 4191 dependencies = [ 2622 - "base64", 4192 + "base64 0.22.1", 2623 4193 "bytes", 2624 4194 "encoding_rs", 2625 4195 "futures-core", ··· 2658 4228 ] 2659 4229 2660 4230 [[package]] 4231 + name = "reserve-port" 4232 + version = "2.4.0" 4233 + source = "registry+https://github.com/rust-lang/crates.io-index" 4234 + checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" 4235 + dependencies = [ 4236 + "thiserror 2.0.18", 4237 + ] 4238 + 4239 + [[package]] 2661 4240 name = "resolv-conf" 2662 4241 version = "0.7.6" 2663 4242 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2674 4253 ] 2675 4254 2676 4255 [[package]] 4256 + name = "rgb" 4257 + version = "0.8.53" 4258 + source = "registry+https://github.com/rust-lang/crates.io-index" 4259 + checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" 4260 + dependencies = [ 4261 + "bytemuck", 4262 + ] 4263 + 4264 + [[package]] 2677 4265 name = "ring" 2678 4266 version = "0.17.14" 2679 4267 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2694 4282 checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" 2695 4283 2696 4284 [[package]] 4285 + name = "rouille" 4286 + version = "3.6.2" 4287 + source = "registry+https://github.com/rust-lang/crates.io-index" 4288 + checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" 4289 + dependencies = [ 4290 + "base64 0.13.1", 4291 + "brotli", 4292 + "chrono", 4293 + "deflate", 4294 + "filetime", 4295 + "multipart", 4296 + "percent-encoding", 4297 + "rand 0.8.5", 4298 + "serde", 4299 + "serde_derive", 4300 + "serde_json", 4301 + "sha1_smol", 4302 + "threadpool", 4303 + "time", 4304 + "tiny_http", 4305 + "url", 4306 + ] 4307 + 4308 + [[package]] 4309 + name = "rsa" 4310 + version = "0.9.10" 4311 + source = "registry+https://github.com/rust-lang/crates.io-index" 4312 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 4313 + dependencies = [ 4314 + "const-oid", 4315 + "digest", 4316 + "num-bigint-dig", 4317 + "num-integer", 4318 + "num-traits", 4319 + "pkcs1", 4320 + "pkcs8", 4321 + "rand_core 0.6.4", 4322 + "signature", 4323 + "spki", 4324 + "subtle", 4325 + "zeroize", 4326 + ] 4327 + 4328 + [[package]] 4329 + name = "rust-multipart-rfc7578_2" 4330 + version = "0.8.0" 4331 + source = "registry+https://github.com/rust-lang/crates.io-index" 4332 + checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" 4333 + dependencies = [ 4334 + "bytes", 4335 + "futures-core", 4336 + "futures-util", 4337 + "http", 4338 + "mime", 4339 + "rand 0.9.2", 4340 + "thiserror 2.0.18", 4341 + ] 4342 + 4343 + [[package]] 2697 4344 name = "rustc-demangle" 2698 4345 version = "0.1.27" 2699 4346 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2716 4363 2717 4364 [[package]] 2718 4365 name = "rustix" 4366 + version = "0.38.44" 4367 + source = "registry+https://github.com/rust-lang/crates.io-index" 4368 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 4369 + dependencies = [ 4370 + "bitflags", 4371 + "errno", 4372 + "libc", 4373 + "linux-raw-sys 0.4.15", 4374 + "windows-sys 0.52.0", 4375 + ] 4376 + 4377 + [[package]] 4378 + name = "rustix" 2719 4379 version = "1.1.4" 2720 4380 source = "registry+https://github.com/rust-lang/crates.io-index" 2721 4381 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" ··· 2723 4383 "bitflags", 2724 4384 "errno", 2725 4385 "libc", 2726 - "linux-raw-sys", 4386 + "linux-raw-sys 0.12.1", 2727 4387 "windows-sys 0.61.2", 2728 4388 ] 2729 4389 ··· 2781 4441 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2782 4442 2783 4443 [[package]] 4444 + name = "rusty-fork" 4445 + version = "0.3.1" 4446 + source = "registry+https://github.com/rust-lang/crates.io-index" 4447 + checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" 4448 + dependencies = [ 4449 + "fnv", 4450 + "quick-error 1.2.3", 4451 + "tempfile", 4452 + "wait-timeout", 4453 + ] 4454 + 4455 + [[package]] 2784 4456 name = "ryu" 2785 4457 version = "1.0.23" 2786 4458 source = "registry+https://github.com/rust-lang/crates.io-index" 2787 4459 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 4460 + 4461 + [[package]] 4462 + name = "safemem" 4463 + version = "0.3.3" 4464 + source = "registry+https://github.com/rust-lang/crates.io-index" 4465 + checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 2788 4466 2789 4467 [[package]] 2790 4468 name = "same-file" ··· 2930 4608 ] 2931 4609 2932 4610 [[package]] 4611 + name = "serde_ipld_dagjson" 4612 + version = "0.2.1" 4613 + source = "registry+https://github.com/rust-lang/crates.io-index" 4614 + checksum = "82d2d9d1f29999ee9a3d774fe2a5db4cc199da5178d0350f5e4482ea04252aee" 4615 + dependencies = [ 4616 + "ipld-core", 4617 + "serde", 4618 + "serde_json", 4619 + ] 4620 + 4621 + [[package]] 2933 4622 name = "serde_json" 2934 4623 version = "1.0.149" 2935 4624 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3001 4690 source = "registry+https://github.com/rust-lang/crates.io-index" 3002 4691 checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" 3003 4692 dependencies = [ 3004 - "base64", 4693 + "base64 0.22.1", 3005 4694 "chrono", 3006 4695 "hex", 3007 4696 "serde_core", ··· 3034 4723 ] 3035 4724 3036 4725 [[package]] 4726 + name = "sha1_smol" 4727 + version = "1.0.1" 4728 + source = "registry+https://github.com/rust-lang/crates.io-index" 4729 + checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" 4730 + 4731 + [[package]] 3037 4732 name = "sha2" 3038 4733 version = "0.10.9" 3039 4734 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3086 4781 checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 3087 4782 3088 4783 [[package]] 4784 + name = "simd_cesu8" 4785 + version = "1.1.1" 4786 + source = "registry+https://github.com/rust-lang/crates.io-index" 4787 + checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" 4788 + dependencies = [ 4789 + "rustc_version", 4790 + "simdutf8", 4791 + ] 4792 + 4793 + [[package]] 4794 + name = "simd_helpers" 4795 + version = "0.1.0" 4796 + source = "registry+https://github.com/rust-lang/crates.io-index" 4797 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 4798 + dependencies = [ 4799 + "quote", 4800 + ] 4801 + 4802 + [[package]] 4803 + name = "simdutf8" 4804 + version = "0.1.5" 4805 + source = "registry+https://github.com/rust-lang/crates.io-index" 4806 + checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 4807 + 4808 + [[package]] 3089 4809 name = "siphasher" 3090 4810 version = "1.0.2" 3091 4811 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3171 4891 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 3172 4892 3173 4893 [[package]] 4894 + name = "string_cache" 4895 + version = "0.8.9" 4896 + source = "registry+https://github.com/rust-lang/crates.io-index" 4897 + checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 4898 + dependencies = [ 4899 + "new_debug_unreachable", 4900 + "parking_lot", 4901 + "phf_shared", 4902 + "precomputed-hash", 4903 + "serde", 4904 + ] 4905 + 4906 + [[package]] 4907 + name = "string_cache_codegen" 4908 + version = "0.5.4" 4909 + source = "registry+https://github.com/rust-lang/crates.io-index" 4910 + checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 4911 + dependencies = [ 4912 + "phf_generator", 4913 + "phf_shared", 4914 + "proc-macro2", 4915 + "quote", 4916 + ] 4917 + 4918 + [[package]] 3174 4919 name = "strsim" 3175 4920 version = "0.11.1" 3176 4921 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3276 5021 "fastrand", 3277 5022 "getrandom 0.3.4", 3278 5023 "once_cell", 3279 - "rustix", 5024 + "rustix 1.1.4", 3280 5025 "windows-sys 0.61.2", 3281 5026 ] 3282 5027 3283 5028 [[package]] 5029 + name = "tendril" 5030 + version = "0.4.3" 5031 + source = "registry+https://github.com/rust-lang/crates.io-index" 5032 + checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 5033 + dependencies = [ 5034 + "futf", 5035 + "mac 0.1.1", 5036 + "utf-8", 5037 + ] 5038 + 5039 + [[package]] 3284 5040 name = "termcolor" 3285 5041 version = "1.4.1" 3286 5042 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3295 5051 source = "registry+https://github.com/rust-lang/crates.io-index" 3296 5052 checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 3297 5053 dependencies = [ 3298 - "rustix", 5054 + "rustix 1.1.4", 3299 5055 "windows-sys 0.60.2", 3300 5056 ] 3301 5057 ··· 3359 5115 ] 3360 5116 3361 5117 [[package]] 5118 + name = "threadpool" 5119 + version = "1.8.1" 5120 + source = "registry+https://github.com/rust-lang/crates.io-index" 5121 + checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 5122 + dependencies = [ 5123 + "num_cpus", 5124 + ] 5125 + 5126 + [[package]] 5127 + name = "tiff" 5128 + version = "0.6.1" 5129 + source = "registry+https://github.com/rust-lang/crates.io-index" 5130 + checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" 5131 + dependencies = [ 5132 + "jpeg-decoder", 5133 + "miniz_oxide 0.4.4", 5134 + "weezl", 5135 + ] 5136 + 5137 + [[package]] 5138 + name = "tiff" 5139 + version = "0.11.3" 5140 + source = "registry+https://github.com/rust-lang/crates.io-index" 5141 + checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" 5142 + dependencies = [ 5143 + "fax", 5144 + "flate2", 5145 + "half", 5146 + "quick-error 2.0.1", 5147 + "weezl", 5148 + "zune-jpeg", 5149 + ] 5150 + 5151 + [[package]] 3362 5152 name = "time" 3363 5153 version = "0.3.47" 3364 5154 source = "registry+https://github.com/rust-lang/crates.io-index" 3365 5155 checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" 3366 5156 dependencies = [ 3367 5157 "deranged", 5158 + "itoa", 5159 + "libc", 3368 5160 "num-conv", 5161 + "num_threads", 3369 5162 "powerfmt", 3370 5163 "serde_core", 3371 5164 "time-core", 5165 + "time-macros", 3372 5166 ] 3373 5167 3374 5168 [[package]] ··· 3376 5170 version = "0.1.8" 3377 5171 source = "registry+https://github.com/rust-lang/crates.io-index" 3378 5172 checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 5173 + 5174 + [[package]] 5175 + name = "time-macros" 5176 + version = "0.2.27" 5177 + source = "registry+https://github.com/rust-lang/crates.io-index" 5178 + checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" 5179 + dependencies = [ 5180 + "num-conv", 5181 + "time-core", 5182 + ] 5183 + 5184 + [[package]] 5185 + name = "tiny_http" 5186 + version = "0.12.0" 5187 + source = "registry+https://github.com/rust-lang/crates.io-index" 5188 + checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" 5189 + dependencies = [ 5190 + "ascii", 5191 + "chunked_transfer", 5192 + "httpdate", 5193 + "log", 5194 + ] 3379 5195 3380 5196 [[package]] 3381 5197 name = "tinystr" ··· 3582 5398 "tokio", 3583 5399 "tower-layer", 3584 5400 "tower-service", 5401 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 3585 5402 ] 3586 5403 3587 5404 [[package]] ··· 3605 5422 "tower", 3606 5423 "tower-layer", 3607 5424 "tower-service", 5425 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 3608 5426 ] 3609 5427 3610 5428 [[package]] ··· 3625 5443 source = "registry+https://github.com/rust-lang/crates.io-index" 3626 5444 checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 3627 5445 dependencies = [ 5446 + "log", 3628 5447 "pin-project-lite", 3629 - "tracing-attributes", 3630 - "tracing-core", 5448 + "tracing-attributes 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", 5449 + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 5450 + ] 5451 + 5452 + [[package]] 5453 + name = "tracing" 5454 + version = "0.1.44" 5455 + source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" 5456 + dependencies = [ 5457 + "pin-project-lite", 5458 + "tracing-attributes 0.1.31 (git+https://github.com/tokio-rs/tracing)", 5459 + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", 3631 5460 ] 3632 5461 3633 5462 [[package]] ··· 3642 5471 ] 3643 5472 3644 5473 [[package]] 5474 + name = "tracing-attributes" 5475 + version = "0.1.31" 5476 + source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" 5477 + dependencies = [ 5478 + "proc-macro2", 5479 + "quote", 5480 + "syn", 5481 + ] 5482 + 5483 + [[package]] 3645 5484 name = "tracing-core" 3646 5485 version = "0.1.36" 3647 5486 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3652 5491 ] 3653 5492 3654 5493 [[package]] 5494 + name = "tracing-core" 5495 + version = "0.1.36" 5496 + source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" 5497 + dependencies = [ 5498 + "valuable", 5499 + ] 5500 + 5501 + [[package]] 3655 5502 name = "tracing-log" 3656 5503 version = "0.2.0" 3657 5504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3659 5506 dependencies = [ 3660 5507 "log", 3661 5508 "once_cell", 3662 - "tracing-core", 5509 + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 5510 + ] 5511 + 5512 + [[package]] 5513 + name = "tracing-log" 5514 + version = "0.2.0" 5515 + source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" 5516 + dependencies = [ 5517 + "log", 5518 + "once_cell", 5519 + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", 3663 5520 ] 3664 5521 3665 5522 [[package]] ··· 3675 5532 "sharded-slab", 3676 5533 "smallvec", 3677 5534 "thread_local", 3678 - "tracing", 3679 - "tracing-core", 3680 - "tracing-log", 5535 + "time", 5536 + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 5537 + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 5538 + "tracing-log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 5539 + ] 5540 + 5541 + [[package]] 5542 + name = "tracing-subscriber" 5543 + version = "0.3.23" 5544 + source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" 5545 + dependencies = [ 5546 + "matchers", 5547 + "nu-ansi-term", 5548 + "once_cell", 5549 + "regex-automata", 5550 + "sharded-slab", 5551 + "smallvec", 5552 + "thread_local", 5553 + "tracing 0.1.44 (git+https://github.com/tokio-rs/tracing)", 5554 + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", 5555 + "tracing-log 0.2.0 (git+https://github.com/tokio-rs/tracing)", 3681 5556 ] 3682 5557 3683 5558 [[package]] ··· 3739 5614 ] 3740 5615 3741 5616 [[package]] 5617 + name = "twoway" 5618 + version = "0.1.8" 5619 + source = "registry+https://github.com/rust-lang/crates.io-index" 5620 + checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 5621 + dependencies = [ 5622 + "memchr", 5623 + ] 5624 + 5625 + [[package]] 5626 + name = "typeid" 5627 + version = "1.0.3" 5628 + source = "registry+https://github.com/rust-lang/crates.io-index" 5629 + checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" 5630 + 5631 + [[package]] 3742 5632 name = "typenum" 3743 5633 version = "1.19.0" 3744 5634 source = "registry+https://github.com/rust-lang/crates.io-index" 3745 5635 checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 3746 5636 3747 5637 [[package]] 5638 + name = "typetag" 5639 + version = "0.2.21" 5640 + source = "registry+https://github.com/rust-lang/crates.io-index" 5641 + checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" 5642 + dependencies = [ 5643 + "erased-serde", 5644 + "inventory", 5645 + "once_cell", 5646 + "serde", 5647 + "typetag-impl", 5648 + ] 5649 + 5650 + [[package]] 5651 + name = "typetag-impl" 5652 + version = "0.2.21" 5653 + source = "registry+https://github.com/rust-lang/crates.io-index" 5654 + checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" 5655 + dependencies = [ 5656 + "proc-macro2", 5657 + "quote", 5658 + "syn", 5659 + ] 5660 + 5661 + [[package]] 5662 + name = "unarray" 5663 + version = "0.1.4" 5664 + source = "registry+https://github.com/rust-lang/crates.io-index" 5665 + checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 5666 + 5667 + [[package]] 5668 + name = "unicase" 5669 + version = "2.9.0" 5670 + source = "registry+https://github.com/rust-lang/crates.io-index" 5671 + checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" 5672 + 5673 + [[package]] 3748 5674 name = "unicode-ident" 3749 5675 version = "1.0.24" 3750 5676 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3782 5708 3783 5709 [[package]] 3784 5710 name = "unsigned-varint" 5711 + version = "0.7.2" 5712 + source = "registry+https://github.com/rust-lang/crates.io-index" 5713 + checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" 5714 + 5715 + [[package]] 5716 + name = "unsigned-varint" 3785 5717 version = "0.8.0" 3786 5718 source = "registry+https://github.com/rust-lang/crates.io-index" 3787 5719 checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" ··· 3821 5753 version = "0.2.2" 3822 5754 source = "registry+https://github.com/rust-lang/crates.io-index" 3823 5755 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 5756 + 5757 + [[package]] 5758 + name = "uuid" 5759 + version = "1.22.0" 5760 + source = "registry+https://github.com/rust-lang/crates.io-index" 5761 + checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" 5762 + dependencies = [ 5763 + "js-sys", 5764 + "wasm-bindgen", 5765 + ] 5766 + 5767 + [[package]] 5768 + name = "v_frame" 5769 + version = "0.3.9" 5770 + source = "registry+https://github.com/rust-lang/crates.io-index" 5771 + checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" 5772 + dependencies = [ 5773 + "aligned-vec", 5774 + "num-traits", 5775 + "wasm-bindgen", 5776 + ] 3824 5777 3825 5778 [[package]] 3826 5779 name = "valuable" ··· 3835 5788 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3836 5789 3837 5790 [[package]] 5791 + name = "viuer" 5792 + version = "0.9.2" 5793 + source = "registry+https://github.com/rust-lang/crates.io-index" 5794 + checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" 5795 + dependencies = [ 5796 + "ansi_colours", 5797 + "base64 0.22.1", 5798 + "console", 5799 + "crossterm", 5800 + "image", 5801 + "lazy_static", 5802 + "tempfile", 5803 + "termcolor", 5804 + ] 5805 + 5806 + [[package]] 5807 + name = "wait-timeout" 5808 + version = "0.2.1" 5809 + source = "registry+https://github.com/rust-lang/crates.io-index" 5810 + checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 5811 + dependencies = [ 5812 + "libc", 5813 + ] 5814 + 5815 + [[package]] 3838 5816 name = "walkdir" 3839 5817 version = "2.5.0" 3840 5818 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4000 5978 ] 4001 5979 4002 5980 [[package]] 5981 + name = "webbrowser" 5982 + version = "1.2.0" 5983 + source = "registry+https://github.com/rust-lang/crates.io-index" 5984 + checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" 5985 + dependencies = [ 5986 + "core-foundation 0.10.1", 5987 + "jni", 5988 + "log", 5989 + "ndk-context", 5990 + "objc2", 5991 + "objc2-foundation", 5992 + "url", 5993 + "web-sys", 5994 + ] 5995 + 5996 + [[package]] 5997 + name = "webpage" 5998 + version = "2.0.1" 5999 + source = "registry+https://github.com/rust-lang/crates.io-index" 6000 + checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" 6001 + dependencies = [ 6002 + "html5ever", 6003 + "markup5ever_rcdom", 6004 + "serde_json", 6005 + "url", 6006 + ] 6007 + 6008 + [[package]] 4003 6009 name = "webpki-roots" 4004 6010 version = "1.0.6" 4005 6011 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4009 6015 ] 4010 6016 4011 6017 [[package]] 6018 + name = "weezl" 6019 + version = "0.1.12" 6020 + source = "registry+https://github.com/rust-lang/crates.io-index" 6021 + checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" 6022 + 6023 + [[package]] 4012 6024 name = "widestring" 4013 6025 version = "1.2.1" 4014 6026 source = "registry+https://github.com/rust-lang/crates.io-index" 4015 6027 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 4016 6028 4017 6029 [[package]] 6030 + name = "winapi" 6031 + version = "0.3.9" 6032 + source = "registry+https://github.com/rust-lang/crates.io-index" 6033 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 6034 + dependencies = [ 6035 + "winapi-i686-pc-windows-gnu", 6036 + "winapi-x86_64-pc-windows-gnu", 6037 + ] 6038 + 6039 + [[package]] 6040 + name = "winapi-i686-pc-windows-gnu" 6041 + version = "0.4.0" 6042 + source = "registry+https://github.com/rust-lang/crates.io-index" 6043 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 6044 + 6045 + [[package]] 4018 6046 name = "winapi-util" 4019 6047 version = "0.1.11" 4020 6048 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4022 6050 dependencies = [ 4023 6051 "windows-sys 0.61.2", 4024 6052 ] 6053 + 6054 + [[package]] 6055 + name = "winapi-x86_64-pc-windows-gnu" 6056 + version = "0.4.0" 6057 + source = "registry+https://github.com/rust-lang/crates.io-index" 6058 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 4025 6059 4026 6060 [[package]] 4027 6061 name = "windows-core" ··· 4107 6141 version = "0.52.0" 4108 6142 source = "registry+https://github.com/rust-lang/crates.io-index" 4109 6143 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 6144 + dependencies = [ 6145 + "windows-targets 0.52.6", 6146 + ] 6147 + 6148 + [[package]] 6149 + name = "windows-sys" 6150 + version = "0.59.0" 6151 + source = "registry+https://github.com/rust-lang/crates.io-index" 6152 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 4110 6153 dependencies = [ 4111 6154 "windows-targets 0.52.6", 4112 6155 ] ··· 4362 6405 checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 4363 6406 4364 6407 [[package]] 6408 + name = "xml5ever" 6409 + version = "0.18.1" 6410 + source = "registry+https://github.com/rust-lang/crates.io-index" 6411 + checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" 6412 + dependencies = [ 6413 + "log", 6414 + "mac 0.1.1", 6415 + "markup5ever", 6416 + ] 6417 + 6418 + [[package]] 6419 + name = "y4m" 6420 + version = "0.8.0" 6421 + source = "registry+https://github.com/rust-lang/crates.io-index" 6422 + checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" 6423 + 6424 + [[package]] 4365 6425 name = "yansi" 4366 6426 version = "1.0.1" 4367 6427 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4436 6496 version = "1.8.2" 4437 6497 source = "registry+https://github.com/rust-lang/crates.io-index" 4438 6498 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 6499 + dependencies = [ 6500 + "serde", 6501 + ] 4439 6502 4440 6503 [[package]] 4441 6504 name = "zerotrie" ··· 4503 6566 "cc", 4504 6567 "pkg-config", 4505 6568 ] 6569 + 6570 + [[package]] 6571 + name = "zune-core" 6572 + version = "0.5.1" 6573 + source = "registry+https://github.com/rust-lang/crates.io-index" 6574 + checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" 6575 + 6576 + [[package]] 6577 + name = "zune-inflate" 6578 + version = "0.2.54" 6579 + source = "registry+https://github.com/rust-lang/crates.io-index" 6580 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 6581 + dependencies = [ 6582 + "simd-adler32", 6583 + ] 6584 + 6585 + [[package]] 6586 + name = "zune-jpeg" 6587 + version = "0.5.14" 6588 + source = "registry+https://github.com/rust-lang/crates.io-index" 6589 + checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" 6590 + dependencies = [ 6591 + "zune-core", 6592 + ]
+1 -1
Cargo.toml
··· 1 1 [workspace] 2 2 resolver = "2" 3 - members = ["crates/jacquard-common", "crates/jacquard-lexicon", "crates/jacquard-derive", "crates/jacquard-lexgen", "crates/jacquard-identity", "crates/jacquard-api"] 3 + members = ["crates/*"] 4 4 5 5 6 6 [workspace.package]
+1 -17
crates/jacquard-common/src/types/did.rs
··· 166 166 } 167 167 } 168 168 169 - impl FromStr for Did { 170 - type Err = AtStrError; 171 - 172 - fn from_str(s: &str) -> Result<Self, Self::Err> { 173 - Self::new_owned(s) 174 - } 175 - } 176 - 177 - impl FromStr for Did<CowStr<'static>> { 178 - type Err = AtStrError; 179 - 180 - fn from_str(s: &str) -> Result<Self, Self::Err> { 181 - Self::new_owned(s) 182 - } 183 - } 184 - 185 - impl FromStr for Did<String> { 169 + impl<S: Bos<str> + FromStr> FromStr for Did<S> { 186 170 type Err = AtStrError; 187 171 188 172 fn from_str(s: &str) -> Result<Self, Self::Err> {
+1 -17
crates/jacquard-common/src/types/nsid.rs
··· 138 138 } 139 139 } 140 140 141 - impl FromStr for Nsid { 142 - type Err = AtStrError; 143 - 144 - fn from_str(s: &str) -> Result<Self, Self::Err> { 145 - Self::new_owned(s) 146 - } 147 - } 148 - 149 - impl FromStr for Nsid<CowStr<'static>> { 150 - type Err = AtStrError; 151 - 152 - fn from_str(s: &str) -> Result<Self, Self::Err> { 153 - Self::new_owned(s) 154 - } 155 - } 156 - 157 - impl FromStr for Nsid<String> { 141 + impl<S: Bos<str> + FromStr> FromStr for Nsid<S> { 158 142 type Err = AtStrError; 159 143 160 144 fn from_str(s: &str) -> Result<Self, Self::Err> {
+6 -6
crates/jacquard-common/src/xrpc.rs
··· 336 336 #[cfg(not(target_arch = "wasm32"))] 337 337 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 338 338 where 339 - R: XrpcRequest + Send + Sync, 339 + R: XrpcRequest + Send + Sync + serde::Serialize, 340 340 <R as XrpcRequest>::Response: Send + Sync, 341 341 Self: Sync; 342 342 ··· 344 344 #[cfg(target_arch = "wasm32")] 345 345 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 346 346 where 347 - R: XrpcRequest + Send + Sync, 347 + R: XrpcRequest + Send + Sync + serde::Serialize, 348 348 <R as XrpcRequest>::Response: Send + Sync; 349 349 350 350 /// Send an XRPC request and parse the response ··· 355 355 opts: CallOptions<'_>, 356 356 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 357 357 where 358 - R: XrpcRequest + Send + Sync, 358 + R: XrpcRequest + Send + Sync + serde::Serialize, 359 359 <R as XrpcRequest>::Response: Send + Sync, 360 360 Self: Sync; 361 361 ··· 367 367 opts: CallOptions<'_>, 368 368 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 369 369 where 370 - R: XrpcRequest + Send + Sync, 370 + R: XrpcRequest + Send + Sync + serde::Serialize, 371 371 <R as XrpcRequest>::Response: Send + Sync; 372 372 } 373 373 ··· 381 381 request: R, 382 382 ) -> impl Future<Output = Result<StreamingResponse, StreamError>> + Send 383 383 where 384 - R: XrpcRequest + Send + Sync, 384 + R: XrpcRequest + Send + Sync + serde::Serialize, 385 385 <R as XrpcRequest>::Response: Send + Sync, 386 386 Self: Sync; 387 387 ··· 392 392 request: R, 393 393 ) -> impl Future<Output = Result<StreamingResponse, StreamError>> 394 394 where 395 - R: XrpcRequest + Send + Sync, 395 + R: XrpcRequest + Send + Sync + serde::Serialize, 396 396 <R as XrpcRequest>::Response: Send + Sync; 397 397 398 398 /// Stream an XRPC procedure call and its response
+149 -105
crates/jacquard-oauth/src/atproto.rs
··· 1 + use std::str::FromStr; 2 + 1 3 use crate::types::OAuthClientMetadata; 2 4 use crate::{keyset::Keyset, scopes::Scope}; 3 - use jacquard_common::cowstr::ToCowStr; 4 5 use jacquard_common::deps::fluent_uri::Uri; 5 - use jacquard_common::{CowStr, IntoStatic}; 6 + use jacquard_common::{BosStr, IntoStatic}; 6 7 use serde::{Deserialize, Serialize}; 7 - use smol_str::{SmolStr, ToSmolStr}; 8 + use smol_str::SmolStr; 8 9 use thiserror::Error; 9 10 10 11 /// Errors that can occur when building AT Protocol OAuth client metadata. ··· 78 79 PrivateKeyJwt, 79 80 } 80 81 81 - impl From<AuthMethod> for CowStr<'static> { 82 + impl From<AuthMethod> for SmolStr { 83 + fn from(value: AuthMethod) -> Self { 84 + match value { 85 + AuthMethod::None => SmolStr::new_static("none"), 86 + AuthMethod::PrivateKeyJwt => SmolStr::new_static("private_key_jwt"), 87 + } 88 + } 89 + } 90 + 91 + impl From<AuthMethod> for &'static str { 82 92 fn from(value: AuthMethod) -> Self { 83 93 match value { 84 - AuthMethod::None => CowStr::new_static("none"), 85 - AuthMethod::PrivateKeyJwt => CowStr::new_static("private_key_jwt"), 94 + AuthMethod::None => "none", 95 + AuthMethod::PrivateKeyJwt => "private_key_jwt", 86 96 } 87 97 } 88 98 } ··· 97 107 RefreshToken, 98 108 } 99 109 100 - impl From<GrantType> for CowStr<'static> { 110 + impl From<GrantType> for SmolStr { 101 111 fn from(value: GrantType) -> Self { 102 112 match value { 103 - GrantType::AuthorizationCode => CowStr::new_static("authorization_code"), 104 - GrantType::RefreshToken => CowStr::new_static("refresh_token"), 113 + GrantType::AuthorizationCode => SmolStr::new_static("authorization_code"), 114 + GrantType::RefreshToken => SmolStr::new_static("refresh_token"), 115 + } 116 + } 117 + } 118 + 119 + impl From<GrantType> for &'static str { 120 + fn from(value: GrantType) -> Self { 121 + match value { 122 + GrantType::AuthorizationCode => "authorization_code", 123 + GrantType::RefreshToken => "refresh_token", 105 124 } 106 125 } 107 126 } ··· 113 132 /// typed fields for URIs and scopes rather than raw strings. Use [`atproto_client_metadata`] 114 133 /// to convert this into the wire format expected by OAuth servers. 115 134 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 116 - pub struct AtprotoClientMetadata<'m> { 135 + pub struct AtprotoClientMetadata<S: BosStr + FromStr + Ord> 136 + where 137 + <S as FromStr>::Err: core::fmt::Debug, 138 + { 117 139 /// The unique identifier for this client, typically the URL of its metadata document. 118 140 pub client_id: Uri<String>, 119 141 /// The URI of the client's homepage or information page. ··· 123 145 /// The grant types this client will use. 124 146 pub grant_types: Vec<GrantType>, 125 147 /// The OAuth scopes this client requests; must include `atproto`. 126 - #[serde(borrow)] 127 - pub scopes: Vec<Scope<'m>>, 148 + pub scopes: Vec<Scope<S>>, 128 149 /// URI pointing to the client's JWK Set; mutually exclusive with inline `jwks`. 129 150 pub jwks_uri: Option<Uri<String>>, 130 151 /// Human-readable display name for the client. 131 - pub client_name: Option<SmolStr>, 152 + pub client_name: Option<S>, 132 153 /// URI of the client's logo image. 133 154 pub logo_uri: Option<Uri<String>>, 134 155 /// URI of the client's terms of service document. ··· 137 158 pub privacy_policy_uri: Option<Uri<String>>, 138 159 } 139 160 140 - impl<'m> IntoStatic for AtprotoClientMetadata<'m> { 141 - type Output = AtprotoClientMetadata<'static>; 142 - fn into_static(self) -> AtprotoClientMetadata<'static> { 161 + impl<S> IntoStatic for AtprotoClientMetadata<S> 162 + where 163 + S: BosStr + IntoStatic + Ord + FromStr, 164 + <S as FromStr>::Err: core::fmt::Debug, 165 + S::Output: BosStr + FromStr + Ord, 166 + <S::Output as FromStr>::Err: core::fmt::Debug, 167 + { 168 + type Output = AtprotoClientMetadata<S::Output>; 169 + fn into_static(self) -> AtprotoClientMetadata<S::Output> { 143 170 AtprotoClientMetadata { 144 171 client_id: self.client_id, 145 172 client_uri: self.client_uri, ··· 147 174 grant_types: self.grant_types, 148 175 scopes: self.scopes.into_static(), 149 176 jwks_uri: self.jwks_uri, 150 - client_name: self.client_name, 177 + client_name: self.client_name.into_static(), 151 178 logo_uri: self.logo_uri, 152 179 tos_uri: self.tos_uri, 153 180 privacy_policy_uri: None, ··· 155 182 } 156 183 } 157 184 158 - impl<'m> AtprotoClientMetadata<'m> { 185 + impl<S> AtprotoClientMetadata<S> 186 + where 187 + S: BosStr + IntoStatic + Ord + FromStr, 188 + <S as FromStr>::Err: core::fmt::Debug, 189 + S::Output: BosStr + FromStr + Ord, 190 + <S::Output as FromStr>::Err: core::fmt::Debug, 191 + { 159 192 /// Attach optional production branding fields to the metadata. 160 193 /// 161 194 /// Chainable builder method for setting display name, logo, and policy URLs after 162 195 /// constructing the base metadata. 163 196 pub fn with_prod_info( 164 197 mut self, 165 - client_name: &str, 198 + client_name: S, 166 199 logo_uri: Option<Uri<String>>, 167 200 tos_uri: Option<Uri<String>>, 168 201 privacy_policy_uri: Option<Uri<String>>, 169 202 ) -> Self { 170 - self.client_name = Some(client_name.to_smolstr()); 203 + self.client_name = Some(client_name); 171 204 self.logo_uri = logo_uri; 172 205 self.tos_uri = tos_uri; 173 206 self.privacy_policy_uri = privacy_policy_uri; ··· 182 215 pub fn default_localhost() -> Self { 183 216 Self::new_localhost( 184 217 None, 185 - Some(Scope::parse_multiple("atproto transition:generic").unwrap()), 218 + Some(vec![ 219 + Scope::Atproto, 220 + Scope::Transition(crate::scopes::TransitionScope::Generic), 221 + ]), 186 222 ) 187 223 } 188 224 ··· 194 230 /// are used. 195 231 pub fn new_localhost( 196 232 redirect_uris: Option<Vec<Uri<String>>>, 197 - scopes: Option<Vec<Scope<'static>>>, 198 - ) -> AtprotoClientMetadata<'static> { 233 + scopes: Option<Vec<Scope<S>>>, 234 + ) -> AtprotoClientMetadata<S> { 199 235 // determine client_id 200 236 #[derive(serde::Serialize)] 201 - struct Parameters<'a> { 237 + struct Parameters { 202 238 #[serde(skip_serializing_if = "Option::is_none")] 203 - redirect_uri: Option<Vec<CowStr<'a>>>, 239 + redirect_uri: Option<Vec<SmolStr>>, 204 240 #[serde(skip_serializing_if = "Option::is_none")] 205 - scope: Option<CowStr<'a>>, 241 + scope: Option<SmolStr>, 206 242 } 207 243 let redir_str = redirect_uris.as_ref().map(|uris| { 208 244 uris.iter() 209 - .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) 245 + .map(|u| SmolStr::from(u.as_str().trim_end_matches("/"))) 210 246 .collect() 211 247 }); 212 248 let query = serde_html_form::to_string(Parameters { 213 249 redirect_uri: redir_str, 214 250 scope: scopes 215 251 .as_ref() 216 - .map(|s| Scope::serialize_multiple(s.as_slice())), 252 + .map(|s| SmolStr::from(Scope::serialize_multiple(s.as_slice()).as_str())), 217 253 }) 218 254 .ok(); 219 255 let mut client_id = String::from("http://localhost/"); ··· 246 282 /// selects the appropriate `token_endpoint_auth_method` based on whether a keyset is provided, 247 283 /// and serializes scopes and grant types into their string representations. Returns an error 248 284 /// if any required field is missing or invalid. 249 - pub fn atproto_client_metadata<'m>( 250 - metadata: AtprotoClientMetadata<'m>, 285 + pub fn atproto_client_metadata<S>( 286 + metadata: &AtprotoClientMetadata<S>, 251 287 keyset: &Option<Keyset>, 252 - ) -> Result<OAuthClientMetadata<'static>> { 288 + ) -> Result<OAuthClientMetadata<S>> 289 + where 290 + S: BosStr + Ord + FromStr + Clone, 291 + <S as FromStr>::Err: core::fmt::Debug, 292 + { 253 293 let is_loopback = metadata.client_id.scheme().as_str() == "http" 254 294 && metadata.client_id.authority().map(|a| a.host()) == Some("localhost"); 255 295 let application_type = if is_loopback { 256 - Some(CowStr::new_static("native")) 296 + Some(S::from_static("native")) 257 297 } else { 258 - Some(CowStr::new_static("web")) 298 + Some(S::from_static("web")) 259 299 }; 260 300 if metadata.redirect_uris.is_empty() { 261 301 return Err(Error::EmptyRedirectUris); ··· 272 312 } else { 273 313 None 274 314 }; 275 - (AuthMethod::PrivateKeyJwt, metadata.jwks_uri, jwks) 315 + (AuthMethod::PrivateKeyJwt, metadata.jwks_uri.as_ref(), jwks) 276 316 } else { 277 317 (AuthMethod::None, None, None) 278 318 }; 279 - let client_id = metadata 280 - .client_id 281 - .as_str() 282 - .trim_end_matches("/") 283 - .to_string(); 319 + let client_id = metadata.client_id.as_str(); 284 320 let client_uri = metadata 285 321 .client_uri 286 322 .as_ref() 287 - .map(|u| u.as_str().trim_end_matches("/").to_string().into()); 323 + .and_then(|u| S::from_str(u.as_str()).ok()); 288 324 let redirect_uris = metadata 289 325 .redirect_uris 290 326 .iter() 291 - .map(|u| u.as_str().trim_end_matches("/").to_string().into()) 327 + .filter_map(|u| S::from_str(u.as_str()).ok()) 292 328 .collect(); 293 - let jwks_uri = jwks_uri.map(|u| u.as_str().trim_end_matches("/").to_string().into()); 329 + let jwks_uri = jwks_uri.as_ref().and_then(|u| S::from_str(u.as_str()).ok()); 294 330 Ok(OAuthClientMetadata { 295 - client_id: client_id.into(), 331 + client_id: S::from_str(client_id).unwrap(), 296 332 client_uri, 297 333 redirect_uris, 298 - application_type, 299 - token_endpoint_auth_method: Some(auth_method.into()), 300 - grant_types: Some(metadata.grant_types.into_iter().map(|v| v.into()).collect()), 301 - response_types: vec!["code".to_cowstr()], 302 - scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())), 334 + application_type: application_type, 335 + token_endpoint_auth_method: Some(S::from_static(auth_method.into())), 336 + grant_types: Some( 337 + metadata 338 + .grant_types 339 + .iter() 340 + .map(|v| S::from_static(v.clone().into())) 341 + .collect(), 342 + ), 343 + response_types: vec![S::from_static("code")], 344 + scope: Some( 345 + S::from_str(Scope::serialize_multiple(metadata.scopes.as_slice()).as_str()).unwrap(), 346 + ), 303 347 dpop_bound_access_tokens: Some(true), 304 348 jwks_uri, 305 349 jwks, 306 350 token_endpoint_auth_signing_alg: if keyset.is_some() { 307 - Some(CowStr::new_static("ES256")) 351 + Some(S::from_static("ES256")) 308 352 } else { 309 353 None 310 354 }, 311 - client_name: metadata.client_name, 355 + client_name: metadata.client_name.as_ref().map(|c| c.clone()), 312 356 logo_uri: metadata 313 357 .logo_uri 314 358 .as_ref() 315 - .map(|u| u.as_str().to_string().into()), 359 + .and_then(|u| S::from_str(u.as_str()).ok()), 316 360 tos_uri: metadata 317 361 .tos_uri 318 362 .as_ref() 319 - .map(|u| u.as_str().to_string().into()), 363 + .and_then(|u| S::from_str(u.as_str()).ok()), 320 364 privacy_policy_uri: metadata 321 365 .privacy_policy_uri 322 366 .as_ref() 323 - .map(|u| u.as_str().to_string().into()), 367 + .and_then(|u| S::from_str(u.as_str()).ok()), 324 368 }) 325 369 } 326 370 ··· 342 386 #[test] 343 387 fn test_localhost_client_metadata_default() { 344 388 assert_eq!( 345 - atproto_client_metadata(AtprotoClientMetadata::new_localhost(None, None), &None) 389 + atproto_client_metadata(&AtprotoClientMetadata::new_localhost(None, None), &None) 346 390 .unwrap(), 347 391 OAuthClientMetadata { 348 - client_id: CowStr::new_static("http://localhost"), 392 + client_id: SmolStr::new_static("http://localhost/"), 349 393 client_uri: None, 350 394 redirect_uris: vec![ 351 - CowStr::new_static("http://127.0.0.1"), 352 - CowStr::new_static("http://[::1]"), 395 + SmolStr::new_static("http://127.0.0.1"), 396 + SmolStr::new_static("http://[::1]"), 353 397 ], 354 - application_type: Some(CowStr::new_static("native")), 355 - scope: Some(CowStr::new_static("atproto")), 398 + application_type: Some(SmolStr::new_static("native")), 399 + scope: Some(SmolStr::new_static("atproto")), 356 400 grant_types: Some(vec![ 357 - "authorization_code".to_cowstr(), 358 - "refresh_token".to_cowstr() 401 + SmolStr::new_static("authorization_code"), 402 + SmolStr::new_static("refresh_token") 359 403 ]), 360 - response_types: vec!["code".to_cowstr()], 404 + response_types: vec![SmolStr::new_static("code")], 361 405 token_endpoint_auth_method: Some(AuthMethod::None.into()), 362 406 dpop_bound_access_tokens: Some(true), 363 407 jwks_uri: None, ··· 375 419 fn test_localhost_client_metadata_custom() { 376 420 assert_eq!( 377 421 atproto_client_metadata( 378 - AtprotoClientMetadata::new_localhost( 422 + &AtprotoClientMetadata::new_localhost( 379 423 Some(vec![ 380 424 Uri::parse("http://127.0.0.1/callback".to_string()).unwrap(), 381 425 Uri::parse("http://[::1]/callback".to_string()).unwrap(), ··· 390 434 ) 391 435 .expect("failed to convert metadata"), 392 436 OAuthClientMetadata { 393 - client_id: CowStr::new_static( 437 + client_id: SmolStr::new_static( 394 438 "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F%5B%3A%3A1%5D%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric" 395 439 ), 396 440 client_uri: None, 397 441 redirect_uris: vec![ 398 - CowStr::new_static("http://127.0.0.1/callback"), 399 - CowStr::new_static("http://[::1]/callback"), 442 + SmolStr::new_static("http://127.0.0.1/callback"), 443 + SmolStr::new_static("http://[::1]/callback"), 400 444 ], 401 - scope: Some(CowStr::new_static( 445 + scope: Some(SmolStr::new_static( 402 446 "account:email atproto transition:generic" 403 447 )), 404 - application_type: Some(CowStr::new_static("native")), 448 + application_type: Some(SmolStr::new_static("native")), 405 449 grant_types: Some(vec![ 406 - "authorization_code".to_cowstr(), 407 - "refresh_token".to_cowstr() 450 + SmolStr::new_static("authorization_code"), 451 + SmolStr::new_static("refresh_token") 408 452 ]), 409 - response_types: vec!["code".to_cowstr()], 453 + response_types: vec![SmolStr::new_static("code")], 410 454 token_endpoint_auth_method: Some(AuthMethod::None.into()), 411 455 dpop_bound_access_tokens: Some(true), 412 456 jwks_uri: None, ··· 425 469 // Invalid inputs are coerced to http://localhost rather than failing 426 470 { 427 471 let out = atproto_client_metadata( 428 - AtprotoClientMetadata::new_localhost( 472 + &AtprotoClientMetadata::new_localhost( 429 473 Some(vec![Uri::parse("https://127.0.0.1".to_string()).unwrap()]), 430 474 None, 431 475 ), ··· 435 479 assert_eq!( 436 480 out, 437 481 OAuthClientMetadata { 438 - client_id: CowStr::new_static( 482 + client_id: SmolStr::new_static( 439 483 "http://localhost/?redirect_uri=https%3A%2F%2F127.0.0.1" 440 484 ), 441 - application_type: Some(CowStr::new_static("native")), 485 + application_type: Some(SmolStr::new_static("native")), 442 486 client_uri: None, 443 - redirect_uris: vec![CowStr::new_static("https://127.0.0.1")], 444 - scope: Some(CowStr::new_static("atproto")), 487 + redirect_uris: vec![SmolStr::new_static("https://127.0.0.1")], 488 + scope: Some(SmolStr::new_static("atproto")), 445 489 grant_types: Some(vec![ 446 - "authorization_code".to_cowstr(), 447 - "refresh_token".to_cowstr() 490 + SmolStr::new_static("authorization_code"), 491 + SmolStr::new_static("refresh_token") 448 492 ]), 449 - response_types: vec!["code".to_cowstr()], 493 + response_types: vec![SmolStr::new_static("code")], 450 494 token_endpoint_auth_method: Some(AuthMethod::None.into()), 451 495 dpop_bound_access_tokens: Some(true), 452 496 jwks_uri: None, ··· 461 505 } 462 506 { 463 507 let out = atproto_client_metadata( 464 - AtprotoClientMetadata::new_localhost( 508 + &AtprotoClientMetadata::new_localhost( 465 509 Some(vec![ 466 510 Uri::parse("http://localhost:8000".to_string()).unwrap(), 467 511 ]), ··· 473 517 assert_eq!( 474 518 out, 475 519 OAuthClientMetadata { 476 - client_id: CowStr::new_static( 520 + client_id: SmolStr::new_static( 477 521 "http://localhost/?redirect_uri=http%3A%2F%2Flocalhost%3A8000" 478 522 ), 479 523 client_uri: None, 480 - redirect_uris: vec![CowStr::new_static("http://localhost:8000")], 481 - scope: Some(CowStr::new_static("atproto")), 524 + redirect_uris: vec![SmolStr::new_static("http://localhost:8000")], 525 + scope: Some(SmolStr::new_static("atproto")), 482 526 grant_types: Some(vec![ 483 - "authorization_code".to_cowstr(), 484 - "refresh_token".to_cowstr() 527 + SmolStr::new_static("authorization_code"), 528 + SmolStr::new_static("refresh_token") 485 529 ]), 486 - application_type: Some(CowStr::new_static("native")), 487 - response_types: vec!["code".to_cowstr()], 530 + application_type: Some(SmolStr::new_static("native")), 531 + response_types: vec![SmolStr::new_static("code")], 488 532 token_endpoint_auth_method: Some(AuthMethod::None.into()), 489 533 dpop_bound_access_tokens: Some(true), 490 534 jwks_uri: None, ··· 499 543 } 500 544 { 501 545 let out = atproto_client_metadata( 502 - AtprotoClientMetadata::new_localhost( 546 + &AtprotoClientMetadata::new_localhost( 503 547 Some(vec![Uri::parse("http://192.168.0.0/".to_string()).unwrap()]), 504 548 None, 505 549 ), ··· 509 553 assert_eq!( 510 554 out, 511 555 OAuthClientMetadata { 512 - client_id: CowStr::new_static( 556 + client_id: SmolStr::new_static( 513 557 "http://localhost/?redirect_uri=http%3A%2F%2F192.168.0.0" 514 558 ), 515 559 client_uri: None, 516 - redirect_uris: vec![CowStr::new_static("http://192.168.0.0")], 517 - scope: Some(CowStr::new_static("atproto")), 560 + redirect_uris: vec![SmolStr::new_static("http://192.168.0.0/")], 561 + scope: Some(SmolStr::new_static("atproto")), 518 562 grant_types: Some(vec![ 519 - "authorization_code".to_cowstr(), 520 - "refresh_token".to_cowstr() 563 + SmolStr::new_static("authorization_code"), 564 + SmolStr::new_static("refresh_token") 521 565 ]), 522 - application_type: Some(CowStr::new_static("native")), 523 - response_types: vec!["code".to_cowstr()], 566 + application_type: Some(SmolStr::new_static("native")), 567 + response_types: vec![SmolStr::new_static("code")], 524 568 token_endpoint_auth_method: Some(AuthMethod::None.into()), 525 569 dpop_bound_access_tokens: Some(true), 526 570 jwks_uri: None, ··· 552 596 { 553 597 // Non-loopback clients without a keyset should fail (must provide JWKS) 554 598 let metadata = metadata.clone(); 555 - let err = atproto_client_metadata(metadata, &None); 599 + let err = atproto_client_metadata(&metadata, &None); 556 600 assert!(err.is_ok()); 557 601 } 558 602 { ··· 568 612 }]; 569 613 let keyset = Keyset::try_from(keys.clone()).expect("failed to create keyset"); 570 614 assert_eq!( 571 - atproto_client_metadata(metadata, &Some(keyset.clone())) 615 + atproto_client_metadata(&metadata, &Some(keyset.clone())) 572 616 .expect("failed to convert metadata"), 573 617 OAuthClientMetadata { 574 - client_id: CowStr::new_static("https://example.com/client_metadata.json"), 575 - client_uri: Some(CowStr::new_static("https://example.com")), 576 - redirect_uris: vec![CowStr::new_static("https://example.com/callback")], 577 - application_type: Some(CowStr::new_static("web")), 578 - scope: Some(CowStr::new_static("atproto")), 579 - grant_types: Some(vec![CowStr::new_static("authorization_code")]), 618 + client_id: SmolStr::new_static("https://example.com/client_metadata.json"), 619 + client_uri: Some(SmolStr::new_static("https://example.com")), 620 + redirect_uris: vec![SmolStr::new_static("https://example.com/callback")], 621 + application_type: Some(SmolStr::new_static("web")), 622 + scope: Some(SmolStr::new_static("atproto")), 623 + grant_types: Some(vec![SmolStr::new_static("authorization_code")]), 580 624 token_endpoint_auth_method: Some(AuthMethod::PrivateKeyJwt.into()), 581 625 dpop_bound_access_tokens: Some(true), 582 - response_types: vec!["code".to_cowstr()], 626 + response_types: vec![SmolStr::new_static("code")], 583 627 jwks_uri: None, 584 628 jwks: Some(keyset.public_jwks()), 585 - token_endpoint_auth_signing_alg: Some(CowStr::new_static("ES256")), 629 + token_endpoint_auth_signing_alg: Some(SmolStr::new_static("ES256")), 586 630 client_name: None, 587 631 logo_uri: None, 588 632 tos_uri: None,
+28 -36
crates/jacquard-oauth/src/authstore.rs
··· 3 3 4 4 use dashmap::DashMap; 5 5 use jacquard_common::{ 6 - IntoStatic, 6 + bos::BosStr, 7 7 session::{SessionStore, SessionStoreError}, 8 8 types::did::Did, 9 9 }; 10 - use smol_str::{SmolStr, ToSmolStr, format_smolstr}; 10 + use smol_str::{SmolStr, format_smolstr}; 11 11 12 12 use crate::session::{AuthRequestData, ClientSessionData}; 13 13 ··· 20 20 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 21 21 pub trait ClientAuthStore { 22 22 /// Retrieve an active session for the given DID and session identifier, if one exists. 23 - fn get_session( 23 + fn get_session<D: BosStr + Send + Sync>( 24 24 &self, 25 - did: &Did<'_>, 25 + did: &Did<D>, 26 26 session_id: &str, 27 - ) -> impl Future<Output = Result<Option<ClientSessionData<'_>>, SessionStoreError>>; 27 + ) -> impl Future<Output = Result<Option<ClientSessionData>, SessionStoreError>>; 28 28 29 29 /// Insert or update a session, replacing any existing entry for the same DID and session ID. 30 30 fn upsert_session( 31 31 &self, 32 - session: ClientSessionData<'_>, 32 + session: ClientSessionData, 33 33 ) -> impl Future<Output = Result<(), SessionStoreError>>; 34 34 35 35 /// Delete the session for the given DID and session identifier. 36 - fn delete_session( 36 + fn delete_session<D: BosStr + Send + Sync>( 37 37 &self, 38 - did: &Did<'_>, 38 + did: &Did<D>, 39 39 session_id: &str, 40 40 ) -> impl Future<Output = Result<(), SessionStoreError>>; 41 41 ··· 43 43 fn get_auth_req_info( 44 44 &self, 45 45 state: &str, 46 - ) -> impl Future<Output = Result<Option<AuthRequestData<'_>>, SessionStoreError>>; 46 + ) -> impl Future<Output = Result<Option<AuthRequestData>, SessionStoreError>>; 47 47 48 48 /// Persist authorization request data so it can be retrieved after the OAuth redirect. 49 49 fn save_auth_req_info( 50 50 &self, 51 - auth_req_info: &AuthRequestData<'_>, 51 + auth_req_info: &AuthRequestData, 52 52 ) -> impl Future<Output = Result<(), SessionStoreError>>; 53 53 54 54 /// Remove authorization request data after the callback has been handled. ··· 61 61 /// An in-memory implementation of [`ClientAuthStore`], suitable for testing and single-process 62 62 /// deployments where session persistence across restarts is not required. 63 63 pub struct MemoryAuthStore { 64 - sessions: DashMap<SmolStr, ClientSessionData<'static>>, 65 - auth_reqs: DashMap<SmolStr, AuthRequestData<'static>>, 64 + sessions: DashMap<SmolStr, ClientSessionData>, 65 + auth_reqs: DashMap<SmolStr, AuthRequestData>, 66 66 } 67 67 68 68 impl MemoryAuthStore { ··· 76 76 } 77 77 78 78 impl ClientAuthStore for MemoryAuthStore { 79 - async fn get_session( 79 + async fn get_session<D: BosStr + Send + Sync>( 80 80 &self, 81 - did: &Did<'_>, 81 + did: &Did<D>, 82 82 session_id: &str, 83 - ) -> Result<Option<ClientSessionData<'_>>, SessionStoreError> { 83 + ) -> Result<Option<ClientSessionData>, SessionStoreError> { 84 84 let key = format_smolstr!("{}_{}", did, session_id); 85 85 Ok(self.sessions.get(&key).map(|v| v.clone())) 86 86 } 87 87 88 - async fn upsert_session( 89 - &self, 90 - session: ClientSessionData<'_>, 91 - ) -> Result<(), SessionStoreError> { 88 + async fn upsert_session(&self, session: ClientSessionData) -> Result<(), SessionStoreError> { 92 89 let key = format_smolstr!("{}_{}", session.account_did, session.session_id); 93 - self.sessions.insert(key, session.into_static()); 90 + self.sessions.insert(key, session); 94 91 Ok(()) 95 92 } 96 93 97 - async fn delete_session( 94 + async fn delete_session<D: BosStr + Send + Sync>( 98 95 &self, 99 - did: &Did<'_>, 96 + did: &Did<D>, 100 97 session_id: &str, 101 98 ) -> Result<(), SessionStoreError> { 102 99 let key = format_smolstr!("{}_{}", did, session_id); ··· 107 104 async fn get_auth_req_info( 108 105 &self, 109 106 state: &str, 110 - ) -> Result<Option<AuthRequestData<'_>>, SessionStoreError> { 107 + ) -> Result<Option<AuthRequestData>, SessionStoreError> { 111 108 Ok(self.auth_reqs.get(state).map(|v| v.clone())) 112 109 } 113 110 114 111 async fn save_auth_req_info( 115 112 &self, 116 - auth_req_info: &AuthRequestData<'_>, 113 + auth_req_info: &AuthRequestData, 117 114 ) -> Result<(), SessionStoreError> { 118 - self.auth_reqs.insert( 119 - auth_req_info.state.clone().to_smolstr(), 120 - auth_req_info.clone().into_static(), 121 - ); 115 + self.auth_reqs 116 + .insert(auth_req_info.state.clone(), auth_req_info.clone()); 122 117 Ok(()) 123 118 } 124 119 ··· 128 123 } 129 124 } 130 125 131 - impl<T: ClientAuthStore + Send + Sync> 132 - SessionStore<(Did<'static>, SmolStr), ClientSessionData<'static>> for Arc<T> 133 - { 126 + impl<T: ClientAuthStore + Send + Sync> SessionStore<(Did, SmolStr), ClientSessionData> for Arc<T> { 134 127 /// Get the current session if present. 135 - async fn get(&self, key: &(Did<'static>, SmolStr)) -> Option<ClientSessionData<'static>> { 128 + async fn get(&self, key: &(Did, SmolStr)) -> Option<ClientSessionData> { 136 129 let (did, session_id) = key; 137 130 self.as_ref() 138 131 .get_session(did, session_id) 139 132 .await 140 133 .ok() 141 134 .flatten() 142 - .into_static() 143 135 } 144 136 /// Persist the given session. 145 137 async fn set( 146 138 &self, 147 - _key: (Did<'static>, SmolStr), 148 - session: ClientSessionData<'static>, 139 + _key: (Did, SmolStr), 140 + session: ClientSessionData, 149 141 ) -> Result<(), SessionStoreError> { 150 142 self.as_ref().upsert_session(session).await 151 143 } 152 144 /// Delete the given session. 153 - async fn del(&self, key: &(Did<'static>, SmolStr)) -> Result<(), SessionStoreError> { 145 + async fn del(&self, key: &(Did, SmolStr)) -> Result<(), SessionStoreError> { 154 146 let (did, session_id) = key; 155 147 self.as_ref().delete_session(did, session_id).await 156 148 }
+102 -79
crates/jacquard-oauth/src/client.rs
··· 11 11 }; 12 12 use jacquard_common::{ 13 13 AuthorizationToken, CowStr, IntoStatic, 14 - cowstr::ToCowStr, 14 + bos::BosStr, 15 15 deps::fluent_uri::Uri, 16 16 error::{AuthError, ClientError, XrpcResult}, 17 17 http_client::HttpClient, 18 18 types::{did::Did, string::Handle}, 19 19 xrpc::{ 20 - CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, 20 + CallOptions, Response, XrpcClient, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, 21 21 build_http_request, process_response, 22 22 }, 23 23 }; ··· 31 31 resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions}, 32 32 }; 33 33 use jose_jwk::JwkSet; 34 - use std::{future::Future, sync::Arc}; 34 + use smol_str::{SmolStr, ToSmolStr}; 35 + use std::{str::FromStr, sync::Arc}; 35 36 use tokio::sync::RwLock; 36 37 37 38 /// The top-level OAuth client responsible for driving the authorization flow. ··· 41 42 S: ClientAuthStore, 42 43 { 43 44 /// Shared session registry that mediates access to the backing auth store. 44 - pub registry: Arc<SessionRegistry<T, S>>, 45 + pub registry: Arc<SessionRegistry<T, S, SmolStr>>, 45 46 /// Default call options applied to every outgoing XRPC request. 46 47 pub options: RwLock<CallOptions<'static>>, 47 48 /// Override for the XRPC base URI; falls back to the public Bluesky AppView when `None`. ··· 52 53 53 54 impl<S: ClientAuthStore> OAuthClient<JacquardResolver, S> { 54 55 /// Create an `OAuthClient` using the default [`JacquardResolver`] for identity and metadata resolution. 55 - pub fn new(store: S, client_data: ClientData<'static>) -> Self { 56 + pub fn new(store: S, client_data: ClientData<SmolStr>) -> Self { 56 57 let client = JacquardResolver::default(); 57 58 Self::new_from_resolver(store, client, client_data) 58 59 } ··· 110 111 S: ClientAuthStore, 111 112 { 112 113 /// Create an OAuth client from an explicit resolver instance, taking ownership of both. 113 - pub fn new_from_resolver(store: S, client: T, client_data: ClientData<'static>) -> Self { 114 + pub fn new_from_resolver(store: S, client: T, client_data: ClientData<SmolStr>) -> Self { 114 115 // #[cfg(feature = "tracing")] 115 116 // tracing::info!( 116 117 // redirect_uris = ?client_data.config.redirect_uris, ··· 133 134 pub fn new_with_shared( 134 135 store: Arc<S>, 135 136 client: Arc<T>, 136 - client_data: ClientData<'static>, 137 + client_data: ClientData<SmolStr>, 137 138 ) -> Self { 138 139 let registry = Arc::new(SessionRegistry::new_shared( 139 140 store, ··· 172 173 /// 173 174 /// The caller is responsible for redirecting the user's browser to the returned URL. 174 175 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self, input), fields(input = input.as_ref())))] 175 - pub async fn start_auth( 176 + pub async fn start_auth<Str: BosStr>( 176 177 &self, 177 178 input: impl AsRef<str>, 178 - options: AuthorizeOptions<'_>, 179 - ) -> Result<String> { 179 + options: AuthorizeOptions<Str>, 180 + ) -> Result<String> 181 + where 182 + Str: FromStr + Ord + Clone + core::fmt::Debug, 183 + <Str as FromStr>::Err: core::fmt::Debug, 184 + { 180 185 let client_metadata = atproto_client_metadata( 181 - self.registry.client_data.config.clone(), 186 + &self.registry.client_data.config, 182 187 &self.registry.client_data.keyset, 183 188 )?; 184 189 let (server_metadata, identity) = self.client.resolve_oauth(input.as_ref()).await?; ··· 187 192 } else { 188 193 None 189 194 }; 190 - let metadata = OAuthMetadata { 195 + let mut metadata = OAuthMetadata { 191 196 server_metadata, 192 197 client_metadata, 193 198 keyset: self.registry.client_data.keyset.clone(), ··· 197 202 self.client.as_ref(), 198 203 login_hint, 199 204 options.prompt, 200 - &metadata, 201 - options.state, 205 + &mut metadata, 206 + options.state.map(|s| s.as_ref().to_smolstr()), 202 207 ) 203 208 .await?; 204 209 ··· 209 214 .await?; 210 215 211 216 #[derive(serde::Serialize)] 212 - struct Parameters<'s> { 213 - client_id: CowStr<'s>, 214 - request_uri: CowStr<'s>, 217 + struct Parameters { 218 + client_id: smol_str::SmolStr, 219 + request_uri: smol_str::SmolStr, 215 220 } 216 221 Ok(metadata.server_metadata.authorization_endpoint.to_string() 217 222 + "?" ··· 227 232 /// Validates the `state` and optional `iss` parameters, exchanges the authorization code for 228 233 /// tokens via the token endpoint, verifies the `sub` claim against the expected issuer, and 229 234 /// persists the resulting session. On success returns an [`OAuthSession`] ready for API calls. 230 - #[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip_all, fields(state = params.state.as_ref().map(|s| s.as_ref()))))] 231 - pub async fn callback(&self, params: CallbackParams<'_>) -> Result<OAuthSession<T, S>> { 235 + #[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip_all, fields(state = params.state.as_ref().map(|s| s.as_str()))))] 236 + pub async fn callback(&self, params: CallbackParams) -> Result<OAuthSession<T, S>> { 232 237 let Some(state_key) = params.state else { 233 238 return Err(CallbackError::MissingState.into()); 234 239 }; 235 240 236 - let Some(auth_req_info) = self.registry.store.get_auth_req_info(&state_key).await? else { 241 + let Some(auth_req_info) = self 242 + .registry 243 + .store 244 + .get_auth_req_info(state_key.as_str()) 245 + .await? 246 + else { 237 247 return Err(CallbackError::MissingState.into()); 238 248 }; 239 249 240 - self.registry.store.delete_auth_req_info(&state_key).await?; 250 + self.registry 251 + .store 252 + .delete_auth_req_info(state_key.as_str()) 253 + .await?; 241 254 242 255 let metadata = self 243 256 .client 244 - .get_authorization_server_metadata(&auth_req_info.authserver_url.to_cowstr()) 257 + .get_authorization_server_metadata(auth_req_info.authserver_url.as_str()) 245 258 .await?; 246 259 247 260 if let Some(iss) = params.iss { ··· 258 271 let metadata = OAuthMetadata { 259 272 server_metadata: metadata, 260 273 client_metadata: atproto_client_metadata( 261 - self.registry.client_data.config.clone(), 274 + &self.registry.client_data.config, 262 275 &self.registry.client_data.keyset, 263 276 )?, 264 277 keyset: self.registry.client_data.keyset.clone(), ··· 268 281 match exchange_code( 269 282 self.client.as_ref(), 270 283 &mut auth_req_info.dpop_data.clone(), 271 - &params.code, 272 - &auth_req_info.pkce_verifier, 284 + params.code.as_str(), 285 + auth_req_info.pkce_verifier.as_str(), 273 286 &metadata, 274 287 ) 275 288 .await 276 289 { 277 290 Ok(token_set) => { 278 291 let scopes = if let Some(scope) = &token_set.scope { 279 - Scope::parse_multiple_reduced(&scope) 292 + Scope::<SmolStr>::parse_multiple_reduced(scope.as_str()) 280 293 .expect("Failed to parse scopes") 281 294 .into_static() 282 295 } else { ··· 285 298 let client_data = ClientSessionData { 286 299 account_did: token_set.sub.clone(), 287 300 session_id: auth_req_info.state, 288 - host_url: Uri::parse(token_set.aud.as_ref())?.to_owned(), 289 - authserver_url: auth_req_info.authserver_url.to_cowstr(), 301 + host_url: Uri::parse(token_set.aud.as_str())?.to_owned(), 302 + authserver_url: auth_req_info.authserver_url, 290 303 authserver_token_endpoint: auth_req_info.authserver_token_endpoint, 291 304 authserver_revocation_endpoint: auth_req_info.authserver_revocation_endpoint, 292 305 scopes, 293 306 dpop_data: DpopClientData { 294 307 dpop_key: auth_req_info.dpop_data.dpop_key.clone(), 295 - dpop_authserver_nonce: authserver_nonce.unwrap_or(CowStr::default()), 308 + dpop_authserver_nonce: authserver_nonce.unwrap_or_default(), 296 309 dpop_host_nonce: auth_req_info 297 310 .dpop_data 298 311 .dpop_authserver_nonce 299 - .unwrap_or(CowStr::default()), 312 + .unwrap_or_default(), 300 313 }, 301 314 token_set, 302 315 }; ··· 307 320 } 308 321 } 309 322 310 - async fn create_session(&self, data: ClientSessionData<'_>) -> Result<OAuthSession<T, S>> { 323 + async fn create_session(&self, data: ClientSessionData) -> Result<OAuthSession<T, S>> { 311 324 self.registry.set(data.clone()).await?; 312 325 Ok(OAuthSession::new( 313 326 self.registry.clone(), ··· 317 330 } 318 331 319 332 /// Restore a previously created session from the backing store, refreshing tokens if needed. 320 - pub async fn restore(&self, did: &Did<'_>, session_id: &str) -> Result<OAuthSession<T, S>> { 333 + pub async fn restore( 334 + &self, 335 + did: &Did<impl BosStr + Send + Sync>, 336 + session_id: &str, 337 + ) -> Result<OAuthSession<T, S>> { 321 338 self.create_session(self.registry.get(did, session_id, true).await?) 322 339 .await 323 340 } ··· 327 344 /// Note: this removes the session from local storage but does **not** call the authorization 328 345 /// server's revocation endpoint. To also invalidate the token server-side, prefer 329 346 /// [`OAuthSession::logout`], which calls `revoke` on the token before deleting the session. 330 - pub async fn revoke(&self, did: &Did<'_>, session_id: &str) -> Result<()> { 347 + pub async fn revoke( 348 + &self, 349 + did: &Did<impl BosStr + Send + Sync>, 350 + session_id: &str, 351 + ) -> Result<()> { 331 352 Ok(self.registry.del(did, session_id).await?) 332 353 } 333 354 } ··· 356 377 self.client.options() 357 378 } 358 379 359 - async fn resolve_handle( 380 + async fn resolve_handle<Str: BosStr + Sync>( 360 381 &self, 361 - handle: &Handle<'_>, 362 - ) -> jacquard_identity::resolver::Result<Did<'static>> { 382 + handle: &Handle<Str>, 383 + ) -> jacquard_identity::resolver::Result<Did> { 363 384 self.client.resolve_handle(handle).await 364 385 } 365 386 366 - async fn resolve_did_doc( 387 + async fn resolve_did_doc<Str: BosStr + Sync>( 367 388 &self, 368 - did: &Did<'_>, 389 + did: &Did<Str>, 369 390 ) -> jacquard_identity::resolver::Result<DidDocResponse> { 370 391 self.client.resolve_did_doc(did).await 371 392 } ··· 401 422 402 423 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> 403 424 where 404 - R: XrpcRequest + Send + Sync, 425 + R: XrpcRequest + Send + Sync + serde::Serialize, 405 426 <R as XrpcRequest>::Response: Send + Sync, 406 427 { 407 428 let opts = self.options.read().await.clone(); ··· 414 435 opts: CallOptions<'_>, 415 436 ) -> XrpcResult<XrpcResponse<R>> 416 437 where 417 - R: XrpcRequest + Send + Sync, 438 + R: XrpcRequest + Send + Sync + serde::Serialize, 418 439 <R as XrpcRequest>::Response: Send + Sync, 419 440 { 420 441 let base_uri = self.base_uri().await; 421 - self.client 422 - .xrpc(base_uri) 423 - .with_options(opts.clone()) 424 - .send(&request) 442 + let http_request = build_http_request(&base_uri.borrow(), &request, &opts)?; 443 + let http_response = self 444 + .client 445 + .send_http(http_request) 425 446 .await 447 + .map_err(|e| ClientError::transport(e).for_nsid(R::NSID))?; 448 + process_response(http_response) 426 449 } 427 450 } 428 451 ··· 439 462 S: ClientAuthStore, 440 463 { 441 464 /// Shared registry used to persist and retrieve session data across refresh operations. 442 - pub registry: Arc<SessionRegistry<T, S>>, 465 + pub registry: Arc<SessionRegistry<T, S, SmolStr>>, 443 466 /// Underlying HTTP/identity/OAuth resolver shared with the parent `OAuthClient`. 444 467 pub client: Arc<T>, 445 468 /// Optional WebSocket client; `()` when WebSocket support is not required. 446 469 pub ws_client: W, 447 470 /// Mutable session data including DPoP key, nonces, and token set. 448 - pub data: RwLock<ClientSessionData<'static>>, 471 + pub data: RwLock<ClientSessionData>, 449 472 /// Default call options applied to every outgoing XRPC request from this session. 450 473 pub options: RwLock<CallOptions<'static>>, 451 474 } ··· 460 483 /// This is the standard constructor used by [`OAuthClient::callback`] and 461 484 /// [`OAuthClient::restore`]. For WebSocket support use [`OAuthSession::new_with_ws`]. 462 485 pub fn new( 463 - registry: Arc<SessionRegistry<T, S>>, 486 + registry: Arc<SessionRegistry<T, S, SmolStr>>, 464 487 client: Arc<T>, 465 - data: ClientSessionData<'static>, 488 + data: ClientSessionData, 466 489 ) -> Self { 467 490 Self { 468 491 registry, ··· 485 508 /// to standard XRPC calls. The `ws_client` is exposed via [`OAuthSession::ws_client`] and 486 509 /// is used by the `WebSocketClient` impl when the `websocket` feature is enabled. 487 510 pub fn new_with_ws( 488 - registry: Arc<SessionRegistry<T, S>>, 511 + registry: Arc<SessionRegistry<T, S, SmolStr>>, 489 512 client: Arc<T>, 490 513 ws_client: W, 491 - data: ClientSessionData<'static>, 514 + data: ClientSessionData, 492 515 ) -> Self { 493 516 Self { 494 517 registry, ··· 527 550 /// 528 551 /// The session ID is the random `state` token generated during the PAR flow and can 529 552 /// be used together with the DID to restore the session via [`OAuthClient::restore`]. 530 - pub async fn session_info(&self) -> (Did<'_>, CowStr<'_>) { 553 + pub async fn session_info(&self) -> (Did, smol_str::SmolStr) { 531 554 let data = self.data.read().await; 532 555 (data.account_did.clone(), data.session_id.clone()) 533 556 } ··· 541 564 /// 542 565 /// The token may be stale if it has expired; use [`OAuthSession::refresh`] or 543 566 /// rely on the automatic refresh performed by `send_with_opts` to obtain a fresh one. 544 - pub async fn access_token(&self) -> AuthorizationToken<'_> { 545 - AuthorizationToken::Dpop(self.data.read().await.token_set.access_token.clone()) 567 + pub async fn access_token(&self) -> AuthorizationToken<'static> { 568 + AuthorizationToken::Dpop(CowStr::Owned( 569 + self.data.read().await.token_set.access_token.clone(), 570 + )) 546 571 } 547 572 548 573 /// Return the current refresh token for this session, if one is present. 549 574 /// 550 575 /// Not all authorization servers issue refresh tokens. When `None` is returned, 551 576 /// the session cannot be silently renewed and the user must re-authenticate. 552 - pub async fn refresh_token(&self) -> Option<AuthorizationToken<'_>> { 577 + pub async fn refresh_token(&self) -> Option<AuthorizationToken<'static>> { 553 578 self.data 554 579 .read() 555 580 .await 556 581 .token_set 557 582 .refresh_token 558 - .as_ref() 559 - .map(|t| AuthorizationToken::Dpop(t.clone())) 583 + .clone() 584 + .map(|t| AuthorizationToken::Dpop(CowStr::Owned(t))) 560 585 } 561 586 562 587 /// Derive an unauthenticated [`OAuthClient`] that shares the same registry and resolver. ··· 628 653 /// The actual token exchange is serialized per `(DID, session_id)` pair via a `Mutex` inside 629 654 /// the registry, so concurrent refresh attempts will not result in duplicate token exchanges. 630 655 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] 631 - pub async fn refresh(&self) -> Result<AuthorizationToken<'_>> { 656 + pub async fn refresh(&self) -> Result<AuthorizationToken<'static>> { 632 657 // Read identifiers without holding the lock across await 633 658 let (did, sid) = { 634 659 let data = self.data.read().await; 635 660 (data.account_did.clone(), data.session_id.clone()) 636 661 }; 637 662 let refreshed = self.registry.as_ref().get(&did, &sid, true).await?; 638 - let token = AuthorizationToken::Dpop(refreshed.token_set.access_token.clone()); 663 + let token = 664 + AuthorizationToken::Dpop(CowStr::Owned(refreshed.token_set.access_token.clone())); 639 665 // Write back updated session 640 666 *self.data.write().await = refreshed.clone().into_static(); 641 667 // Store in the registry ··· 687 713 688 714 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> 689 715 where 690 - R: XrpcRequest + Send + Sync, 716 + R: XrpcRequest + Send + Sync + serde::Serialize, 691 717 <R as XrpcRequest>::Response: Send + Sync, 692 718 { 693 719 let opts = self.options.read().await.clone(); ··· 700 726 mut opts: CallOptions<'_>, 701 727 ) -> XrpcResult<XrpcResponse<R>> 702 728 where 703 - R: XrpcRequest + Send + Sync, 729 + R: XrpcRequest + Send + Sync + serde::Serialize, 704 730 <R as XrpcRequest>::Response: Send + Sync, 705 731 { 706 732 let base_uri = self.base_uri().await; ··· 711 737 let http_response = self 712 738 .client 713 739 .dpop_call(&mut dpop) 714 - .send(build_http_request(&base_uri, &request, &opts)?) 740 + .send(build_http_request(&base_uri.borrow(), &request, &opts)?) 715 741 .await 716 742 .map_err(|e| ClientError::from(e).for_nsid(R::NSID))?; 717 743 let resp = process_response(http_response); ··· 741 767 let http_response = self 742 768 .client 743 769 .dpop_call(&mut dpop) 744 - .send(build_http_request(&base_uri, &request, &opts)?) 770 + .send(build_http_request(&base_uri.borrow(), &request, &opts)?) 745 771 .await 746 772 .map_err(|e| { 747 773 ClientError::from(e) ··· 832 858 request: R, 833 859 ) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError> 834 860 where 835 - R: XrpcRequest + Send + Sync, 861 + R: XrpcRequest + Send + Sync + serde::Serialize, 836 862 <R as XrpcRequest>::Response: Send + Sync, 837 863 { 838 864 use jacquard_common::StreamError; ··· 840 866 let base_uri = <Self as XrpcClient>::base_uri(self).await; 841 867 let mut opts = self.options.read().await.clone(); 842 868 opts.auth = Some(self.access_token().await); 843 - let http_request = build_http_request(&base_uri, &request, &opts) 869 + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) 844 870 .map_err(|e| StreamError::protocol(e.to_string()))?; 845 871 let guard = self.data.read().await; 846 872 let mut dpop = guard.dpop_data.clone(); ··· 860 886 .await 861 887 .map_err(|e| StreamError::transport(e))?, 862 888 ); 863 - let http_request = build_http_request(&base_uri, &request, &opts) 889 + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) 864 890 .map_err(|e| StreamError::protocol(e.to_string()))?; 865 891 let guard = self.data.read().await; 866 892 let mut dpop = guard.dpop_data.clone(); ··· 976 1002 .is_ok_and(|s| s.starts_with("DPoP ") && s.contains("error=\"invalid_token\"")), 977 1003 _ => false, 978 1004 }, 979 - Ok(resp) => match resp.parse() { 980 - Err(XrpcError::Auth(AuthError::InvalidToken)) => true, 981 - _ => false, 982 - }, 1005 + Ok(_) => false, 983 1006 } 984 1007 } 985 1008 ··· 993 1016 self.client.options() 994 1017 } 995 1018 996 - fn resolve_handle( 1019 + async fn resolve_handle<Str: BosStr + Sync>( 997 1020 &self, 998 - handle: &Handle<'_>, 999 - ) -> impl Future<Output = std::result::Result<Did<'static>, IdentityError>> { 1000 - async { self.client.resolve_handle(handle).await } 1021 + handle: &Handle<Str>, 1022 + ) -> std::result::Result<Did, IdentityError> { 1023 + self.client.resolve_handle(handle).await 1001 1024 } 1002 1025 1003 - fn resolve_did_doc( 1026 + async fn resolve_did_doc<Str: BosStr + Sync>( 1004 1027 &self, 1005 - did: &Did<'_>, 1006 - ) -> impl Future<Output = std::result::Result<DidDocResponse, IdentityError>> { 1007 - async { self.client.resolve_did_doc(did).await } 1028 + did: &Did<Str>, 1029 + ) -> std::result::Result<DidDocResponse, IdentityError> { 1030 + self.client.resolve_did_doc(did).await 1008 1031 } 1009 1032 } 1010 1033 ··· 1061 1084 params: &Sub, 1062 1085 ) -> std::result::Result<jacquard_common::xrpc::SubscriptionStream<Sub::Stream>, Self::Error> 1063 1086 where 1064 - Sub: XrpcSubscription + Send + Sync, 1087 + Sub: XrpcSubscription + Send + Sync + serde::Serialize, 1065 1088 { 1066 1089 let opts = self.subscription_opts().await; 1067 1090 self.subscribe_with_opts(params, opts).await ··· 1073 1096 opts: jacquard_common::xrpc::SubscriptionOptions<'_>, 1074 1097 ) -> std::result::Result<jacquard_common::xrpc::SubscriptionStream<Sub::Stream>, Self::Error> 1075 1098 where 1076 - Sub: XrpcSubscription + Send + Sync, 1099 + Sub: XrpcSubscription + Send + Sync + serde::Serialize, 1077 1100 { 1078 1101 use jacquard_common::xrpc::SubscriptionExt; 1079 1102 let base = self.base_uri().await;
+104 -85
crates/jacquard-oauth/src/dpop.rs
··· 5 5 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 6 6 use chrono::Utc; 7 7 use http::{Request, Response, header::InvalidHeaderValue}; 8 - use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr, http_client::HttpClient}; 8 + use jacquard_common::http_client::HttpClient; 9 9 use jacquard_identity::JacquardResolver; 10 10 use jose_jwa::{Algorithm, Signing}; 11 11 use jose_jwk::{Jwk, Key, crypto}; 12 12 use p256::ecdsa::SigningKey; 13 13 use rand::{RngCore, SeedableRng}; 14 14 use sha2::Digest; 15 - use smol_str::SmolStr; 15 + use smol_str::{SmolStr, ToSmolStr}; 16 16 17 17 use crate::{ 18 18 jose::{ ··· 461 461 } 462 462 } 463 463 464 - /// Extract authorization hash from request headers 465 - fn extract_ath(headers: &http::HeaderMap) -> Option<CowStr<'static>> { 464 + /// Extract authorization hash from request headers. 465 + fn extract_ath(headers: &http::HeaderMap) -> Option<SmolStr> { 466 466 headers 467 467 .get("authorization") 468 468 .filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP "))) 469 469 .map(|auth| { 470 - URL_SAFE_NO_PAD 471 - .encode(sha2::Sha256::digest(&auth.as_bytes()[5..])) 472 - .into() 470 + SmolStr::new(URL_SAFE_NO_PAD.encode(sha2::Sha256::digest(&auth.as_bytes()[5..]))) 473 471 }) 474 472 } 475 473 476 - /// Get nonce from data source based on target 477 - fn get_nonce<N: DpopDataSource>(data_source: &N, is_to_auth_server: bool) -> Option<CowStr<'_>> { 474 + /// Get nonce from data source based on target, returning an owned copy of the current nonce. 475 + /// 476 + /// Returning an owned `SmolStr` rather than a borrow ensures callers can later take a 477 + /// mutable reference to the data source (e.g., to call `store_nonce`) without violating 478 + /// Rust's aliasing rules. 479 + fn get_nonce<N: DpopDataSource>(data_source: &N, is_to_auth_server: bool) -> Option<SmolStr> { 478 480 if is_to_auth_server { 479 - data_source.authserver_nonce() 481 + data_source.authserver_nonce().map(SmolStr::new) 480 482 } else { 481 - data_source.host_nonce() 483 + data_source.host_nonce().map(SmolStr::new) 482 484 } 483 485 } 484 486 485 - /// Store nonce in data source based on target 486 - fn store_nonce<N: DpopDataSource>( 487 - data_source: &mut N, 488 - is_to_auth_server: bool, 489 - nonce: CowStr<'static>, 490 - ) { 487 + /// Store nonce in data source based on target. 488 + fn store_nonce<N: DpopDataSource>(data_source: &mut N, is_to_auth_server: bool, nonce: SmolStr) { 491 489 if is_to_auth_server { 492 490 data_source.set_authserver_nonce(nonce); 493 491 } else { ··· 515 513 } else { 516 514 DpopTarget::ResourceServer 517 515 }; 518 - let uri = request.uri().clone(); 519 - let method = request.method().to_cowstr().into_static(); 520 - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); 521 - let uri = uri.to_cowstr(); 516 + let method = request.method().to_smolstr(); 517 + let uri = request.uri().to_smolstr(); 522 518 let ath = extract_ath(request.headers()); 523 519 524 520 let init_nonce = get_nonce(data_source, is_to_auth_server); 525 521 let init_proof = build_dpop_proof( 526 522 data_source.key(), 527 - method.clone(), 528 - uri.clone(), 529 - init_nonce.clone(), 530 - ath.clone(), 523 + &method, 524 + &uri, 525 + init_nonce.as_deref(), 526 + ath.as_deref(), 531 527 )?; 532 - request.headers_mut().insert("DPoP", init_proof.parse()?); 528 + request 529 + .headers_mut() 530 + .insert("DPoP", init_proof.as_str().parse()?); 533 531 let response = client 534 532 .send_http(request.clone()) 535 533 .await 536 - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; 534 + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; 537 535 538 - let next_nonce = response 536 + let next_nonce: Option<SmolStr> = response 539 537 .headers() 540 538 .get("dpop-nonce") 541 539 .and_then(|v| v.to_str().ok()) 542 - .map(|c| CowStr::copy_from_str(c)); 540 + .map(SmolStr::new); 543 541 match &next_nonce { 544 - Some(s) if next_nonce != init_nonce => { 545 - store_nonce(data_source, is_to_auth_server, s.clone()); 542 + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { 543 + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); 546 544 } 547 545 _ => { 548 546 return Ok(response); ··· 552 550 if !is_use_dpop_nonce_error(is_to_auth_server, &response) { 553 551 return Ok(response); 554 552 } 555 - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; 556 - request.headers_mut().insert("DPoP", next_proof.parse()?); 553 + let next_proof = build_dpop_proof( 554 + data_source.key(), 555 + &method, 556 + &uri, 557 + next_nonce.as_deref(), 558 + ath.as_deref(), 559 + )?; 560 + request 561 + .headers_mut() 562 + .insert("DPoP", next_proof.as_str().parse()?); 557 563 let response = client 558 564 .send_http(request) 559 565 .await 560 - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; 566 + .map_err(|e| DpopError::nonce_retry(target, uri.clone(), e))?; 561 567 Ok(response) 562 568 } 563 569 ··· 584 590 } else { 585 591 DpopTarget::ResourceServer 586 592 }; 587 - let uri = request.uri().clone(); 588 - let method = request.method().to_cowstr().into_static(); 589 - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); 590 - let uri = uri.to_cowstr(); 593 + let method = request.method().to_smolstr(); 594 + let uri = request.uri().to_smolstr(); 591 595 let ath = extract_ath(request.headers()); 592 596 593 597 let init_nonce = get_nonce(data_source, is_to_auth_server); 594 598 let init_proof = build_dpop_proof( 595 599 data_source.key(), 596 - method.clone(), 597 - uri.clone(), 598 - init_nonce.clone(), 599 - ath.clone(), 600 + &method, 601 + &uri, 602 + init_nonce.as_deref(), 603 + ath.as_deref(), 600 604 )?; 601 - request.headers_mut().insert("DPoP", init_proof.parse()?); 605 + request 606 + .headers_mut() 607 + .insert("DPoP", init_proof.as_str().parse()?); 602 608 let http_response = client 603 609 .send_http_streaming(request.clone()) 604 610 .await 605 - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; 611 + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; 606 612 607 613 let (parts, body) = http_response.into_parts(); 608 - let next_nonce = parts 614 + let next_nonce: Option<SmolStr> = parts 609 615 .headers 610 616 .get("DPoP-Nonce") 611 617 .and_then(|v| v.to_str().ok()) 612 - .map(|c| CowStr::from(c.to_string())); 618 + .map(SmolStr::new); 613 619 match &next_nonce { 614 - Some(s) if next_nonce != init_nonce => { 615 - store_nonce(data_source, is_to_auth_server, s.clone()); 620 + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { 621 + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); 616 622 } 617 623 _ => { 618 624 return Ok(StreamingResponse::new(parts, body)); 619 625 } 620 626 } 621 627 622 - // For streaming responses, we can't easily check the body for use_dpop_nonce error 623 - // We check status code + headers only 628 + // For streaming responses, we can't easily check the body for use_dpop_nonce error. 629 + // We check status code + headers only. 624 630 if !is_use_dpop_nonce_error_streaming(is_to_auth_server, parts.status, &parts.headers) { 625 631 return Ok(StreamingResponse::new(parts, body)); 626 632 } 627 633 628 - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; 629 - request.headers_mut().insert("DPoP", next_proof.parse()?); 634 + let next_proof = build_dpop_proof( 635 + data_source.key(), 636 + &method, 637 + &uri, 638 + next_nonce.as_deref(), 639 + ath.as_deref(), 640 + )?; 641 + request 642 + .headers_mut() 643 + .insert("DPoP", next_proof.as_str().parse()?); 630 644 let http_response = client 631 645 .send_http_streaming(request) 632 646 .await 633 - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; 647 + .map_err(|e| DpopError::nonce_retry(target, uri, e))?; 634 648 let (parts, body) = http_response.into_parts(); 635 649 Ok(StreamingResponse::new(parts, body)) 636 650 } ··· 658 672 } else { 659 673 DpopTarget::ResourceServer 660 674 }; 661 - let uri = parts.uri.clone(); 662 - let method = parts.method.to_cowstr().into_static(); 663 - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); 664 - let uri = uri.to_cowstr(); 675 + let method = parts.method.to_smolstr(); 676 + let uri = parts.uri.to_smolstr(); 665 677 let ath = extract_ath(&parts.headers); 666 678 667 679 let init_nonce = get_nonce(data_source, is_to_auth_server); 668 680 let init_proof = build_dpop_proof( 669 681 data_source.key(), 670 - method.clone(), 671 - uri.clone(), 672 - init_nonce.clone(), 673 - ath.clone(), 682 + &method, 683 + &uri, 684 + init_nonce.as_deref(), 685 + ath.as_deref(), 674 686 )?; 675 - parts.headers.insert("DPoP", init_proof.parse()?); 687 + parts.headers.insert("DPoP", init_proof.as_str().parse()?); 676 688 677 - // Clone the stream for potential retry 689 + // Clone the stream for potential retry. 678 690 let (body1, body2) = body.tee(); 679 691 680 692 let http_response = client 681 693 .send_http_bidirectional(parts.clone(), body1.into_inner()) 682 694 .await 683 - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; 695 + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; 684 696 685 697 let (resp_parts, resp_body) = http_response.into_parts(); 686 - let next_nonce = resp_parts 698 + let next_nonce: Option<SmolStr> = resp_parts 687 699 .headers 688 700 .get("DPoP-Nonce") 689 701 .and_then(|v| v.to_str().ok()) 690 - .map(|c| CowStr::from(c.to_string())); 702 + .map(SmolStr::new); 691 703 match &next_nonce { 692 - Some(s) if next_nonce != init_nonce => { 693 - store_nonce(data_source, is_to_auth_server, s.clone()); 704 + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { 705 + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); 694 706 } 695 707 _ => { 696 708 return Ok(StreamingResponse::new(resp_parts, resp_body)); 697 709 } 698 710 } 699 711 700 - // For streaming responses, we can't easily check the body for use_dpop_nonce error 701 - // We check status code + headers only 712 + // For streaming responses, we can't easily check the body for use_dpop_nonce error. 713 + // We check status code + headers only. 702 714 if !is_use_dpop_nonce_error_streaming(is_to_auth_server, resp_parts.status, &resp_parts.headers) 703 715 { 704 716 return Ok(StreamingResponse::new(resp_parts, resp_body)); 705 717 } 706 718 707 - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; 708 - parts.headers.insert("DPoP", next_proof.parse()?); 719 + let next_proof = build_dpop_proof( 720 + data_source.key(), 721 + &method, 722 + &uri, 723 + next_nonce.as_deref(), 724 + ath.as_deref(), 725 + )?; 726 + parts.headers.insert("DPoP", next_proof.as_str().parse()?); 709 727 let http_response = client 710 728 .send_http_bidirectional(parts, body2.into_inner()) 711 729 .await 712 - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; 730 + .map_err(|e| DpopError::nonce_retry(target, uri, e))?; 713 731 let (parts, body) = http_response.into_parts(); 714 732 Ok(StreamingResponse::new(parts, body)) 715 733 } ··· 760 778 } 761 779 762 780 #[inline] 763 - pub(crate) fn generate_jti() -> CowStr<'static> { 781 + pub(crate) fn generate_jti() -> SmolStr { 764 782 let mut rng = rand::rngs::SmallRng::from_entropy(); 765 783 let mut bytes = [0u8; 12]; 766 784 rng.fill_bytes(&mut bytes); ··· 769 787 770 788 /// Build a compact JWS (ES256) for DPoP with embedded public JWK. 771 789 #[inline] 772 - pub fn build_dpop_proof<'s>( 790 + pub fn build_dpop_proof( 773 791 key: &Key, 774 - method: CowStr<'s>, 775 - url: CowStr<'s>, 776 - nonce: Option<CowStr<'s>>, 777 - ath: Option<CowStr<'s>>, 778 - ) -> Result<CowStr<'s>> { 792 + method: &str, 793 + url: &str, 794 + nonce: Option<&str>, 795 + ath: Option<&str>, 796 + ) -> Result<SmolStr> { 779 797 let secret = match crypto::Key::try_from(key).map_err(DpopError::crypto)? { 780 798 crypto::Key::P256(crypto::Kind::Secret(sk)) => sk, 781 799 _ => return Err(DpopError::unsupported_key()), 782 800 }; 783 - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); 784 - header.typ = Some(JWT_HEADER_TYP_DPOP.into()); 801 + let mut header: RegisteredHeader<&str> = 802 + RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); 803 + header.typ = Some(JWT_HEADER_TYP_DPOP); 785 804 header.jwk = Some(Jwk { 786 805 key: Key::from(&crypto::Key::from(secret.public_key())), 787 806 prm: Default::default(), 788 807 }); 789 808 790 - let claims = Claims { 809 + let claims: Claims<&str> = Claims { 791 810 registered: RegisteredClaims { 792 811 jti: Some(generate_jti()), 793 812 iat: Some(Utc::now().timestamp()), ··· 796 815 public: PublicClaims { 797 816 htm: Some(method), 798 817 htu: Some(url), 799 - ath: ath, 800 - nonce: nonce, 818 + ath, 819 + nonce, 801 820 }, 802 821 }; 803 822 Ok(signing::create_signed_jwt_es256(
+4 -3
crates/jacquard-oauth/src/jose.rs
··· 5 5 /// Signed JWT creation for supported algorithms (ES256, ES384, ES256K, EdDSA). 6 6 pub mod signing; 7 7 8 + use jacquard_common::bos::{BosStr, DefaultStr}; 8 9 use serde::{Deserialize, Serialize}; 9 10 10 11 /// A JOSE header, covering the supported JWS formats. ··· 12 13 /// Serialized as an untagged enum so the wire format matches the relevant JOSE spec directly. 13 14 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 14 15 #[serde(untagged)] 15 - pub enum Header<'a> { 16 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 17 + pub enum Header<S: BosStr = DefaultStr> { 16 18 /// A JWS compact-serialization header. 17 - #[serde(borrow)] 18 - Jws(jws::Header<'a>), 19 + Jws(jws::Header<S>), 19 20 } 20 21
+38 -22
crates/jacquard-oauth/src/jose/jws.rs
··· 1 - use jacquard_common::{CowStr, IntoStatic}; 1 + use jacquard_common::{IntoStatic, bos::{BosStr, DefaultStr}}; 2 2 use jose_jwa::Algorithm; 3 3 use jose_jwk::Jwk; 4 4 use serde::{Deserialize, Serialize}; 5 5 6 6 /// A JWS compact-serialization header, wrapping the registered header fields. 7 7 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 8 - pub struct Header<'a> { 8 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 9 + pub struct Header<S: BosStr = DefaultStr> { 9 10 /// The registered header parameters defined by the JWS specification. 10 11 #[serde(flatten)] 11 - #[serde(borrow)] 12 - pub registered: RegisteredHeader<'a>, 12 + pub registered: RegisteredHeader<S>, 13 13 } 14 14 15 - impl<'a> From<Header<'a>> for super::super::jose::Header<'a> { 16 - fn from(header: Header<'a>) -> Self { 15 + impl<S: BosStr> From<Header<S>> for super::super::jose::Header<S> { 16 + fn from(header: Header<S>) -> Self { 17 17 super::super::jose::Header::Jws(header) 18 18 } 19 19 } 20 20 21 21 /// Registered JWS header parameters as defined in RFC 7515 §4.1. 22 22 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 23 - 24 - pub struct RegisteredHeader<'a> { 23 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 24 + pub struct RegisteredHeader<S: BosStr = DefaultStr> { 25 25 /// The cryptographic algorithm used to sign the JWS (e.g., `ES256`). 26 26 pub alg: Algorithm, 27 27 /// JWK Set URL: a URI pointing to a resource containing the public key(s) used to sign the JWS. 28 - #[serde(borrow)] 29 28 #[serde(skip_serializing_if = "Option::is_none")] 30 - pub jku: Option<CowStr<'a>>, 29 + pub jku: Option<S>, 31 30 /// JSON Web Key: the public key used to verify the JWS, embedded directly in the header. 32 31 #[serde(skip_serializing_if = "Option::is_none")] 33 32 pub jwk: Option<Jwk>, 34 33 /// Key ID: a hint indicating which key was used to sign the JWS. 35 34 #[serde(skip_serializing_if = "Option::is_none")] 36 - pub kid: Option<CowStr<'a>>, 35 + pub kid: Option<S>, 37 36 /// X.509 URL: a URI pointing to a resource for the X.509 certificate used to sign the JWS. 38 37 #[serde(skip_serializing_if = "Option::is_none")] 39 - pub x5u: Option<CowStr<'a>>, 38 + pub x5u: Option<S>, 40 39 /// X.509 certificate chain: the certificate (and chain) corresponding to the key used to sign the JWS. 41 40 #[serde(skip_serializing_if = "Option::is_none")] 42 - pub x5c: Option<CowStr<'a>>, 41 + pub x5c: Option<S>, 43 42 /// X.509 certificate SHA-1 thumbprint: base64url-encoded SHA-1 digest of the DER-encoded certificate. 44 43 #[serde(skip_serializing_if = "Option::is_none")] 45 - pub x5t: Option<CowStr<'a>>, 44 + pub x5t: Option<S>, 46 45 /// X.509 certificate SHA-256 thumbprint: base64url-encoded SHA-256 digest of the DER-encoded certificate. 47 46 #[serde(skip_serializing_if = "Option::is_none")] 48 47 #[serde(rename = "x5t#S256")] 49 - pub x5ts256: Option<CowStr<'a>>, 48 + pub x5ts256: Option<S>, 50 49 /// Type: declares the media type of the complete JWS, used by applications to disambiguate among JOSe objects. 51 50 #[serde(skip_serializing_if = "Option::is_none")] 52 - pub typ: Option<CowStr<'a>>, 51 + pub typ: Option<S>, 53 52 /// Content type: declares the media type of the secured content (the payload). 54 53 #[serde(skip_serializing_if = "Option::is_none")] 55 - pub cty: Option<CowStr<'a>>, 54 + pub cty: Option<S>, 56 55 } 57 56 58 - impl From<Algorithm> for RegisteredHeader<'_> { 57 + impl<S: BosStr> From<Algorithm> for RegisteredHeader<S> { 59 58 fn from(alg: Algorithm) -> Self { 60 59 Self { 61 60 alg, ··· 72 71 } 73 72 } 74 73 75 - impl<'a> From<RegisteredHeader<'a>> for super::super::jose::Header<'a> { 76 - fn from(registered: RegisteredHeader<'a>) -> Self { 74 + impl<S: BosStr> From<RegisteredHeader<S>> for super::super::jose::Header<S> { 75 + fn from(registered: RegisteredHeader<S>) -> Self { 77 76 super::super::jose::Header::Jws(Header { registered }) 78 77 } 79 78 } 80 79 81 - impl IntoStatic for RegisteredHeader<'_> { 82 - type Output = RegisteredHeader<'static>; 80 + impl<S> IntoStatic for RegisteredHeader<S> 81 + where 82 + S: BosStr + IntoStatic, 83 + S::Output: BosStr, 84 + { 85 + type Output = RegisteredHeader<S::Output>; 83 86 fn into_static(self) -> Self::Output { 84 87 RegisteredHeader { 85 88 alg: self.alg, ··· 95 98 } 96 99 } 97 100 } 101 + 102 + impl<S> IntoStatic for Header<S> 103 + where 104 + S: BosStr + IntoStatic, 105 + S::Output: BosStr, 106 + { 107 + type Output = Header<S::Output>; 108 + fn into_static(self) -> Self::Output { 109 + Header { 110 + registered: self.registered.into_static(), 111 + } 112 + } 113 + }
+105 -34
crates/jacquard-oauth/src/jose/jwt.rs
··· 1 - use jacquard_common::{CowStr, IntoStatic}; 1 + use jacquard_common::{ 2 + IntoStatic, 3 + bos::{BosStr, DefaultStr}, 4 + }; 2 5 use serde::{Deserialize, Serialize}; 6 + use smol_str::SmolStr; 3 7 4 8 /// Full JWT claims payload, combining registered and public (DPoP-specific) claims. 5 - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] 6 - pub struct Claims<'a> { 9 + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 10 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 11 + pub struct Claims<S: BosStr = DefaultStr> { 7 12 /// Standard registered JWT claims (iss, sub, aud, exp, etc.). 8 13 #[serde(flatten)] 9 - pub registered: RegisteredClaims<'a>, 14 + pub registered: RegisteredClaims<S>, 10 15 /// Public claims used in DPoP proofs (htm, htu, ath, nonce). 11 16 #[serde(flatten)] 12 - #[serde(borrow)] 13 - pub public: PublicClaims<'a>, 17 + pub public: PublicClaims<S>, 18 + } 19 + 20 + /// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. 21 + /// 22 + /// All `S`-typed fields are wrapped in `Option<S>`, which is `Default` regardless of `S`. 23 + impl<S: BosStr> Default for Claims<S> { 24 + fn default() -> Self { 25 + Self { 26 + registered: RegisteredClaims::default(), 27 + public: PublicClaims::default(), 28 + } 29 + } 14 30 } 15 31 16 32 /// Standard registered JWT claims as defined in RFC 7519 §4.1. 17 - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] 18 - 19 - pub struct RegisteredClaims<'a> { 33 + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 34 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 35 + pub struct RegisteredClaims<S: BosStr = DefaultStr> { 20 36 /// Issuer: identifies the principal that issued the JWT. 21 - #[serde(borrow)] 22 37 #[serde(skip_serializing_if = "Option::is_none")] 23 - pub iss: Option<CowStr<'a>>, 38 + pub iss: Option<S>, 24 39 /// Subject: identifies the principal that is the subject of the JWT. 25 40 #[serde(skip_serializing_if = "Option::is_none")] 26 - pub sub: Option<CowStr<'a>>, 41 + pub sub: Option<S>, 27 42 /// Audience: recipients that the JWT is intended for. 28 43 #[serde(skip_serializing_if = "Option::is_none")] 29 - pub aud: Option<RegisteredClaimsAud<'a>>, 44 + pub aud: Option<RegisteredClaimsAud<S>>, 30 45 /// Expiration time (Unix timestamp): the JWT must not be accepted on or after this time. 31 46 #[serde(skip_serializing_if = "Option::is_none")] 32 47 pub exp: Option<i64>, ··· 38 53 pub iat: Option<i64>, 39 54 /// JWT ID: unique identifier for the token, used to prevent replay attacks. 40 55 #[serde(skip_serializing_if = "Option::is_none")] 41 - pub jti: Option<CowStr<'a>>, 56 + pub jti: Option<SmolStr>, 57 + } 58 + 59 + /// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. 60 + /// 61 + /// All `S`-typed fields are wrapped in `Option<S>`, which is `Default` regardless of `S`. 62 + impl<S: BosStr> Default for RegisteredClaims<S> { 63 + fn default() -> Self { 64 + Self { 65 + iss: None, 66 + sub: None, 67 + aud: None, 68 + exp: None, 69 + nbf: None, 70 + iat: None, 71 + jti: None, 72 + } 73 + } 42 74 } 43 75 44 76 /// Public claims used in DPoP proof JWTs (RFC 9449). 45 77 /// 46 78 /// These claims bind the DPoP proof to a specific HTTP request, preventing 47 79 /// the proof from being replayed against a different endpoint or method. 48 - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] 49 - 50 - pub struct PublicClaims<'a> { 80 + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 81 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 82 + pub struct PublicClaims<S: BosStr = DefaultStr> { 51 83 /// HTTP method of the request the DPoP proof is bound to (e.g., `"POST"`). 52 - #[serde(borrow)] 53 84 #[serde(skip_serializing_if = "Option::is_none")] 54 - pub htm: Option<CowStr<'a>>, 85 + pub htm: Option<S>, 55 86 /// HTTP target URI of the request the DPoP proof is bound to. 56 87 #[serde(skip_serializing_if = "Option::is_none")] 57 - pub htu: Option<CowStr<'a>>, 88 + pub htu: Option<S>, 58 89 /// Access token hash: base64url-encoded SHA-256 of the access token, binding the proof to a specific token. 59 90 #[serde(skip_serializing_if = "Option::is_none")] 60 - pub ath: Option<CowStr<'a>>, 91 + pub ath: Option<S>, 61 92 /// Server-provided nonce, included to prevent replay attacks when required by the authorization server. 62 93 #[serde(skip_serializing_if = "Option::is_none")] 63 - pub nonce: Option<CowStr<'a>>, 94 + pub nonce: Option<S>, 95 + } 96 + 97 + /// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. 98 + /// 99 + /// All `S`-typed fields are wrapped in `Option<S>`, which is `Default` regardless of `S`. 100 + impl<S: BosStr> Default for PublicClaims<S> { 101 + fn default() -> Self { 102 + Self { 103 + htm: None, 104 + htu: None, 105 + ath: None, 106 + nonce: None, 107 + } 108 + } 64 109 } 65 110 66 - impl<'a> From<RegisteredClaims<'a>> for Claims<'a> { 67 - fn from(registered: RegisteredClaims<'a>) -> Self { 111 + impl<S: BosStr> From<RegisteredClaims<S>> for Claims<S> { 112 + fn from(registered: RegisteredClaims<S>) -> Self { 68 113 Self { 69 114 registered, 70 115 public: PublicClaims::default(), ··· 75 120 /// The `aud` (audience) claim, which may be a single string or a list of strings per RFC 7519. 76 121 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 77 122 #[serde(untagged)] 78 - pub enum RegisteredClaimsAud<'a> { 123 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 124 + pub enum RegisteredClaimsAud<S: BosStr = DefaultStr> { 79 125 /// A single audience identifier. 80 - #[serde(borrow)] 81 - Single(CowStr<'a>), 126 + Single(S), 82 127 /// Multiple audience identifiers. 83 - Multiple(Vec<CowStr<'a>>), 128 + Multiple(Vec<S>), 84 129 } 85 130 86 - impl IntoStatic for RegisteredClaims<'_> { 87 - type Output = RegisteredClaims<'static>; 131 + impl<S> IntoStatic for RegisteredClaims<S> 132 + where 133 + S: BosStr + IntoStatic, 134 + S::Output: BosStr, 135 + { 136 + type Output = RegisteredClaims<S::Output>; 88 137 fn into_static(self) -> Self::Output { 89 138 RegisteredClaims { 90 139 iss: self.iss.map(IntoStatic::into_static), ··· 98 147 } 99 148 } 100 149 101 - impl IntoStatic for PublicClaims<'_> { 102 - type Output = PublicClaims<'static>; 150 + impl<S> IntoStatic for PublicClaims<S> 151 + where 152 + S: BosStr + IntoStatic, 153 + S::Output: BosStr, 154 + { 155 + type Output = PublicClaims<S::Output>; 103 156 fn into_static(self) -> Self::Output { 104 157 PublicClaims { 105 158 htm: self.htm.map(IntoStatic::into_static), ··· 110 163 } 111 164 } 112 165 113 - impl IntoStatic for RegisteredClaimsAud<'_> { 114 - type Output = RegisteredClaimsAud<'static>; 166 + impl<S> IntoStatic for Claims<S> 167 + where 168 + S: BosStr + IntoStatic, 169 + S::Output: BosStr, 170 + { 171 + type Output = Claims<S::Output>; 172 + fn into_static(self) -> Self::Output { 173 + Claims { 174 + registered: self.registered.into_static(), 175 + public: self.public.into_static(), 176 + } 177 + } 178 + } 179 + 180 + impl<S> IntoStatic for RegisteredClaimsAud<S> 181 + where 182 + S: BosStr + IntoStatic, 183 + S::Output: BosStr, 184 + { 185 + type Output = RegisteredClaimsAud<S::Output>; 115 186 fn into_static(self) -> Self::Output { 116 187 match self { 117 188 RegisteredClaimsAud::Single(s) => RegisteredClaimsAud::Single(s.into_static()),
+24 -20
crates/jacquard-oauth/src/jose/signing.rs
··· 1 1 use base64::Engine; 2 2 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 3 - use jacquard_common::CowStr; 3 + use jacquard_common::bos::BosStr; 4 + use smol_str::SmolStr; 4 5 5 6 use super::{Header, jwt::Claims}; 6 7 7 8 /// Builds the base64url-encoded `header.payload` signing input. 8 - fn signing_input(header: &Header, claims: &Claims) -> serde_json::Result<(String, String)> { 9 + fn signing_input<S: BosStr + serde::Serialize>( 10 + header: &Header<S>, 11 + claims: &Claims<&str>, 12 + ) -> serde_json::Result<(String, String)> { 9 13 let h = URL_SAFE_NO_PAD.encode(serde_json::to_string(header)?); 10 14 let p = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims)?); 11 15 Ok((h, p)) 12 16 } 13 17 14 18 /// Assembles a compact JWS from pre-encoded parts and raw signature bytes. 15 - fn assemble(header: &str, payload: &str, sig: &[u8]) -> CowStr<'static> { 16 - format!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(sig)).into() 19 + fn assemble(header: &str, payload: &str, sig: &[u8]) -> SmolStr { 20 + smol_str::format_smolstr!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(sig)) 17 21 } 18 22 19 23 /// Creates a compact-serialized signed JWT using ES256 (P-256 ECDSA with SHA-256). 20 - pub fn create_signed_jwt_es256( 24 + pub fn create_signed_jwt_es256<S: BosStr + serde::Serialize>( 21 25 key: p256::ecdsa::SigningKey, 22 - header: Header, 23 - claims: Claims, 24 - ) -> serde_json::Result<CowStr<'static>> { 26 + header: Header<S>, 27 + claims: Claims<&str>, 28 + ) -> serde_json::Result<SmolStr> { 25 29 use p256::ecdsa::signature::Signer; 26 30 let (h, p) = signing_input(&header, &claims)?; 27 31 let sig: p256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); ··· 29 33 } 30 34 31 35 /// Creates a compact-serialized signed JWT using ES384 (P-384 ECDSA with SHA-384). 32 - pub fn create_signed_jwt_es384( 36 + pub fn create_signed_jwt_es384<S: BosStr + serde::Serialize>( 33 37 key: p384::ecdsa::SigningKey, 34 - header: Header, 35 - claims: Claims, 36 - ) -> serde_json::Result<CowStr<'static>> { 38 + header: Header<S>, 39 + claims: Claims<&str>, 40 + ) -> serde_json::Result<SmolStr> { 37 41 use p384::ecdsa::signature::Signer; 38 42 let (h, p) = signing_input(&header, &claims)?; 39 43 let sig: p384::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); ··· 41 45 } 42 46 43 47 /// Creates a compact-serialized signed JWT using ES256K (secp256k1 ECDSA with SHA-256). 44 - pub fn create_signed_jwt_es256k( 48 + pub fn create_signed_jwt_es256k<S: BosStr + serde::Serialize>( 45 49 key: k256::ecdsa::SigningKey, 46 - header: Header, 47 - claims: Claims, 48 - ) -> serde_json::Result<CowStr<'static>> { 50 + header: Header<S>, 51 + claims: Claims<&str>, 52 + ) -> serde_json::Result<SmolStr> { 49 53 use k256::ecdsa::signature::Signer; 50 54 let (h, p) = signing_input(&header, &claims)?; 51 55 let sig: k256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); ··· 53 57 } 54 58 55 59 /// Creates a compact-serialized signed JWT using EdDSA (Ed25519). 56 - pub fn create_signed_jwt_eddsa( 60 + pub fn create_signed_jwt_eddsa<S: BosStr + serde::Serialize>( 57 61 key: ed25519_dalek::SigningKey, 58 - header: Header, 59 - claims: Claims, 60 - ) -> serde_json::Result<CowStr<'static>> { 62 + header: Header<S>, 63 + claims: Claims<&str>, 64 + ) -> serde_json::Result<SmolStr> { 61 65 use ed25519_dalek::Signer; 62 66 let (h, p) = signing_input(&header, &claims)?; 63 67 let sig = key.sign(format!("{h}.{p}").as_bytes());
+13 -11
crates/jacquard-oauth/src/keyset.rs
··· 1 1 use crate::jose::jws::RegisteredHeader; 2 2 use crate::jose::jwt::Claims; 3 3 use crate::jose::signing; 4 - use jacquard_common::CowStr; 5 4 use jose_jwa::{Algorithm, Signing}; 6 5 use jose_jwk::{Class, EcCurves, OkpCurves, crypto}; 7 6 use jose_jwk::{Jwk, JwkSet, Key}; 7 + use smol_str::{SmolStr, ToSmolStr}; 8 8 use std::collections::HashSet; 9 9 use thiserror::Error; 10 10 ··· 87 87 /// Signs a JWT with the best available key that matches one of the requested algorithms. 88 88 /// 89 89 /// Returns [`Error::NotFound`] if no key in the keyset supports any of the given algorithms. 90 - pub fn create_jwt(&self, algs: &[Signing], claims: Claims) -> Result<CowStr<'static>> { 90 + pub fn create_jwt(&self, algs: &[Signing], claims: Claims<&str>) -> Result<SmolStr> { 91 91 let Some(jwk) = self.find_key(algs, Class::Signing) else { 92 92 return Err(Error::NotFound(algs.to_vec())); 93 93 }; ··· 116 116 None 117 117 } 118 118 119 - fn create_jwt_with_key(&self, key: &Jwk, claims: Claims) -> Result<CowStr<'static>> { 120 - let kid = key.prm.kid.clone().unwrap(); 119 + fn create_jwt_with_key(&self, key: &Jwk, claims: Claims<&str>) -> Result<SmolStr> { 120 + let kid = key.prm.kid.as_ref().unwrap().to_smolstr(); 121 121 match &key.key { 122 122 Key::Ec(ec) => { 123 123 let d = ec.d.as_ref().ok_or(Error::MissingPrivateKey)?; ··· 127 127 let signing_key = p256::ecdsa::SigningKey::from_bytes(d_bytes.into()) 128 128 .map_err(|e| Error::InvalidKey(e.to_string()))?; 129 129 let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); 130 - header.kid = Some(kid.into()); 130 + header.kid = Some(kid); 131 131 Ok(signing::create_signed_jwt_es256( 132 132 signing_key, 133 133 header.into(), ··· 137 137 EcCurves::P384 => { 138 138 let signing_key = p384::ecdsa::SigningKey::from_bytes(d_bytes.into()) 139 139 .map_err(|e| Error::InvalidKey(e.to_string()))?; 140 - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es384)); 141 - header.kid = Some(kid.into()); 140 + let mut header: RegisteredHeader<&str> = 141 + RegisteredHeader::from(Algorithm::Signing(Signing::Es384)); 142 + header.kid = Some(kid.as_str()); 142 143 Ok(signing::create_signed_jwt_es384( 143 144 signing_key, 144 145 header.into(), ··· 148 149 EcCurves::P256K => { 149 150 let signing_key = k256::ecdsa::SigningKey::from_bytes(d_bytes.into()) 150 151 .map_err(|e| Error::InvalidKey(e.to_string()))?; 151 - let mut header = 152 + let mut header: RegisteredHeader<&str> = 152 153 RegisteredHeader::from(Algorithm::Signing(Signing::Es256K)); 153 - header.kid = Some(kid.into()); 154 + header.kid = Some(kid.as_str()); 154 155 Ok(signing::create_signed_jwt_es256k( 155 156 signing_key, 156 157 header.into(), ··· 166 167 let d_bytes: &[u8] = d.as_ref(); 167 168 let signing_key = ed25519_dalek::SigningKey::try_from(d_bytes) 168 169 .map_err(|e| Error::InvalidKey(e.to_string()))?; 169 - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa)); 170 - header.kid = Some(kid.into()); 170 + let mut header: RegisteredHeader<&str> = 171 + RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa)); 172 + header.kid = Some(kid.as_str()); 171 173 Ok(signing::create_signed_jwt_eddsa( 172 174 signing_key, 173 175 header.into(),
+19 -10
crates/jacquard-oauth/src/loopback.rs
··· 53 53 resolver::OAuthResolver, 54 54 types::{AuthorizeOptions, CallbackParams}, 55 55 }; 56 + use jacquard_common::IntoStatic; 56 57 use jacquard_common::deps::fluent_uri::Uri; 57 - use jacquard_common::{IntoStatic, cowstr::ToCowStr}; 58 58 use rouille::Server; 59 + use smol_str::{SmolStr, ToSmolStr}; 59 60 use std::net::SocketAddr; 60 61 use tokio::sync::mpsc; 61 62 ··· 115 116 let code = request.get_param("code").unwrap(); 116 117 let iss = request.get_param("iss").unwrap(); 117 118 let callback_params = CallbackParams { 118 - state: Some(state.to_cowstr().into_static()), 119 - code: code.to_cowstr().into_static(), 120 - iss: Some(iss.to_cowstr().into_static()), 119 + state: Some(state.to_smolstr()), 120 + code: code.to_smolstr(), 121 + iss: Some(iss.to_smolstr()), 121 122 }; 122 123 tx.try_send(callback_params).unwrap(); 123 124 rouille::Response::text("Logged in!") ··· 131 132 #[allow(dead_code)] 132 133 server_handle: std::thread::JoinHandle<()>, 133 134 server_stop: std::sync::mpsc::Sender<()>, 134 - callback_rx: mpsc::Receiver<CallbackParams<'static>>, 135 + callback_rx: mpsc::Receiver<CallbackParams>, 135 136 } 136 137 137 138 /// One-shot OAuth callback server. ··· 214 215 pub async fn login_with_local_server( 215 216 &self, 216 217 input: impl AsRef<str>, 217 - opts: AuthorizeOptions<'_>, 218 + opts: AuthorizeOptions<SmolStr>, 218 219 cfg: LoopbackConfig, 219 220 ) -> crate::error::Result<super::client::OAuthSession<T, S>> { 220 221 let port = match cfg.port { ··· 252 253 pub fn build_localhost_client_data( 253 254 &self, 254 255 cfg: &LoopbackConfig, 255 - opts: &AuthorizeOptions<'_>, 256 + opts: &AuthorizeOptions<SmolStr>, 256 257 local_addr: SocketAddr, 257 - ) -> crate::session::ClientData<'static> { 258 + ) -> crate::session::ClientData<SmolStr> { 258 259 let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); 259 260 let redirect = Uri::parse(redirect_uri).unwrap(); 260 261 261 262 let scopes = if opts.scopes.is_empty() { 262 - Some(self.registry.client_data.config.scopes.clone()) 263 + Some( 264 + self.registry 265 + .client_data 266 + .config 267 + .scopes 268 + .iter() 269 + .cloned() 270 + .collect(), 271 + ) 263 272 } else { 264 - Some(opts.scopes.clone().into_static()) 273 + Some(opts.scopes.clone()) 265 274 }; 266 275 267 276 crate::session::ClientData {
+148 -132
crates/jacquard-oauth/src/request.rs
··· 1 + use std::str::FromStr; 2 + 1 3 use chrono::{TimeDelta, Utc}; 2 4 use http::{Method, Request, StatusCode}; 3 5 use jacquard_common::{ 4 6 CowStr, IntoStatic, 5 - cowstr::ToCowStr, 7 + bos::{BosStr, DefaultStr}, 6 8 http_client::HttpClient, 7 9 session::SessionStoreError, 8 10 types::{ ··· 34 36 OAuthTokenResponse, ParParameters, RefreshRequestParameters, RevocationRequestParameters, 35 37 TokenGrantType, TokenRequestParameters, TokenSet, 36 38 }, 37 - utils::{compare_algos, generate_dpop_key, generate_nonce, generate_pkce}, 39 + utils::{generate_dpop_key, generate_nonce, generate_pkce}, 38 40 }; 39 41 40 42 // https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 ··· 428 430 #[allow(dead_code)] 429 431 pub enum OAuthRequest<'a> { 430 432 /// Standard authorization-code token exchange. 431 - Token(TokenRequestParameters<'a>), 433 + Token(TokenRequestParameters<&'a str>), 432 434 /// Refresh-token grant to obtain a fresh access token. 433 - Refresh(RefreshRequestParameters<'a>), 435 + Refresh(RefreshRequestParameters<&'a str>), 434 436 /// Token revocation request (RFC 7009). 435 - Revocation(RevocationRequestParameters<'a>), 437 + Revocation(RevocationRequestParameters<&'a str>), 436 438 /// Token introspection request (RFC 7662). 437 439 Introspection, 438 440 /// Pushed authorization request (RFC 9126) for pre-registering auth parameters. 439 - PushedAuthorizationRequest(ParParameters<'a>), 441 + PushedAuthorizationRequest(ParParameters<&'a str>), 440 442 } 441 443 442 444 impl OAuthRequest<'_> { 443 445 /// Return a human-readable name for this request variant, used in error messages. 444 - pub fn name(&self) -> CowStr<'static> { 445 - CowStr::new_static(match self { 446 + pub fn name(&self) -> &'static str { 447 + match self { 446 448 Self::Token(_) => "token", 447 449 Self::Refresh(_) => "refresh", 448 450 Self::Revocation(_) => "revocation", 449 451 Self::Introspection => "introspection", 450 452 Self::PushedAuthorizationRequest(_) => "pushed_authorization_request", 451 - }) 453 + } 452 454 } 453 455 /// Returns the HTTP status code that a successful response to this request should carry. 454 456 pub fn expected_status(&self) -> StatusCode { ··· 476 478 client_assertion_type: Option<CowStr<'a>>, 477 479 /// A JWT signed with the client's private key, proving client identity to the server. 478 480 #[serde(skip_serializing_if = "Option::is_none")] 479 - client_assertion: Option<CowStr<'a>>, 481 + client_assertion: Option<SmolStr>, 480 482 /// The grant-specific parameters (token request, refresh, PAR, etc.) flattened into the body. 481 483 #[serde(flatten)] 482 484 parameters: T, ··· 488 490 /// and the optional signing keyset into a single value that is passed to helper functions such 489 491 /// as [`par`], [`exchange_code`], [`refresh`], and [`revoke`]. 490 492 #[derive(Debug, Clone)] 491 - pub struct OAuthMetadata { 493 + pub struct OAuthMetadata<S: BosStr = DefaultStr> { 492 494 /// Metadata fetched from the authorization server's `/.well-known/oauth-authorization-server` document. 493 - pub server_metadata: OAuthAuthorizationServerMetadata<'static>, 495 + pub server_metadata: OAuthAuthorizationServerMetadata, 494 496 /// This client's registered metadata, derived from [`crate::atproto::AtprotoClientMetadata`]. 495 - pub client_metadata: OAuthClientMetadata<'static>, 497 + pub client_metadata: OAuthClientMetadata<S>, 496 498 /// Optional signing keyset; required for `private_key_jwt` client authentication. 497 499 pub keyset: Option<Keyset>, 498 500 } 499 501 500 - impl OAuthMetadata { 502 + impl<S: BosStr> OAuthMetadata<S> { 501 503 /// Fetch server metadata and assemble an `OAuthMetadata` from an active session context. 502 504 /// 503 505 /// Contacts the authorization server recorded in `session_data` to retrieve its current 504 506 /// metadata, then combines it with the client configuration. This is the preferred way to 505 507 /// build an `OAuthMetadata` during token refresh or revocation. 506 - pub async fn new<'r, T: HttpClient + OAuthResolver + Send + Sync>( 508 + pub async fn new<T: HttpClient + OAuthResolver + Send + Sync>( 507 509 client: &T, 508 - ClientData { keyset, config }: &ClientData<'r>, 509 - session_data: &ClientSessionData<'r>, 510 - ) -> Result<Self> { 510 + ClientData { keyset, config }: &ClientData<S>, 511 + session_data: &ClientSessionData, 512 + ) -> Result<Self> 513 + where 514 + S: Clone + FromStr + Ord, 515 + <S as FromStr>::Err: core::fmt::Debug, 516 + { 511 517 Ok(OAuthMetadata { 512 518 server_metadata: client 513 - .get_authorization_server_metadata(&session_data.authserver_url) 519 + .get_authorization_server_metadata(session_data.authserver_url.as_ref()) 514 520 .await?, 515 - client_metadata: atproto_client_metadata(config.clone(), &keyset) 516 - .unwrap() 517 - .into_static(), 521 + client_metadata: atproto_client_metadata(&config, &keyset)?, 518 522 keyset: keyset.clone(), 519 523 }) 520 524 } ··· 527 531 /// persisted (e.g., in the auth store) so it can be retrieved and verified during 528 532 /// [`crate::client::OAuthClient::callback`]. 529 533 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all, fields(login_hint = login_hint.as_ref().map(|h| h.as_ref()))))] 530 - pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( 534 + pub async fn par< 535 + S: BosStr + Clone + Send + Sync, 536 + T: OAuthResolver + DpopExt + Send + Sync + 'static, 537 + >( 531 538 client: &T, 532 - login_hint: Option<CowStr<'r>>, 539 + login_hint: Option<S>, 533 540 prompt: Option<AuthorizeOptionPrompt>, 534 - metadata: &OAuthMetadata, 535 - state: Option<CowStr<'r>>, 536 - ) -> crate::request::Result<AuthRequestData<'r>> { 537 - let state = if let Some(state) = state { 538 - state 539 - } else { 540 - generate_nonce() 541 - }; 541 + metadata: &mut OAuthMetadata<S>, 542 + state: Option<SmolStr>, 543 + ) -> crate::request::Result<AuthRequestData> { 544 + let state = state.unwrap_or_else(generate_nonce); 542 545 let (code_challenge, verifier) = generate_pkce(); 543 546 544 - let Some(dpop_key) = generate_dpop_key(&metadata.server_metadata) else { 547 + let Some(dpop_key) = generate_dpop_key(&mut metadata.server_metadata) else { 545 548 return Err(RequestError::token_verification()); 546 549 }; 547 550 let mut dpop_data = DpopReqData { 548 551 dpop_key, 549 552 dpop_authserver_nonce: None, 550 553 }; 551 - let parameters = ParParameters { 554 + let parameters: ParParameters<&str> = ParParameters { 552 555 response_type: AuthorizationResponseType::Code, 553 - redirect_uri: metadata.client_metadata.redirect_uris[0].to_cowstr(), 554 - state: state.clone(), 555 - scope: metadata.client_metadata.scope.clone(), 556 + redirect_uri: metadata.client_metadata.redirect_uris[0].as_ref(), 557 + state: state.as_ref(), 558 + scope: metadata.client_metadata.scope.as_ref().map(|s| s.as_ref()), 556 559 response_mode: None, 557 - code_challenge, 560 + code_challenge: code_challenge.as_str(), 558 561 code_challenge_method: AuthorizationCodeChallengeMethod::S256, 559 - login_hint: login_hint, 560 - prompt: prompt.map(CowStr::from), 562 + login_hint: login_hint.as_ref().map(|h| h.as_ref()), 563 + prompt: prompt.map(|p| p.into()), 561 564 }; 562 565 563 566 if metadata ··· 565 568 .pushed_authorization_request_endpoint 566 569 .is_some() 567 570 { 568 - let par_response = oauth_request::<OAuthParResponse, T, DpopReqData>( 571 + let par_response = oauth_request::<OAuthParResponse, T, DpopReqData, _>( 569 572 &client, 570 573 &mut dpop_data, 571 574 OAuthRequest::PushedAuthorizationRequest(parameters), ··· 574 577 .await?; 575 578 576 579 let scopes = if let Some(scope) = &metadata.client_metadata.scope { 577 - Scope::parse_multiple_reduced(&scope) 580 + Scope::<SmolStr>::parse_multiple_reduced(scope.as_ref()) 578 581 .expect("Failed to parse scopes") 579 582 .into_static() 580 583 } else { 581 584 vec![] 582 585 }; 583 - let auth_req_data = AuthRequestData { 584 - state, 585 - authserver_url: metadata.server_metadata.issuer.clone(), 586 + let auth_req_data: AuthRequestData = AuthRequestData { 587 + state: state.into(), 588 + authserver_url: metadata.server_metadata.issuer.to_smolstr(), 586 589 account_did: None, 587 590 scopes, 588 - request_uri: par_response.request_uri.to_cowstr().into_static(), 589 - authserver_token_endpoint: metadata.server_metadata.token_endpoint.clone(), 591 + request_uri: par_response.request_uri.clone(), 592 + authserver_token_endpoint: metadata.server_metadata.token_endpoint.to_smolstr(), 590 593 authserver_revocation_endpoint: metadata.server_metadata.revocation_endpoint.clone(), 591 - pkce_verifier: verifier, 594 + pkce_verifier: verifier.into(), 592 595 dpop_data, 593 596 }; 594 597 ··· 606 609 607 610 /// Exchange a refresh token for a fresh token set and update the session data in place. 608 611 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all, fields(did = %session_data.account_did)))] 609 - pub async fn refresh<'r, T>( 612 + pub async fn refresh<S, T>( 610 613 client: &T, 611 - mut session_data: ClientSessionData<'r>, 612 - metadata: &OAuthMetadata, 613 - ) -> Result<ClientSessionData<'r>> 614 + mut session_data: ClientSessionData, 615 + metadata: &OAuthMetadata<S>, 616 + ) -> Result<ClientSessionData> 614 617 where 618 + S: BosStr + FromStr, 615 619 T: OAuthResolver + DpopExt + Send + Sync + 'static, 616 620 { 617 621 let Some(refresh_token) = session_data.token_set.refresh_token.as_ref() else { ··· 631 635 .await?; 632 636 let iss = metadata.server_metadata.issuer.clone(); 633 637 634 - let response = oauth_request::<OAuthTokenResponse, T, DpopClientData>( 638 + let response = oauth_request::<OAuthTokenResponse, T, DpopClientData, _>( 635 639 client, 636 640 &mut session_data.dpop_data, 637 641 OAuthRequest::Refresh(RefreshRequestParameters { 638 642 grant_type: TokenGrantType::RefreshToken, 639 - refresh_token: refresh_token.clone(), 643 + refresh_token: refresh_token.as_ref(), 640 644 scope: None, 641 645 }), 642 646 metadata, ··· 650 654 .map(Datetime::new) 651 655 }); 652 656 653 - session_data.update_with_tokens(TokenSet { 657 + session_data.update_with_tokens(&TokenSet { 654 658 iss, 655 659 sub: session_data.token_set.sub.clone(), 656 - aud: CowStr::Owned(aud.to_smolstr()), 657 - scope: response.scope.map(CowStr::Owned), 658 - access_token: CowStr::Owned(response.access_token), 659 - refresh_token: response.refresh_token.map(CowStr::Owned), 660 + aud: SmolStr::from(aud.as_str()), 661 + scope: response.scope, 662 + access_token: response.access_token, 663 + refresh_token: response.refresh_token, 660 664 token_type: response.token_type, 661 665 expires_at, 662 666 }); ··· 671 675 /// function performs that verification as part of the exchange, so callers receive a token 672 676 /// set that is safe to persist. 673 677 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] 674 - pub async fn exchange_code<'r, T, D>( 678 + pub async fn exchange_code<S, T, D>( 675 679 client: &T, 676 - data_source: &'r mut D, 680 + data_source: &mut D, 677 681 code: &str, 678 682 verifier: &str, 679 - metadata: &OAuthMetadata, 680 - ) -> Result<TokenSet<'r>> 683 + metadata: &OAuthMetadata<S>, 684 + ) -> Result<TokenSet> 681 685 where 686 + S: BosStr + Send + Sync, 682 687 T: OAuthResolver + DpopExt + Send + Sync + 'static, 683 688 D: DpopDataSource, 684 689 { 685 - let token_response = oauth_request::<OAuthTokenResponse, T, D>( 690 + let token_response = oauth_request::<OAuthTokenResponse, T, D, _>( 686 691 client, 687 692 data_source, 688 693 OAuthRequest::Token(TokenRequestParameters { 689 694 grant_type: TokenGrantType::AuthorizationCode, 690 695 code: code.into(), 691 - redirect_uri: CowStr::Owned( 692 - metadata.client_metadata.redirect_uris[0] 693 - .clone() 694 - .to_smolstr(), 695 - ), 696 + redirect_uri: metadata.client_metadata.redirect_uris[0].as_ref(), 696 697 code_verifier: verifier.into(), 697 698 }), 698 699 metadata, ··· 720 721 Ok(TokenSet { 721 722 iss, 722 723 sub, 723 - aud: CowStr::Owned(aud.to_smolstr()), 724 - scope: token_response.scope.map(CowStr::Owned), 725 - access_token: CowStr::Owned(token_response.access_token), 726 - refresh_token: token_response.refresh_token.map(CowStr::Owned), 724 + aud: SmolStr::from(aud.as_str()), 725 + scope: token_response.scope, 726 + access_token: token_response.access_token, 727 + refresh_token: token_response.refresh_token, 727 728 token_type: token_response.token_type, 728 729 expires_at, 729 730 }) ··· 735 736 /// by the server. The caller is responsible for deleting the session from local storage regardless 736 737 /// of whether revocation succeeds. 737 738 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] 738 - pub async fn revoke<'r, T, D>( 739 + pub async fn revoke<S: BosStr + Send + Sync, T, D>( 739 740 client: &T, 740 - data_source: &'r mut D, 741 + data_source: &mut D, 741 742 token: &str, 742 - metadata: &OAuthMetadata, 743 + metadata: &OAuthMetadata<S>, 743 744 ) -> Result<()> 744 745 where 745 746 T: OAuthResolver + DpopExt + Send + Sync + 'static, 746 747 D: DpopDataSource, 747 748 { 748 - oauth_request::<(), T, D>( 749 + oauth_request::<(), T, D, _>( 749 750 client, 750 751 data_source, 751 752 OAuthRequest::Revocation(RevocationRequestParameters { ··· 763 764 /// client authentication, performs the DPoP-wrapped HTTP POST, and deserializes the response 764 765 /// body into `O`. The type parameter `O` is inferred from the call site; use `()` for requests 765 766 /// where the response body is empty (e.g., revocation). 766 - pub async fn oauth_request<'de: 'r, 'r, O, T, D>( 767 + pub async fn oauth_request<'r, O, T, D, S: BosStr>( 767 768 client: &T, 768 - data_source: &'r mut D, 769 + data_source: &mut D, 769 770 request: OAuthRequest<'r>, 770 - metadata: &OAuthMetadata, 771 + metadata: &OAuthMetadata<S>, 771 772 ) -> Result<O> 772 773 where 773 774 T: OAuthResolver + DpopExt + Send + Sync + 'static, ··· 792 793 _ => unimplemented!(), 793 794 }; 794 795 let req = Request::builder() 795 - .uri(url.to_string()) 796 + .uri(url) 796 797 .method(Method::POST) 797 798 .header("Content-Type", "application/x-www-form-urlencoded") 798 799 .body(body.into_bytes())?; ··· 817 818 } 818 819 819 820 #[inline] 820 - fn endpoint_for_req<'a, 'r>( 821 - server_metadata: &'r OAuthAuthorizationServerMetadata<'a>, 821 + fn endpoint_for_req<'r, S: BosStr>( 822 + server_metadata: &'r OAuthAuthorizationServerMetadata<S>, 822 823 request: &'r OAuthRequest, 823 - ) -> Option<&'r CowStr<'a>> { 824 + ) -> Option<&'r str> { 824 825 match request { 825 - OAuthRequest::Token(_) | OAuthRequest::Refresh(_) => Some(&server_metadata.token_endpoint), 826 - OAuthRequest::Revocation(_) => server_metadata.revocation_endpoint.as_ref(), 827 - OAuthRequest::Introspection => server_metadata.introspection_endpoint.as_ref(), 826 + OAuthRequest::Token(_) | OAuthRequest::Refresh(_) => { 827 + Some(server_metadata.token_endpoint.as_ref()) 828 + } 829 + OAuthRequest::Revocation(_) => server_metadata 830 + .revocation_endpoint 831 + .as_ref() 832 + .map(AsRef::as_ref), 833 + OAuthRequest::Introspection => server_metadata 834 + .introspection_endpoint 835 + .as_ref() 836 + .map(AsRef::as_ref), 828 837 OAuthRequest::PushedAuthorizationRequest(_) => server_metadata 829 838 .pushed_authorization_request_endpoint 830 - .as_ref(), 839 + .as_ref() 840 + .map(AsRef::as_ref), 831 841 } 832 842 } 833 843 ··· 856 866 /// Either absent (for `none` auth) or `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. 857 867 assertion_type: Option<CowStr<'a>>, 858 868 /// A signed JWT proving client identity; present only for `private_key_jwt` auth. 859 - assertion: Option<CowStr<'a>>, 869 + assertion: Option<SmolStr>, 860 870 } 861 871 862 872 impl<'s> ClientAuth<'s> { ··· 870 880 } 871 881 } 872 882 873 - fn build_auth<'a>( 883 + fn build_auth<'a, S: BosStr>( 874 884 keyset: Option<&Keyset>, 875 - server_metadata: &OAuthAuthorizationServerMetadata<'a>, 876 - client_metadata: &OAuthClientMetadata<'a>, 885 + server_metadata: &'a OAuthAuthorizationServerMetadata, 886 + client_metadata: &'a OAuthClientMetadata<S>, 877 887 ) -> Result<ClientAuth<'a>> { 878 888 let method_supported = server_metadata 879 889 .token_endpoint_auth_methods_supported 880 890 .as_ref(); 881 891 882 - let client_id = client_metadata.client_id.to_cowstr().into_static(); 892 + let client_id = CowStr::Borrowed(client_metadata.client_id.as_ref()); 883 893 if let Some(method) = client_metadata.token_endpoint_auth_method.as_ref() { 884 - match (*method).as_ref() { 894 + match method.as_ref() { 885 895 "private_key_jwt" 886 896 if method_supported 887 897 .as_ref() 888 - .is_some_and(|v| v.contains(&CowStr::new_static("private_key_jwt"))) => 898 + .is_some_and(|v| v.iter().any(|s| s.as_str() == "private_key_jwt")) => 889 899 { 890 900 if let Some(keyset) = &keyset { 891 - let mut alg_strs = server_metadata 901 + let mut alg_strs: Vec<&str> = server_metadata 892 902 .token_endpoint_auth_signing_alg_values_supported 893 - .clone() 894 - .unwrap_or(vec![FALLBACK_ALG.into()]); 895 - alg_strs.sort_by(compare_algos); 903 + .as_ref() 904 + .map(|v| v.iter().map(|s| s.as_ref()).collect()) 905 + .unwrap_or_default(); 906 + if alg_strs.is_empty() { 907 + alg_strs.push(FALLBACK_ALG); 908 + } 896 909 let algs: Vec<Signing> = alg_strs 897 910 .iter() 898 911 .filter_map(|s| crate::keyset::parse_signing_alg(s)) 899 912 .collect(); 900 913 let iat = Utc::now().timestamp(); 914 + let client_id_str: &str = client_metadata.client_id.as_ref(); 915 + let issuer_str: &str = server_metadata.issuer.as_ref(); 901 916 return Ok(ClientAuth { 902 917 client_id: client_id.clone(), 903 918 assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)), ··· 906 921 &algs, 907 922 // https://datatracker.ietf.org/doc/html/rfc7523#section-3 908 923 RegisteredClaims { 909 - iss: Some(client_id.clone()), 910 - sub: Some(client_id), 911 - aud: Some(RegisteredClaimsAud::Single( 912 - server_metadata.issuer.clone(), 913 - )), 924 + iss: Some(client_id_str), 925 + sub: Some(client_id_str), 926 + aud: Some(RegisteredClaimsAud::Single(issuer_str)), 914 927 exp: Some(iat + 60), 915 928 // "iat" is required and **MUST** be less than one minute 916 929 // https://datatracker.ietf.org/doc/html/rfc9101 ··· 928 941 "none" 929 942 if method_supported 930 943 .as_ref() 931 - .is_some_and(|v| v.contains(&CowStr::new_static("none"))) => 944 + .is_some_and(|v| v.iter().any(|s| s.as_str() == "none")) => 932 945 { 933 946 return Ok(ClientAuth::new_id(client_id)); 934 947 } ··· 945 958 use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata}; 946 959 use bytes::Bytes; 947 960 use http::{Response as HttpResponse, StatusCode}; 948 - use jacquard_common::{deps::fluent_uri::Uri, http_client::HttpClient, types::string::Did}; 961 + use jacquard_common::{ 962 + bos::BosStr, deps::fluent_uri::Uri, http_client::HttpClient, types::string::Did, 963 + }; 949 964 use jacquard_identity::resolver::IdentityResolver; 965 + use smol_str::SmolStr; 950 966 use std::sync::Arc; 951 967 use tokio::sync::Mutex; 952 968 ··· 976 992 LazyLock::new(|| jacquard_identity::resolver::ResolverOptions::default()); 977 993 &OPTS 978 994 } 979 - async fn resolve_handle( 995 + async fn resolve_handle<S: BosStr + Sync>( 980 996 &self, 981 - _handle: &jacquard_common::types::string::Handle<'_>, 982 - ) -> std::result::Result<Did<'static>, jacquard_identity::resolver::IdentityError> { 997 + _handle: &jacquard_common::types::string::Handle<S>, 998 + ) -> std::result::Result<Did, jacquard_identity::resolver::IdentityError> { 983 999 Ok(Did::new_static("did:plc:alice").unwrap()) 984 1000 } 985 - async fn resolve_did_doc( 1001 + async fn resolve_did_doc<S: BosStr + Sync>( 986 1002 &self, 987 - _did: &Did<'_>, 1003 + _did: &Did<S>, 988 1004 ) -> std::result::Result< 989 1005 jacquard_identity::resolver::DidDocResponse, 990 1006 jacquard_identity::resolver::IdentityError, ··· 1012 1028 1013 1029 fn base_metadata() -> OAuthMetadata { 1014 1030 let mut server = OAuthAuthorizationServerMetadata::default(); 1015 - server.issuer = CowStr::from("https://issuer"); 1016 - server.authorization_endpoint = CowStr::from("https://issuer/authorize"); 1017 - server.token_endpoint = CowStr::from("https://issuer/token"); 1018 - server.token_endpoint_auth_methods_supported = Some(vec![CowStr::from("none")]); 1031 + server.issuer = SmolStr::new_static("https://issuer"); 1032 + server.authorization_endpoint = SmolStr::new_static("https://issuer/authorize"); 1033 + server.token_endpoint = SmolStr::new_static("https://issuer/token"); 1034 + server.token_endpoint_auth_methods_supported = Some(vec![SmolStr::new_static("none")]); 1019 1035 OAuthMetadata { 1020 1036 server_metadata: server, 1021 1037 client_metadata: OAuthClientMetadata { 1022 - client_id: CowStr::new_static("https://client"), 1038 + client_id: SmolStr::new_static("https://client"), 1023 1039 client_uri: None, 1024 - redirect_uris: vec![CowStr::new_static("https://client/cb")], 1025 - scope: Some(CowStr::from("atproto")), 1040 + redirect_uris: vec![SmolStr::new_static("https://client/cb")], 1041 + scope: Some(SmolStr::new_static("atproto")), 1026 1042 grant_types: None, 1027 - response_types: vec![CowStr::new_static("code")], 1028 - application_type: Some(CowStr::new_static("web")), 1029 - token_endpoint_auth_method: Some(CowStr::from("none")), 1043 + response_types: vec![SmolStr::new_static("code")], 1044 + application_type: Some(SmolStr::new_static("web")), 1045 + token_endpoint_auth_method: Some(SmolStr::new_static("none")), 1030 1046 dpop_bound_access_tokens: None, 1031 1047 jwks_uri: None, 1032 1048 jwks: None, ··· 1046 1062 meta.server_metadata.require_pushed_authorization_requests = Some(true); 1047 1063 meta.server_metadata.pushed_authorization_request_endpoint = None; 1048 1064 // require_pushed_authorization_requests is true and no endpoint 1049 - let err = super::par(&MockClient::default(), None, None, &meta, None) 1065 + let err = super::par(&MockClient::default(), None, None, &mut meta, None) 1050 1066 .await 1051 1067 .unwrap_err(); 1052 1068 assert!( ··· 1060 1076 let meta = base_metadata(); 1061 1077 let session = ClientSessionData { 1062 1078 account_did: Did::new_static("did:plc:alice").unwrap(), 1063 - session_id: CowStr::from("state"), 1079 + session_id: SmolStr::new_static("state"), 1064 1080 host_url: Uri::parse("https://pds").expect("valid").to_owned(), 1065 - authserver_url: CowStr::new_static("https://issuer"), 1066 - authserver_token_endpoint: CowStr::from("https://issuer/token"), 1081 + authserver_url: SmolStr::new_static("https://issuer"), 1082 + authserver_token_endpoint: SmolStr::new_static("https://issuer/token"), 1067 1083 authserver_revocation_endpoint: None, 1068 1084 scopes: vec![], 1069 1085 dpop_data: DpopClientData { 1070 - dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(), 1071 - dpop_authserver_nonce: CowStr::from(""), 1072 - dpop_host_nonce: CowStr::from(""), 1086 + dpop_key: crate::utils::generate_key(&[SmolStr::new_static("ES256")]).unwrap(), 1087 + dpop_authserver_nonce: SmolStr::default(), 1088 + dpop_host_nonce: SmolStr::default(), 1073 1089 }, 1074 1090 token_set: crate::types::TokenSet { 1075 - iss: CowStr::from("https://issuer"), 1091 + iss: SmolStr::new_static("https://issuer"), 1076 1092 sub: Did::new_static("did:plc:alice").unwrap(), 1077 - aud: CowStr::from("https://pds"), 1093 + aud: SmolStr::new_static("https://pds"), 1078 1094 scope: None, 1079 1095 refresh_token: None, 1080 - access_token: CowStr::from("abc"), 1096 + access_token: SmolStr::new_static("abc"), 1081 1097 token_type: crate::types::OAuthTokenType::DPoP, 1082 1098 expires_at: None, 1083 1099 }, ··· 1105 1121 ); 1106 1122 let meta = base_metadata(); 1107 1123 let mut dpop = DpopReqData { 1108 - dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(), 1124 + dpop_key: crate::utils::generate_key(&[SmolStr::new_static("ES256")]).unwrap(), 1109 1125 dpop_authserver_nonce: None, 1110 1126 }; 1111 1127 let err = super::exchange_code(&client, &mut dpop, "abc", "verifier", &meta)
+81 -114
crates/jacquard-oauth/src/resolver.rs
··· 3 3 4 4 use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata}; 5 5 use http::{Request, StatusCode}; 6 - use jacquard_common::CowStr; 6 + #[cfg(not(target_arch = "wasm32"))] 7 + use jacquard_common::BosStr; 7 8 use jacquard_common::IntoStatic; 8 - #[allow(unused_imports)] 9 - use jacquard_common::cowstr::ToCowStr; 10 9 use jacquard_common::deps::fluent_uri::Uri; 11 10 use jacquard_common::types::did_doc::DidDocument; 12 11 use jacquard_common::types::ident::AtIdentifier; ··· 96 95 code(jacquard_oauth::resolver::unsupported_did_method), 97 96 help("supported DID methods: did:web, did:plc") 98 97 )] 99 - UnsupportedDidMethod(Did<'static>), 98 + UnsupportedDidMethod(Did), 100 99 101 100 /// HTTP transport error 102 101 #[error("transport error")] ··· 245 244 } 246 245 247 246 /// Create an unsupported DID method error 248 - pub fn unsupported_did_method(did: Did<'static>) -> Self { 247 + pub fn unsupported_did_method(did: Did) -> Self { 249 248 Self::new(ResolverErrorKind::UnsupportedDidMethod(did), None) 250 249 } 251 250 ··· 319 318 // } 320 319 321 320 #[cfg(not(target_arch = "wasm32"))] 322 - async fn verify_issuer_impl<T: OAuthResolver + Sync + ?Sized>( 321 + async fn verify_issuer_impl<S: BosStr, T: OAuthResolver + Sync + ?Sized>( 323 322 resolver: &T, 324 - server_metadata: &OAuthAuthorizationServerMetadata<'_>, 325 - sub: &Did<'_>, 323 + server_metadata: &OAuthAuthorizationServerMetadata, 324 + sub: &Did<S>, 326 325 ) -> Result<Uri<String>> { 327 326 let (metadata, identity) = resolver.resolve_from_identity(sub.as_str()).await?; 328 - if metadata.issuer != server_metadata.issuer { 327 + if metadata.issuer.as_str() != server_metadata.issuer.as_str() { 329 328 return Err(ResolverError::authorization_server_metadata( 330 329 "issuer mismatch", 331 330 )); 332 331 } 333 332 Ok(identity 334 333 .pds_endpoint() 335 - .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))?) 334 + .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))? 335 + .to_owned()) 336 336 } 337 337 338 338 #[cfg(target_arch = "wasm32")] 339 - async fn verify_issuer_impl<T: OAuthResolver + ?Sized>( 339 + async fn verify_issuer_impl<S: BosStr, T: OAuthResolver + ?Sized>( 340 340 resolver: &T, 341 - server_metadata: &OAuthAuthorizationServerMetadata<'_>, 342 - sub: &Did<'_>, 341 + server_metadata: &OAuthAuthorizationServerMetadata, 342 + sub: &Did<S>, 343 343 ) -> Result<Uri<String>> { 344 344 let (metadata, identity) = resolver.resolve_from_identity(sub.as_str()).await?; 345 - if metadata.issuer != server_metadata.issuer { 345 + if metadata.issuer.as_str() != server_metadata.issuer.as_str() { 346 346 return Err(ResolverError::authorization_server_metadata( 347 347 "issuer mismatch", 348 348 )); 349 349 } 350 350 Ok(identity 351 351 .pds_endpoint() 352 - .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))?) 352 + .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))? 353 + .to_owned()) 353 354 } 354 355 355 356 #[cfg(not(target_arch = "wasm32"))] 356 357 async fn resolve_oauth_impl<T: OAuthResolver + Sync + ?Sized>( 357 358 resolver: &T, 358 359 input: &str, 359 - ) -> Result<( 360 - OAuthAuthorizationServerMetadata<'static>, 361 - Option<DidDocument<'static>>, 362 - )> { 360 + ) -> Result<(OAuthAuthorizationServerMetadata, Option<DidDocument>)> { 363 361 // Allow using an entryway, or PDS url, directly as login input (e.g. 364 362 // when the user forgot their handle, or when the handle does not 365 363 // resolve to a DID) ··· 370 368 err.with_context("failed to parse service URL") 371 369 })? 372 370 .to_owned(); 373 - ( 374 - resolver.resolve_from_service(&uri.as_str().into()).await?, 375 - None, 376 - ) 371 + (resolver.resolve_from_service(uri.as_str()).await?, None) 377 372 } else { 378 373 let (metadata, identity) = resolver.resolve_from_identity(input).await?; 379 374 (metadata, Some(identity)) ··· 384 379 async fn resolve_oauth_impl<T: OAuthResolver + ?Sized>( 385 380 resolver: &T, 386 381 input: &str, 387 - ) -> Result<( 388 - OAuthAuthorizationServerMetadata<'static>, 389 - Option<DidDocument<'static>>, 390 - )> { 382 + ) -> Result<(OAuthAuthorizationServerMetadata, Option<DidDocument>)> { 391 383 // Allow using an entryway, or PDS url, directly as login input (e.g. 392 384 // when the user forgot their handle, or when the handle does not 393 385 // resolve to a DID) ··· 398 390 err.with_context("failed to parse service URL") 399 391 })? 400 392 .to_owned(); 401 - ( 402 - resolver.resolve_from_service(&uri.as_str().into()).await?, 403 - None, 404 - ) 393 + (resolver.resolve_from_service(uri.as_str()).await?, None) 405 394 } else { 406 395 let (metadata, identity) = resolver.resolve_from_identity(input).await?; 407 396 (metadata, Some(identity)) ··· 411 400 #[cfg(not(target_arch = "wasm32"))] 412 401 async fn resolve_from_service_impl<T: OAuthResolver + Sync + ?Sized>( 413 402 resolver: &T, 414 - input: &CowStr<'_>, 415 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 403 + input: &str, 404 + ) -> Result<OAuthAuthorizationServerMetadata> { 416 405 // Assume first that input is a PDS URL (as required by ATPROTO) 417 406 if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { 418 407 return Ok(metadata); ··· 424 413 #[cfg(target_arch = "wasm32")] 425 414 async fn resolve_from_service_impl<T: OAuthResolver + ?Sized>( 426 415 resolver: &T, 427 - input: &CowStr<'_>, 428 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 416 + input: &str, 417 + ) -> Result<OAuthAuthorizationServerMetadata> { 429 418 // Assume first that input is a PDS URL (as required by ATPROTO) 430 419 if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { 431 420 return Ok(metadata); ··· 438 427 async fn resolve_from_identity_impl<T: OAuthResolver + Sync + ?Sized>( 439 428 resolver: &T, 440 429 input: &str, 441 - ) -> Result<( 442 - OAuthAuthorizationServerMetadata<'static>, 443 - DidDocument<'static>, 444 - )> { 430 + ) -> Result<(OAuthAuthorizationServerMetadata, DidDocument)> { 445 431 let actor = AtIdentifier::new(input) 446 432 .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; 447 433 let identity = resolver.resolve_ident_owned(&actor).await?; 448 434 if let Some(pds) = &identity.pds_endpoint() { 449 - use jacquard_common::cowstr::ToCowStr; 450 - 451 - let metadata = resolver 452 - .get_resource_server_metadata(&pds.to_cowstr()) 453 - .await?; 435 + let metadata = resolver.get_resource_server_metadata(pds.as_str()).await?; 454 436 Ok((metadata, identity)) 455 437 } else { 456 438 Err(ResolverError::did_document("Did doc lacking pds")) ··· 461 443 async fn resolve_from_identity_impl<T: OAuthResolver + ?Sized>( 462 444 resolver: &T, 463 445 input: &str, 464 - ) -> Result<( 465 - OAuthAuthorizationServerMetadata<'static>, 466 - DidDocument<'static>, 467 - )> { 446 + ) -> Result<(OAuthAuthorizationServerMetadata, DidDocument)> { 468 447 let actor = AtIdentifier::new(input) 469 448 .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; 470 449 let identity = resolver.resolve_ident_owned(&actor).await?; 471 450 if let Some(pds) = &identity.pds_endpoint() { 472 - let metadata = resolver 473 - .get_resource_server_metadata(&pds.to_cowstr()) 474 - .await?; 451 + let metadata = resolver.get_resource_server_metadata(pds.as_str()).await?; 475 452 Ok((metadata, identity)) 476 453 } else { 477 454 Err(ResolverError::did_document("Did doc lacking pds")) ··· 481 458 #[cfg(not(target_arch = "wasm32"))] 482 459 async fn get_authorization_server_metadata_impl<T: HttpClient + Sync + ?Sized>( 483 460 client: &T, 484 - issuer: &CowStr<'_>, 485 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 461 + issuer: &str, 462 + ) -> Result<OAuthAuthorizationServerMetadata> { 486 463 let mut md = resolve_authorization_server(client, issuer).await?; 487 - md.issuer = issuer.clone().into_static(); 464 + md.issuer = SmolStr::from(issuer); 488 465 Ok(md) 489 466 } 490 467 491 468 #[cfg(target_arch = "wasm32")] 492 469 async fn get_authorization_server_metadata_impl<T: HttpClient + ?Sized>( 493 470 client: &T, 494 - issuer: &CowStr<'_>, 495 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 471 + issuer: &str, 472 + ) -> Result<OAuthAuthorizationServerMetadata> { 496 473 let mut md = resolve_authorization_server(client, issuer).await?; 497 - md.issuer = issuer.clone().into_static(); 474 + md.issuer = SmolStr::from(issuer); 498 475 Ok(md) 499 476 } 500 477 501 478 #[cfg(not(target_arch = "wasm32"))] 502 479 async fn get_resource_server_metadata_impl<T: OAuthResolver + Sync + ?Sized>( 503 480 resolver: &T, 504 - pds: &CowStr<'_>, 505 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 481 + pds: &str, 482 + ) -> Result<OAuthAuthorizationServerMetadata> { 506 483 let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; 507 484 // ATPROTO requires one, and only one, authorization server entry 508 485 // > That document MUST contain a single item in the authorization_servers array. ··· 524 501 )); 525 502 } 526 503 }; 527 - let as_metadata = resolver.get_authorization_server_metadata(issuer).await?; 504 + let as_metadata = resolver 505 + .get_authorization_server_metadata(issuer.as_ref()) 506 + .await?; 528 507 // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada 529 508 if let Some(protected_resources) = &as_metadata.protected_resources { 530 509 let resource_url = rs_metadata 531 510 .resource 532 511 .strip_suffix('/') 533 512 .unwrap_or(rs_metadata.resource.as_str()); 534 - if !protected_resources.contains(&CowStr::Borrowed(resource_url)) { 513 + if !protected_resources 514 + .iter() 515 + .any(|s| s.as_str() == resource_url) 516 + { 535 517 return Err(ResolverError::authorization_server_metadata( 536 518 smol_str::format_smolstr!( 537 519 "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}", ··· 559 541 #[cfg(target_arch = "wasm32")] 560 542 async fn get_resource_server_metadata_impl<T: OAuthResolver + ?Sized>( 561 543 resolver: &T, 562 - pds: &CowStr<'_>, 563 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 544 + pds: &str, 545 + ) -> Result<OAuthAuthorizationServerMetadata> { 564 546 let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; 565 547 // ATPROTO requires one, and only one, authorization server entry 566 548 // > That document MUST contain a single item in the authorization_servers array. ··· 582 564 )); 583 565 } 584 566 }; 585 - let as_metadata = resolver.get_authorization_server_metadata(issuer).await?; 567 + let as_metadata = resolver 568 + .get_authorization_server_metadata(issuer.as_ref()) 569 + .await?; 586 570 // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada 587 571 if let Some(protected_resources) = &as_metadata.protected_resources { 588 572 let resource_url = rs_metadata 589 573 .resource 590 574 .strip_suffix('/') 591 575 .unwrap_or(rs_metadata.resource.as_str()); 592 - if !protected_resources.contains(&CowStr::Borrowed(resource_url)) { 576 + if !protected_resources 577 + .iter() 578 + .any(|s| s.as_str() == resource_url) 579 + { 593 580 return Err(ResolverError::authorization_server_metadata( 594 581 smol_str::format_smolstr!( 595 582 "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}", ··· 628 615 pub trait OAuthResolver: IdentityResolver + HttpClient { 629 616 /// Verify that the authorization server in `server_metadata` is the correct issuer for `sub`. 630 617 #[cfg(not(target_arch = "wasm32"))] 631 - fn verify_issuer( 618 + fn verify_issuer<S: BosStr + Sync>( 632 619 &self, 633 - server_metadata: &OAuthAuthorizationServerMetadata<'_>, 634 - sub: &Did<'_>, 620 + server_metadata: &OAuthAuthorizationServerMetadata, 621 + sub: &Did<S>, 635 622 ) -> impl Future<Output = Result<Uri<String>>> + Send 636 623 where 637 624 Self: Sync, ··· 641 628 642 629 /// Verify that the authorization server in `server_metadata` is the correct issuer for `sub`. 643 630 #[cfg(target_arch = "wasm32")] 644 - fn verify_issuer( 631 + fn verify_issuer<S: BosStr>( 645 632 &self, 646 - server_metadata: &OAuthAuthorizationServerMetadata<'_>, 647 - sub: &Did<'_>, 633 + server_metadata: &OAuthAuthorizationServerMetadata, 634 + sub: &Did<S>, 648 635 ) -> impl Future<Output = Result<Uri<String>>> { 649 636 verify_issuer_impl(self, server_metadata, sub) 650 637 } ··· 659 646 fn resolve_oauth( 660 647 &self, 661 648 input: &str, 662 - ) -> impl Future< 663 - Output = Result<( 664 - OAuthAuthorizationServerMetadata<'static>, 665 - Option<DidDocument<'static>>, 666 - )>, 667 - > + Send 649 + ) -> impl Future<Output = Result<(OAuthAuthorizationServerMetadata, Option<DidDocument>)>> + Send 668 650 where 669 651 Self: Sync, 670 652 { ··· 681 663 fn resolve_oauth( 682 664 &self, 683 665 input: &str, 684 - ) -> impl Future< 685 - Output = Result<( 686 - OAuthAuthorizationServerMetadata<'static>, 687 - Option<DidDocument<'static>>, 688 - )>, 689 - > { 666 + ) -> impl Future<Output = Result<(OAuthAuthorizationServerMetadata, Option<DidDocument>)>> { 690 667 resolve_oauth_impl(self, input) 691 668 } 692 669 ··· 697 674 #[cfg(not(target_arch = "wasm32"))] 698 675 fn resolve_from_service( 699 676 &self, 700 - input: &CowStr<'_>, 701 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 677 + input: &str, 678 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> + Send 702 679 where 703 680 Self: Sync, 704 681 { ··· 712 689 #[cfg(target_arch = "wasm32")] 713 690 fn resolve_from_service( 714 691 &self, 715 - input: &CowStr<'_>, 716 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 692 + input: &str, 693 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> { 717 694 resolve_from_service_impl(self, input) 718 695 } 719 696 ··· 722 699 fn resolve_from_identity( 723 700 &self, 724 701 input: &str, 725 - ) -> impl Future< 726 - Output = Result<( 727 - OAuthAuthorizationServerMetadata<'static>, 728 - DidDocument<'static>, 729 - )>, 730 - > + Send 702 + ) -> impl Future<Output = Result<(OAuthAuthorizationServerMetadata, DidDocument)>> + Send 731 703 where 732 704 Self: Sync, 733 705 { ··· 739 711 fn resolve_from_identity( 740 712 &self, 741 713 input: &str, 742 - ) -> impl Future< 743 - Output = Result<( 744 - OAuthAuthorizationServerMetadata<'static>, 745 - DidDocument<'static>, 746 - )>, 747 - > { 714 + ) -> impl Future<Output = Result<(OAuthAuthorizationServerMetadata, DidDocument)>> { 748 715 resolve_from_identity_impl(self, input) 749 716 } 750 717 ··· 755 722 #[cfg(not(target_arch = "wasm32"))] 756 723 fn get_authorization_server_metadata( 757 724 &self, 758 - issuer: &CowStr<'_>, 759 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 725 + issuer: &str, 726 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> + Send 760 727 where 761 728 Self: Sync, 762 729 { ··· 770 737 #[cfg(target_arch = "wasm32")] 771 738 fn get_authorization_server_metadata( 772 739 &self, 773 - issuer: &CowStr<'_>, 774 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 740 + issuer: &str, 741 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> { 775 742 get_authorization_server_metadata_impl(self, issuer) 776 743 } 777 744 ··· 779 746 #[cfg(not(target_arch = "wasm32"))] 780 747 fn get_resource_server_metadata( 781 748 &self, 782 - pds: &CowStr<'_>, 783 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 749 + pds: &str, 750 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> + Send 784 751 where 785 752 Self: Sync, 786 753 { ··· 791 758 #[cfg(target_arch = "wasm32")] 792 759 fn get_resource_server_metadata( 793 760 &self, 794 - pds: &CowStr<'_>, 795 - ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 761 + pds: &str, 762 + ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata>> { 796 763 get_resource_server_metadata_impl(self, pds) 797 764 } 798 765 } ··· 803 770 /// this prevents a compromised server from claiming to be a different issuer. 804 771 pub async fn resolve_authorization_server<T: HttpClient + ?Sized>( 805 772 client: &T, 806 - server: &CowStr<'_>, 807 - ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 773 + server: &str, 774 + ) -> Result<OAuthAuthorizationServerMetadata> { 808 775 let url = format!( 809 776 "{}/.well-known/oauth-authorization-server", 810 777 server.trim_end_matches("/") ··· 821 788 if res.status() == StatusCode::OK { 822 789 let metadata = serde_json::from_slice::<OAuthAuthorizationServerMetadata>(res.body())?; 823 790 // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3 824 - if metadata.issuer == server.as_str() { 791 + if metadata.issuer.as_str() == server { 825 792 Ok(metadata.into_static()) 826 793 } else { 827 794 Err(ResolverError::authorization_server_metadata( ··· 839 806 /// that the metadata belongs to the PDS we queried and not a different resource. 840 807 pub async fn resolve_protected_resource_info<T: HttpClient + ?Sized>( 841 808 client: &T, 842 - server: &CowStr<'_>, 843 - ) -> Result<OAuthProtectedResourceMetadata<'static>> { 809 + server: &str, 810 + ) -> Result<OAuthProtectedResourceMetadata> { 844 811 let url = format!( 845 812 "{}/.well-known/oauth-protected-resource", 846 813 server.trim_end_matches("/") ··· 857 824 if res.status() == StatusCode::OK { 858 825 let metadata = serde_json::from_slice::<OAuthProtectedResourceMetadata>(res.body())?; 859 826 // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3 860 - if metadata.resource == server.as_str() { 827 + if metadata.resource.as_str() == server { 861 828 Ok(metadata.into_static()) 862 829 } else { 863 830 Err(ResolverError::authorization_server_metadata( ··· 878 845 879 846 use super::*; 880 847 use http::{Request as HttpRequest, Response as HttpResponse, StatusCode}; 881 - use jacquard_common::http_client::HttpClient; 848 + use jacquard_common::{CowStr, http_client::HttpClient}; 882 849 use tokio::sync::Mutex; 883 850 884 851 #[derive(Default, Clone)]
+472 -273
crates/jacquard-oauth/src/scopes.rs
··· 21 21 22 22 use std::collections::{BTreeMap, BTreeSet}; 23 23 use std::fmt; 24 + use std::marker::PhantomData; 24 25 use std::str::FromStr; 25 26 27 + use jacquard_common::bos::{BosStr, DefaultStr}; 26 28 use jacquard_common::types::did::Did; 27 29 use jacquard_common::types::nsid::Nsid; 28 30 use jacquard_common::types::string::AtStrError; 29 - use jacquard_common::{CowStr, IntoStatic}; 31 + use jacquard_common::{Bos, FromStaticStr, IntoStatic}; 30 32 use serde::de::Visitor; 31 33 use serde::{Deserialize, Serialize}; 32 - use smol_str::{SmolStr, ToSmolStr}; 34 + use smol_str::{SmolStr, SmolStrBuilder, ToSmolStr, format_smolstr}; 33 35 34 36 /// Represents an AT Protocol OAuth scope 35 37 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 36 - pub enum Scope<'s> { 38 + pub enum Scope<S: BosStr = DefaultStr> { 37 39 /// Account scope for accessing account information 38 40 Account(AccountScope), 39 41 /// Identity scope for accessing identity information 40 42 Identity(IdentityScope), 41 43 /// Blob scope for blob operations with mime type constraints 42 - Blob(BlobScope<'s>), 44 + Blob(BlobScope<S>), 43 45 /// Repository scope for collection operations 44 - Repo(RepoScope<'s>), 46 + Repo(RepoScope<S>), 45 47 /// RPC scope for method access 46 - Rpc(RpcScope<'s>), 48 + Rpc(RpcScope<S>), 47 49 /// AT Protocol scope - required to indicate that other AT Protocol scopes will be used 48 50 Atproto, 49 51 /// Transition scope for migration operations ··· 56 58 Email, 57 59 } 58 60 59 - impl Serialize for Scope<'_> { 60 - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 61 + impl<S: BosStr + Ord> Serialize for Scope<S> { 62 + fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> 61 63 where 62 - S: serde::Serializer, 64 + Ser: serde::Serializer, 63 65 { 64 66 serializer.serialize_str(&self.to_string_normalized()) 65 67 } 66 68 } 67 69 68 - impl<'de> Deserialize<'de> for Scope<'_> { 70 + impl<'de, S> Deserialize<'de> for Scope<S> 71 + where 72 + S: BosStr + Ord + Deserialize<'de> + FromStr, 73 + <S as FromStr>::Err: core::fmt::Debug, 74 + { 69 75 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 70 76 where 71 77 D: serde::Deserializer<'de>, 72 78 { 73 - struct ScopeVisitor; 79 + struct ScopeVisitor<St: BosStr + Ord + FromStr>(PhantomData<St>); 74 80 75 - impl Visitor<'_> for ScopeVisitor { 76 - type Value = Scope<'static>; 81 + impl<St: BosStr + Ord + FromStr> Visitor<'_> for ScopeVisitor<St> 82 + where 83 + <St as FromStr>::Err: core::fmt::Debug, 84 + { 85 + type Value = Scope<St>; 77 86 78 87 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 79 88 write!(formatter, "a scope string") ··· 82 91 where 83 92 E: serde::de::Error, 84 93 { 85 - Scope::parse(v) 86 - .map(|s| s.into_static()) 87 - .map_err(|e| serde::de::Error::custom(format!("{:?}", e))) 94 + Scope::parse(v).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) 88 95 } 89 96 } 90 - deserializer.deserialize_str(ScopeVisitor) 97 + deserializer 98 + .deserialize_str(ScopeVisitor(PhantomData)) 99 + .map(|scope| scope) 91 100 } 92 101 } 93 102 94 - impl IntoStatic for Scope<'_> { 95 - type Output = Scope<'static>; 103 + impl<S: BosStr + Ord + IntoStatic> IntoStatic for Scope<S> 104 + where 105 + S::Output: BosStr + Ord, 106 + { 107 + type Output = Scope<S::Output>; 96 108 97 109 fn into_static(self) -> Self::Output { 98 110 match self { ··· 159 171 160 172 /// Blob scope with mime type constraints 161 173 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 162 - pub struct BlobScope<'s> { 174 + pub struct BlobScope<S: BosStr = DefaultStr> { 163 175 /// Accepted mime types 164 - pub accept: BTreeSet<MimePattern<'s>>, 176 + pub accept: BTreeSet<MimePattern<S>>, 165 177 } 166 178 167 - impl IntoStatic for BlobScope<'_> { 168 - type Output = BlobScope<'static>; 179 + impl<S: BosStr + AsRef<str> + Ord> BlobScope<S> { 180 + /// Convert to a `BlobScope` with a different backing type. 181 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr + Ord>(self) -> BlobScope<B> { 182 + BlobScope { 183 + accept: self.accept.into_iter().map(|p| p.convert()).collect(), 184 + } 185 + } 186 + } 187 + 188 + impl<S: BosStr + IntoStatic> IntoStatic for BlobScope<S> 189 + where 190 + S::Output: BosStr, 191 + MimePattern<S::Output>: Ord, 192 + { 193 + type Output = BlobScope<S::Output>; 169 194 170 195 fn into_static(self) -> Self::Output { 171 196 BlobScope { ··· 176 201 177 202 /// MIME type pattern for blob scope 178 203 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 179 - pub enum MimePattern<'s> { 204 + pub enum MimePattern<S: BosStr = DefaultStr> { 180 205 /// Match all types 181 206 All, 182 207 /// Match all subtypes of a type (e.g., "image/*") 183 - TypeWildcard(CowStr<'s>), 208 + TypeWildcard(S), 184 209 /// Exact mime type match 185 - Exact(CowStr<'s>), 210 + Exact(S), 186 211 } 187 212 188 - impl IntoStatic for MimePattern<'_> { 189 - type Output = MimePattern<'static>; 213 + impl<S: BosStr> MimePattern<S> { 214 + /// Convert to a `MimePattern` with a different backing type. 215 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr>(self) -> MimePattern<B> { 216 + match self { 217 + MimePattern::All => MimePattern::All, 218 + MimePattern::TypeWildcard(s) => MimePattern::TypeWildcard(s.into()), 219 + MimePattern::Exact(s) => MimePattern::Exact(s.into()), 220 + } 221 + } 222 + } 223 + 224 + impl<S: BosStr + IntoStatic> IntoStatic for MimePattern<S> 225 + where 226 + S::Output: BosStr, 227 + { 228 + type Output = MimePattern<S::Output>; 190 229 191 230 fn into_static(self) -> Self::Output { 192 231 match self { ··· 199 238 200 239 /// Repository scope with collection and action constraints 201 240 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 202 - pub struct RepoScope<'s> { 241 + pub struct RepoScope<S: BosStr = DefaultStr> { 203 242 /// Collection NSID or wildcard 204 - pub collection: RepoCollection<'s>, 243 + pub collection: RepoCollection<S>, 205 244 /// Allowed actions 206 245 pub actions: BTreeSet<RepoAction>, 207 246 } 208 247 209 - impl IntoStatic for RepoScope<'_> { 210 - type Output = RepoScope<'static>; 248 + impl<S: BosStr + Ord> RepoScope<S> { 249 + /// Convert to a `RepoScope` with a different backing type. 250 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr + Ord>(self) -> RepoScope<B> { 251 + RepoScope { 252 + collection: self.collection.convert(), 253 + actions: self.actions, 254 + } 255 + } 256 + } 257 + 258 + impl<S: BosStr + IntoStatic> IntoStatic for RepoScope<S> 259 + where 260 + S::Output: BosStr, 261 + { 262 + type Output = RepoScope<S::Output>; 211 263 212 264 fn into_static(self) -> Self::Output { 213 265 RepoScope { ··· 219 271 220 272 /// Repository collection identifier 221 273 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 222 - pub enum RepoCollection<'s> { 274 + pub enum RepoCollection<S: BosStr = DefaultStr> { 223 275 /// All collections (wildcard) 224 276 All, 225 277 /// Specific collection NSID 226 - Nsid(Nsid<'s>), 278 + Nsid(Nsid<S>), 279 + } 280 + 281 + impl<S: BosStr> RepoCollection<S> { 282 + /// Convert to an `Nsid` with a different backing type. 283 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr>(self) -> RepoCollection<B> { 284 + match self { 285 + RepoCollection::All => RepoCollection::All, 286 + RepoCollection::Nsid(nsid) => RepoCollection::Nsid(nsid.convert()), 287 + } 288 + } 227 289 } 228 290 229 - impl IntoStatic for RepoCollection<'_> { 230 - type Output = RepoCollection<'static>; 291 + impl<S: BosStr + IntoStatic> IntoStatic for RepoCollection<S> 292 + where 293 + S::Output: BosStr, 294 + { 295 + type Output = RepoCollection<S::Output>; 231 296 232 297 fn into_static(self) -> Self::Output { 233 298 match self { ··· 250 315 251 316 /// RPC scope with lexicon method and audience constraints 252 317 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 253 - pub struct RpcScope<'s> { 318 + pub struct RpcScope<S: BosStr = DefaultStr> { 254 319 /// Lexicon methods (NSIDs or wildcard) 255 - pub lxm: BTreeSet<RpcLexicon<'s>>, 320 + pub lxm: BTreeSet<RpcLexicon<S>>, 256 321 /// Audiences (DIDs or wildcard) 257 - pub aud: BTreeSet<RpcAudience<'s>>, 322 + pub aud: BTreeSet<RpcAudience<S>>, 323 + } 324 + 325 + impl<S: BosStr + Ord> RpcScope<S> { 326 + /// Convert to a `RpcScope` with a different backing type. 327 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr + Ord>(self) -> RpcScope<B> { 328 + RpcScope { 329 + lxm: self.lxm.into_iter().map(|s| s.convert()).collect(), 330 + aud: self.aud.into_iter().map(|s| s.convert()).collect(), 331 + } 332 + } 258 333 } 259 334 260 - impl IntoStatic for RpcScope<'_> { 261 - type Output = RpcScope<'static>; 335 + impl<S: BosStr + IntoStatic> IntoStatic for RpcScope<S> 336 + where 337 + S::Output: BosStr, 338 + RpcLexicon<S::Output>: Ord, 339 + RpcAudience<S::Output>: Ord, 340 + { 341 + type Output = RpcScope<S::Output>; 262 342 263 343 fn into_static(self) -> Self::Output { 264 344 RpcScope { ··· 270 350 271 351 /// RPC lexicon identifier 272 352 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 273 - pub enum RpcLexicon<'s> { 353 + pub enum RpcLexicon<S: BosStr = DefaultStr> { 274 354 /// All lexicons (wildcard) 275 355 All, 276 356 /// Specific lexicon NSID 277 - Nsid(Nsid<'s>), 357 + Nsid(Nsid<S>), 358 + } 359 + 360 + impl<S: BosStr> RpcLexicon<S> { 361 + /// Convert to an `Nsid` with a different backing type. 362 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr>(self) -> RpcLexicon<B> { 363 + match self { 364 + RpcLexicon::All => RpcLexicon::All, 365 + RpcLexicon::Nsid(nsid) => RpcLexicon::Nsid(nsid.convert()), 366 + } 367 + } 278 368 } 279 369 280 - impl IntoStatic for RpcLexicon<'_> { 281 - type Output = RpcLexicon<'static>; 370 + impl<S: BosStr + IntoStatic> IntoStatic for RpcLexicon<S> 371 + where 372 + S::Output: BosStr, 373 + { 374 + type Output = RpcLexicon<S::Output>; 282 375 283 376 fn into_static(self) -> Self::Output { 284 377 match self { ··· 290 383 291 384 /// RPC audience identifier 292 385 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 293 - pub enum RpcAudience<'s> { 386 + pub enum RpcAudience<S: BosStr = DefaultStr> { 294 387 /// All audiences (wildcard) 295 388 All, 296 389 /// Specific DID 297 - Did(Did<'s>), 390 + Did(Did<S>), 391 + } 392 + 393 + impl<S: BosStr> RpcAudience<S> { 394 + /// Convert to an `Nsid` with a different backing type. 395 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr>(self) -> RpcAudience<B> { 396 + match self { 397 + RpcAudience::All => RpcAudience::All, 398 + RpcAudience::Did(did) => RpcAudience::Did(did.convert()), 399 + } 400 + } 298 401 } 299 402 300 - impl IntoStatic for RpcAudience<'_> { 301 - type Output = RpcAudience<'static>; 403 + impl<S: BosStr + IntoStatic> IntoStatic for RpcAudience<S> 404 + where 405 + S::Output: BosStr, 406 + { 407 + type Output = RpcAudience<S::Output>; 302 408 303 409 fn into_static(self) -> Self::Output { 304 410 match self { ··· 308 414 } 309 415 } 310 416 311 - impl<'s> Scope<'s> { 417 + impl<S: BosStr + Ord> Scope<S> { 418 + /// Convert to a `Scope` with a different backing type. 419 + pub fn convert<B: Bos<str> + From<S> + AsRef<str> + FromStaticStr + Ord>(self) -> Scope<B> { 420 + match self { 421 + Scope::Account(scope) => Scope::Account(scope), 422 + Scope::Identity(scope) => Scope::Identity(scope), 423 + Scope::Blob(scope) => Scope::Blob(scope.convert()), 424 + Scope::Repo(scope) => Scope::Repo(scope.convert()), 425 + Scope::Rpc(scope) => Scope::Rpc(scope.convert()), 426 + Scope::Atproto => Scope::Atproto, 427 + Scope::Transition(scope) => Scope::Transition(scope), 428 + Scope::OpenId => Scope::OpenId, 429 + Scope::Profile => Scope::Profile, 430 + Scope::Email => Scope::Email, 431 + } 432 + } 433 + 312 434 /// Parse multiple space-separated scopes from a string 313 435 /// 314 436 /// # Examples ··· 317 439 /// let scopes = Scope::parse_multiple("atproto repo:*").unwrap(); 318 440 /// assert_eq!(scopes.len(), 2); 319 441 /// ``` 320 - pub fn parse_multiple(s: &'s str) -> Result<Vec<Self>, ParseError> { 442 + pub fn parse_multiple<'a>(s: &'a str) -> Result<Vec<Self>, ParseError> 443 + where 444 + S: FromStr, 445 + <S as FromStr>::Err: core::fmt::Debug, 446 + { 321 447 if s.trim().is_empty() { 322 448 return Ok(Vec::new()); 323 449 } ··· 342 468 /// let scopes = Scope::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*").unwrap(); 343 469 /// assert_eq!(scopes.len(), 2); // atproto and repo:* 344 470 /// ``` 345 - pub fn parse_multiple_reduced(s: &'s str) -> Result<Vec<Self>, ParseError> { 471 + pub fn parse_multiple_reduced<'a>(s: &'a str) -> Result<Vec<Self>, ParseError> 472 + where 473 + S: FromStr, 474 + <S as FromStr>::Err: core::fmt::Debug, 475 + { 346 476 let all_scopes = Self::parse_multiple(s)?; 347 477 348 478 if all_scopes.is_empty() { ··· 403 533 /// let result = Scope::serialize_multiple(&scopes); 404 534 /// assert_eq!(result, "account:email atproto repo:*"); 405 535 /// ``` 406 - pub fn serialize_multiple(scopes: &[Self]) -> CowStr<'static> { 536 + pub fn serialize_multiple(scopes: &[Self]) -> SmolStr { 407 537 if scopes.is_empty() { 408 - return CowStr::default(); 538 + return SmolStr::new_static(""); 409 539 } 410 540 411 - let mut serialized: Vec<String> = scopes 541 + let mut serialized: Vec<SmolStr> = scopes 412 542 .iter() 413 543 .map(|scope| scope.to_string_normalized()) 414 544 .collect(); 415 545 416 546 serialized.sort(); 417 - serialized.join(" ").into() 547 + let mut builder = SmolStrBuilder::new(); 548 + for (i, scope) in serialized.iter().enumerate() { 549 + if i > 0 { 550 + builder.push_str(" "); 551 + } 552 + builder.push_str(scope); 553 + } 554 + builder.finish() 418 555 } 419 556 420 557 /// Remove a scope from a list of scopes ··· 435 572 /// assert_eq!(result.len(), 2); 436 573 /// assert!(!result.contains(&to_remove)); 437 574 /// ``` 438 - pub fn remove_scope(scopes: &[Self], scope_to_remove: &Self) -> Vec<Self> { 575 + pub fn remove_scope(scopes: &[Self], scope_to_remove: &Self) -> Vec<Self> 576 + where 577 + S: Clone, 578 + { 439 579 scopes 440 580 .iter() 441 581 .filter(|s| *s != scope_to_remove) ··· 444 584 } 445 585 446 586 /// Parse a scope from a string 447 - pub fn parse(s: &'s str) -> Result<Self, ParseError> { 587 + pub fn parse<'a>(s: &'a str) -> Result<Self, ParseError> 588 + where 589 + S: FromStr, 590 + <S as FromStr>::Err: core::fmt::Debug, 591 + { 448 592 // Determine the prefix first by checking for known prefixes 449 593 let prefixes = [ 450 594 "account", ··· 500 644 } 501 645 } 502 646 503 - fn parse_account(suffix: Option<&'s str>) -> Result<Self, ParseError> { 647 + fn parse_account(suffix: Option<&str>) -> Result<Self, ParseError> { 504 648 let (resource_str, params) = match suffix { 505 649 Some(s) => { 506 650 if let Some(pos) = s.find('?') { ··· 538 682 Ok(Scope::Account(AccountScope { resource, action })) 539 683 } 540 684 541 - fn parse_identity(suffix: Option<&'s str>) -> Result<Self, ParseError> { 685 + fn parse_identity(suffix: Option<&str>) -> Result<Self, ParseError> { 542 686 let scope = match suffix { 543 687 Some("handle") => IdentityScope::Handle, 544 688 Some("*") => IdentityScope::All, ··· 549 693 Ok(Scope::Identity(scope)) 550 694 } 551 695 552 - fn parse_blob(suffix: Option<&'s str>) -> Result<Self, ParseError> { 553 - let mut accept = BTreeSet::new(); 696 + fn parse_blob<'a>(suffix: Option<&'a str>) -> Result<Self, ParseError> 697 + where 698 + S: FromStr, 699 + <S as FromStr>::Err: core::fmt::Debug, 700 + { 701 + let mut accept: BTreeSet<MimePattern<S>> = BTreeSet::new(); 554 702 555 703 match suffix { 556 704 Some(s) if s.starts_with('?') => { 557 705 let params = parse_query_string(&s[1..]); 558 706 if let Some(values) = params.get("accept") { 559 707 for value in values { 560 - accept.insert(MimePattern::from_str(value)?); 708 + accept.insert(MimePattern::from_str(*value)?); 561 709 } 562 710 } 563 711 } ··· 576 724 Ok(Scope::Blob(BlobScope { accept })) 577 725 } 578 726 579 - fn parse_repo(suffix: Option<&'s str>) -> Result<Self, ParseError> { 727 + fn parse_repo<'a>(suffix: Option<&'a str>) -> Result<Self, ParseError> 728 + where 729 + S: FromStr, 730 + { 580 731 let (collection_str, params) = match suffix { 581 732 Some(s) => { 582 733 if let Some(pos) = s.find('?') { ··· 590 741 591 742 let collection = match collection_str { 592 743 Some("*") | None => RepoCollection::All, 593 - Some(nsid) => RepoCollection::Nsid(Nsid::new(nsid)?), 744 + Some(nsid) => RepoCollection::Nsid(Nsid::from_str(nsid)?), 594 745 }; 595 746 596 747 let mut actions = BTreeSet::new(); ··· 631 782 })) 632 783 } 633 784 634 - fn parse_rpc(suffix: Option<&'s str>) -> Result<Self, ParseError> { 785 + fn parse_rpc<'a>(suffix: Option<&'a str>) -> Result<Self, ParseError> 786 + where 787 + S: FromStr, 788 + { 635 789 let mut lxm = BTreeSet::new(); 636 790 let mut aud = BTreeSet::new(); 637 791 ··· 645 799 646 800 if let Some(values) = params.get("lxm") { 647 801 for value in values { 648 - if value.as_ref() == "*" { 802 + if *value == "*" { 649 803 lxm.insert(RpcLexicon::All); 650 804 } else { 651 - lxm.insert(RpcLexicon::Nsid(Nsid::new(value)?.into_static())); 805 + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(*value)?)); 652 806 } 653 807 } 654 808 } 655 809 656 810 if let Some(values) = params.get("aud") { 657 811 for value in values { 658 - if value.as_ref() == "*" { 812 + if *value == "*" { 659 813 aud.insert(RpcAudience::All); 660 814 } else { 661 - aud.insert(RpcAudience::Did(Did::new(value)?.into_static())); 815 + aud.insert(RpcAudience::Did(Did::from_str(*value)?)); 662 816 } 663 817 } 664 818 } ··· 669 823 let nsid = &s[..pos]; 670 824 let params = parse_query_string(&s[pos + 1..]); 671 825 672 - lxm.insert(RpcLexicon::Nsid(Nsid::new(nsid)?.into_static())); 826 + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(nsid)?)); 673 827 674 828 if let Some(values) = params.get("aud") { 675 829 for value in values { 676 - if value.as_ref() == "*" { 830 + if *value == "*" { 677 831 aud.insert(RpcAudience::All); 678 832 } else { 679 - aud.insert(RpcAudience::Did(Did::new(value)?.into_static())); 833 + aud.insert(RpcAudience::Did(Did::from_str(*value)?)); 680 834 } 681 835 } 682 836 } 683 837 } else { 684 - lxm.insert(RpcLexicon::Nsid(Nsid::new(s)?.into_static())); 838 + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(s)?)); 685 839 } 686 840 } 687 841 None => {} ··· 743 897 } 744 898 Ok(Scope::Email) 745 899 } 900 + } 746 901 902 + impl<S: BosStr + Ord> Scope<S> { 747 903 /// Convert the scope to its normalized string representation 748 - pub fn to_string_normalized(&self) -> String { 904 + pub fn to_string_normalized(&self) -> SmolStr { 749 905 match self { 750 906 Scope::Account(scope) => { 751 907 let resource = match scope.resource { ··· 755 911 }; 756 912 757 913 match scope.action { 758 - AccountAction::Read => format!("account:{}", resource), 759 - AccountAction::Manage => format!("account:{}?action=manage", resource), 914 + AccountAction::Read => format_smolstr!("account:{}", resource), 915 + AccountAction::Manage => format_smolstr!("account:{}?action=manage", resource), 760 916 } 761 917 } 762 918 Scope::Identity(scope) => match scope { 763 - IdentityScope::Handle => "identity:handle".to_string(), 764 - IdentityScope::All => "identity:*".to_string(), 919 + IdentityScope::Handle => "identity:handle".to_smolstr(), 920 + IdentityScope::All => "identity:*".to_smolstr(), 765 921 }, 766 922 Scope::Blob(scope) => { 767 923 if scope.accept.len() == 1 { 768 924 if let Some(pattern) = scope.accept.iter().next() { 769 925 match pattern { 770 - MimePattern::All => "blob:*/*".to_string(), 771 - MimePattern::TypeWildcard(t) => format!("blob:{}/*", t), 772 - MimePattern::Exact(mime) => format!("blob:{}", mime), 926 + MimePattern::All => "blob:*/*".to_smolstr(), 927 + MimePattern::TypeWildcard(t) => { 928 + format_smolstr!("blob:{}/*", t.as_ref()) 929 + } 930 + MimePattern::Exact(mime) => format_smolstr!("blob:{}", mime.as_ref()), 773 931 } 774 932 } else { 775 - "blob:*/*".to_string() 933 + "blob:*/*".to_smolstr() 776 934 } 777 935 } else { 778 936 let mut params = Vec::new(); 779 937 for pattern in &scope.accept { 780 938 match pattern { 781 - MimePattern::All => params.push("accept=*/*".to_string()), 782 - MimePattern::TypeWildcard(t) => params.push(format!("accept={}/*", t)), 783 - MimePattern::Exact(mime) => params.push(format!("accept={}", mime)), 939 + MimePattern::All => params.push("accept=*/*".to_smolstr()), 940 + MimePattern::TypeWildcard(t) => { 941 + params.push(format_smolstr!("accept={}/*", t.as_ref())) 942 + } 943 + MimePattern::Exact(mime) => { 944 + params.push(format_smolstr!("accept={}", mime.as_ref())) 945 + } 784 946 } 785 947 } 786 948 params.sort(); 787 - format!("blob?{}", params.join("&")) 949 + format_smolstr!("blob?{}", params.join("&")) 788 950 } 789 951 } 790 952 Scope::Repo(scope) => { ··· 794 956 }; 795 957 796 958 if scope.actions.len() == 3 { 797 - format!("repo:{}", collection) 959 + format_smolstr!("repo:{}", collection) 798 960 } else { 799 961 let mut params = Vec::new(); 800 962 for action in &scope.actions { ··· 804 966 RepoAction::Delete => params.push("action=delete"), 805 967 } 806 968 } 807 - format!("repo:{}?{}", collection, params.join("&")) 969 + format_smolstr!("repo:{}?{}", collection, params.join("&")) 808 970 } 809 971 } 810 972 Scope::Rpc(scope) => { ··· 813 975 && scope.aud.len() == 1 814 976 && scope.aud.contains(&RpcAudience::All) 815 977 { 816 - "rpc:*".to_string() 978 + "rpc:*".to_smolstr() 817 979 } else if scope.lxm.len() == 1 818 980 && scope.aud.len() == 1 819 981 && scope.aud.contains(&RpcAudience::All) 820 982 { 821 983 if let Some(lxm) = scope.lxm.iter().next() { 822 984 match lxm { 823 - RpcLexicon::All => "rpc:*".to_string(), 824 - RpcLexicon::Nsid(nsid) => format!("rpc:{}", nsid), 985 + RpcLexicon::All => "rpc:*".to_smolstr(), 986 + RpcLexicon::Nsid(nsid) => format_smolstr!("rpc:{}", nsid), 825 987 } 826 988 } else { 827 - "rpc:*".to_string() 989 + "rpc:*".to_smolstr() 828 990 } 829 991 } else { 830 992 let mut params = Vec::new(); 831 993 832 994 for lxm in &scope.lxm { 833 995 match lxm { 834 - RpcLexicon::All => params.push("lxm=*".to_string()), 835 - RpcLexicon::Nsid(nsid) => params.push(format!("lxm={}", nsid)), 996 + RpcLexicon::All => params.push("lxm=*".to_smolstr()), 997 + RpcLexicon::Nsid(nsid) => params.push(format_smolstr!("lxm={}", nsid)), 836 998 } 837 999 } 838 1000 839 1001 for aud in &scope.aud { 840 1002 match aud { 841 - RpcAudience::All => params.push("aud=*".to_string()), 842 - RpcAudience::Did(did) => params.push(format!("aud={}", did)), 1003 + RpcAudience::All => params.push("aud=*".to_smolstr()), 1004 + RpcAudience::Did(did) => params.push(format_smolstr!("aud={}", did)), 843 1005 } 844 1006 } 845 1007 846 1008 params.sort(); 847 1009 848 1010 if params.is_empty() { 849 - "rpc:*".to_string() 1011 + "rpc:*".to_smolstr() 850 1012 } else { 851 - format!("rpc?{}", params.join("&")) 1013 + format_smolstr!("rpc?{}", params.join("&")) 852 1014 } 853 1015 } 854 1016 } 855 - Scope::Atproto => "atproto".to_string(), 1017 + Scope::Atproto => "atproto".to_smolstr(), 856 1018 Scope::Transition(scope) => match scope { 857 - TransitionScope::Generic => "transition:generic".to_string(), 858 - TransitionScope::Email => "transition:email".to_string(), 1019 + TransitionScope::Generic => "transition:generic".to_smolstr(), 1020 + TransitionScope::Email => "transition:email".to_smolstr(), 859 1021 }, 860 - Scope::OpenId => "openid".to_string(), 861 - Scope::Profile => "profile".to_string(), 862 - Scope::Email => "email".to_string(), 1022 + Scope::OpenId => "openid".to_smolstr(), 1023 + Scope::Profile => "profile".to_smolstr(), 1024 + Scope::Email => "email".to_smolstr(), 863 1025 } 864 1026 } 865 1027 866 1028 /// Check if this scope grants the permissions of another scope 867 - pub fn grants(&self, other: &Scope) -> bool { 1029 + pub fn grants<T: BosStr>(&self, other: &Scope<T>) -> bool { 868 1030 match (self, other) { 869 1031 // Atproto only grants itself (it's a required scope, not a permission grant) 870 1032 (Scope::Atproto, Scope::Atproto) => true, ··· 916 1078 let collection_match = match (&a.collection, &b.collection) { 917 1079 (RepoCollection::All, _) => true, 918 1080 (RepoCollection::Nsid(a_nsid), RepoCollection::Nsid(b_nsid)) => { 919 - a_nsid == b_nsid 1081 + // Compare as strings to support cross-type-parameter equality. 1082 + a_nsid.as_ref() == b_nsid.as_ref() 920 1083 } 921 1084 _ => false, 922 1085 }; ··· 928 1091 b.actions.is_subset(&a.actions) || a.actions.len() == 3 929 1092 } 930 1093 (Scope::Rpc(a), Scope::Rpc(b)) => { 931 - let lxm_match = if a.lxm.contains(&RpcLexicon::All) { 1094 + let lxm_match = if a.lxm.iter().any(|l| matches!(l, RpcLexicon::All)) { 932 1095 true 933 1096 } else { 934 1097 b.lxm.iter().all(|b_lxm| match b_lxm { 935 1098 RpcLexicon::All => false, 936 - RpcLexicon::Nsid(_) => a.lxm.contains(b_lxm), 1099 + // Compare as strings to support cross-type-parameter equality. 1100 + RpcLexicon::Nsid(b_nsid) => a.lxm.iter().any(|a_lxm| match a_lxm { 1101 + RpcLexicon::All => false, 1102 + RpcLexicon::Nsid(a_nsid) => a_nsid.as_ref() == b_nsid.as_ref(), 1103 + }), 937 1104 }) 938 1105 }; 939 1106 940 - let aud_match = if a.aud.contains(&RpcAudience::All) { 1107 + let aud_match = if a.aud.iter().any(|a| matches!(a, RpcAudience::All)) { 941 1108 true 942 1109 } else { 943 1110 b.aud.iter().all(|b_aud| match b_aud { 944 1111 RpcAudience::All => false, 945 - RpcAudience::Did(_) => a.aud.contains(b_aud), 1112 + // Compare as strings to support cross-type-parameter equality. 1113 + RpcAudience::Did(b_did) => a.aud.iter().any(|a_aud| match a_aud { 1114 + RpcAudience::All => false, 1115 + RpcAudience::Did(a_did) => a_did.as_ref() == b_did.as_ref(), 1116 + }), 946 1117 }) 947 1118 }; 948 1119 ··· 953 1124 } 954 1125 } 955 1126 956 - impl MimePattern<'_> { 957 - fn grants(&self, other: &MimePattern) -> bool { 1127 + impl<S: BosStr> MimePattern<S> { 1128 + fn grants<T: BosStr>(&self, other: &MimePattern<T>) -> bool { 958 1129 match (self, other) { 959 1130 (MimePattern::All, _) => true, 960 1131 (MimePattern::TypeWildcard(a_type), MimePattern::TypeWildcard(b_type)) => { 961 - a_type == b_type 962 - } 963 - (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => { 964 - b_mime.starts_with(&format!("{}/", a_type)) 1132 + // Compare as strings to support cross-type-parameter equality. 1133 + a_type.as_ref() == b_type.as_ref() 965 1134 } 966 - (MimePattern::Exact(a), MimePattern::Exact(b)) => a == b, 1135 + (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => b_mime 1136 + .as_ref() 1137 + .starts_with(&format!("{}/", a_type.as_ref())), 1138 + (MimePattern::Exact(a), MimePattern::Exact(b)) => a.as_ref() == b.as_ref(), 967 1139 _ => false, 968 1140 } 969 1141 } 970 1142 } 971 1143 972 - impl FromStr for MimePattern<'_> { 1144 + impl<S: BosStr + FromStr> FromStr for MimePattern<S> 1145 + where 1146 + <S as FromStr>::Err: core::fmt::Debug, 1147 + { 973 1148 type Err = ParseError; 974 1149 975 1150 fn from_str(s: &str) -> Result<Self, Self::Err> { 976 1151 if s == "*/*" { 977 1152 Ok(MimePattern::All) 978 1153 } else if let Some(stripped) = s.strip_suffix("/*") { 979 - Ok(MimePattern::TypeWildcard(CowStr::Owned( 980 - stripped.to_smolstr(), 981 - ))) 1154 + Ok(MimePattern::TypeWildcard(S::from_str(stripped).unwrap())) 982 1155 } else if s.contains('/') { 983 - Ok(MimePattern::Exact(CowStr::Owned(s.to_smolstr()))) 1156 + Ok(MimePattern::Exact(S::from_str(s).unwrap())) 984 1157 } else { 985 1158 Err(ParseError::InvalidMimeType(s.to_string())) 986 1159 } 987 1160 } 988 1161 } 989 1162 990 - impl FromStr for Scope<'_> { 991 - type Err = ParseError; 1163 + impl<'a, S: BosStr + From<&'a str>> TryFrom<&'a str> for MimePattern<S> { 1164 + type Error = ParseError; 992 1165 993 - fn from_str(s: &str) -> Result<Scope<'static>, Self::Err> { 994 - match Scope::parse(s) { 995 - Ok(parsed) => Ok(parsed.into_static()), 996 - Err(e) => Err(e), 1166 + fn try_from(s: &'a str) -> Result<Self, Self::Error> { 1167 + if s == "*/*" { 1168 + Ok(MimePattern::All) 1169 + } else if let Some(stripped) = s.strip_suffix("/*") { 1170 + Ok(MimePattern::TypeWildcard(S::from(stripped))) 1171 + } else if s.contains('/') { 1172 + Ok(MimePattern::Exact(S::from(s))) 1173 + } else { 1174 + Err(ParseError::InvalidMimeType(s.to_string())) 997 1175 } 998 1176 } 999 1177 } 1000 1178 1001 - impl fmt::Display for Scope<'_> { 1179 + impl<S: BosStr + Ord> fmt::Display for Scope<S> { 1002 1180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1003 1181 write!(f, "{}", self.to_string_normalized()) 1004 1182 } 1005 1183 } 1006 1184 1007 1185 /// Parse a query string into a map of keys to lists of values 1008 - fn parse_query_string(query: &str) -> BTreeMap<SmolStr, Vec<CowStr<'static>>> { 1186 + fn parse_query_string(query: &str) -> BTreeMap<SmolStr, Vec<&str>> { 1009 1187 let mut params = BTreeMap::new(); 1010 1188 1011 1189 for pair in query.split('&') { ··· 1015 1193 params 1016 1194 .entry(key.to_smolstr()) 1017 1195 .or_insert_with(Vec::new) 1018 - .push(CowStr::Owned(value.to_smolstr())); 1196 + .push(value); 1019 1197 } 1020 1198 } 1021 1199 ··· 1059 1237 1060 1238 #[test] 1061 1239 fn test_account_scope_parsing() { 1062 - let scope = Scope::parse("account:email").unwrap(); 1240 + let scope: Scope = Scope::parse("account:email").unwrap(); 1063 1241 assert_eq!( 1064 1242 scope, 1065 1243 Scope::Account(AccountScope { ··· 1068 1246 }) 1069 1247 ); 1070 1248 1071 - let scope = Scope::parse("account:repo?action=manage").unwrap(); 1249 + let scope: Scope = Scope::parse("account:repo?action=manage").unwrap(); 1072 1250 assert_eq!( 1073 1251 scope, 1074 1252 Scope::Account(AccountScope { ··· 1077 1255 }) 1078 1256 ); 1079 1257 1080 - let scope = Scope::parse("account:status?action=read").unwrap(); 1258 + let scope: Scope = Scope::parse("account:status?action=read").unwrap(); 1081 1259 assert_eq!( 1082 1260 scope, 1083 1261 Scope::Account(AccountScope { ··· 1089 1267 1090 1268 #[test] 1091 1269 fn test_identity_scope_parsing() { 1092 - let scope = Scope::parse("identity:handle").unwrap(); 1270 + let scope: Scope = Scope::parse("identity:handle").unwrap(); 1093 1271 assert_eq!(scope, Scope::Identity(IdentityScope::Handle)); 1094 1272 1095 - let scope = Scope::parse("identity:*").unwrap(); 1273 + let scope: Scope = Scope::parse("identity:*").unwrap(); 1096 1274 assert_eq!(scope, Scope::Identity(IdentityScope::All)); 1097 1275 } 1098 1276 1099 1277 #[test] 1100 1278 fn test_blob_scope_parsing() { 1101 - let scope = Scope::parse("blob:*/*").unwrap(); 1279 + let scope: Scope = Scope::parse("blob:*/*").unwrap(); 1102 1280 let mut accept = BTreeSet::new(); 1103 1281 accept.insert(MimePattern::All); 1104 1282 assert_eq!(scope, Scope::Blob(BlobScope { accept })); 1105 1283 1106 - let scope = Scope::parse("blob:image/png").unwrap(); 1284 + let scope: Scope<SmolStr> = Scope::parse("blob:image/png").unwrap(); 1107 1285 let mut accept = BTreeSet::new(); 1108 - accept.insert(MimePattern::Exact(CowStr::new_static("image/png"))); 1286 + accept.insert(MimePattern::Exact(SmolStr::new_static("image/png"))); 1109 1287 assert_eq!(scope, Scope::Blob(BlobScope { accept })); 1110 1288 1111 1289 let scope = Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(); 1112 1290 let mut accept = BTreeSet::new(); 1113 - accept.insert(MimePattern::Exact(CowStr::new_static("image/png"))); 1114 - accept.insert(MimePattern::Exact(CowStr::new_static("image/jpeg"))); 1291 + accept.insert(MimePattern::Exact(SmolStr::new_static("image/png"))); 1292 + accept.insert(MimePattern::Exact(SmolStr::new_static("image/jpeg"))); 1115 1293 assert_eq!(scope, Scope::Blob(BlobScope { accept })); 1116 1294 1117 1295 let scope = Scope::parse("blob:image/*").unwrap(); 1118 1296 let mut accept = BTreeSet::new(); 1119 - accept.insert(MimePattern::TypeWildcard(CowStr::new_static("image"))); 1297 + accept.insert(MimePattern::TypeWildcard(SmolStr::new_static("image"))); 1120 1298 assert_eq!(scope, Scope::Blob(BlobScope { accept })); 1121 1299 } 1122 1300 1123 1301 #[test] 1124 1302 fn test_repo_scope_parsing() { 1125 - let scope = Scope::parse("repo:*?action=create").unwrap(); 1303 + let scope: Scope<SmolStr> = Scope::parse("repo:*?action=create").unwrap(); 1126 1304 let mut actions = BTreeSet::new(); 1127 1305 actions.insert(RepoAction::Create); 1128 1306 assert_eq!( ··· 1133 1311 }) 1134 1312 ); 1135 1313 1136 - let scope = Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(); 1314 + let scope: Scope = 1315 + Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(); 1137 1316 let mut actions = BTreeSet::new(); 1138 1317 actions.insert(RepoAction::Create); 1139 1318 actions.insert(RepoAction::Update); 1140 1319 assert_eq!( 1141 1320 scope, 1142 1321 Scope::Repo(RepoScope { 1143 - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), 1322 + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), 1144 1323 actions, 1145 1324 }) 1146 1325 ); 1147 1326 1148 - let scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); 1327 + let scope: Scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); 1149 1328 let mut actions = BTreeSet::new(); 1150 1329 actions.insert(RepoAction::Create); 1151 1330 actions.insert(RepoAction::Update); ··· 1153 1332 assert_eq!( 1154 1333 scope, 1155 1334 Scope::Repo(RepoScope { 1156 - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), 1335 + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), 1157 1336 actions, 1158 1337 }) 1159 1338 ); ··· 1161 1340 1162 1341 #[test] 1163 1342 fn test_rpc_scope_parsing() { 1164 - let scope = Scope::parse("rpc:*").unwrap(); 1343 + let scope: Scope = Scope::parse("rpc:*").unwrap(); 1165 1344 let mut lxm = BTreeSet::new(); 1166 1345 let mut aud = BTreeSet::new(); 1167 1346 lxm.insert(RpcLexicon::All); 1168 1347 aud.insert(RpcAudience::All); 1169 1348 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); 1170 1349 1171 - let scope = Scope::parse("rpc:com.example.service").unwrap(); 1350 + let scope: Scope<SmolStr> = Scope::parse("rpc:com.example.service").unwrap(); 1172 1351 let mut lxm = BTreeSet::new(); 1173 1352 let mut aud = BTreeSet::new(); 1174 1353 lxm.insert(RpcLexicon::Nsid( ··· 1177 1356 aud.insert(RpcAudience::All); 1178 1357 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); 1179 1358 1180 - let scope = 1359 + let scope: Scope = 1181 1360 Scope::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(); 1182 1361 let mut lxm = BTreeSet::new(); 1183 1362 let mut aud = BTreeSet::new(); 1184 1363 lxm.insert(RpcLexicon::Nsid( 1185 - Nsid::new_static("com.example.service").unwrap(), 1364 + Nsid::new_owned("com.example.service").unwrap(), 1186 1365 )); 1187 1366 aud.insert(RpcAudience::Did( 1188 - Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), 1367 + Did::new_owned("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), 1189 1368 )); 1190 1369 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); 1191 1370 1192 - let scope = 1371 + let scope: Scope = 1193 1372 Scope::parse("rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g") 1194 1373 .unwrap(); 1195 1374 let mut lxm = BTreeSet::new(); 1196 1375 let mut aud = BTreeSet::new(); 1197 1376 lxm.insert(RpcLexicon::Nsid( 1198 - Nsid::new_static("com.example.method1").unwrap(), 1377 + Nsid::new_owned("com.example.method1").unwrap(), 1199 1378 )); 1200 1379 lxm.insert(RpcLexicon::Nsid( 1201 - Nsid::new_static("com.example.method2").unwrap(), 1380 + Nsid::new_owned("com.example.method2").unwrap(), 1202 1381 )); 1203 1382 aud.insert(RpcAudience::Did( 1204 - Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), 1383 + Did::new_owned("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), 1205 1384 )); 1206 1385 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); 1207 1386 } ··· 1226 1405 ]; 1227 1406 1228 1407 for (input, expected) in tests { 1229 - let scope = Scope::parse(input).unwrap(); 1408 + let scope: Scope = Scope::parse(input).unwrap(); 1230 1409 assert_eq!(scope.to_string_normalized(), expected); 1231 1410 } 1232 1411 } 1233 1412 1234 1413 #[test] 1235 1414 fn test_account_scope_grants() { 1236 - let manage = Scope::parse("account:email?action=manage").unwrap(); 1237 - let read = Scope::parse("account:email?action=read").unwrap(); 1238 - let other_read = Scope::parse("account:repo?action=read").unwrap(); 1415 + let manage: Scope = Scope::parse("account:email?action=manage").unwrap(); 1416 + let read: Scope = Scope::parse("account:email?action=read").unwrap(); 1417 + let other_read: Scope = Scope::parse("account:repo?action=read").unwrap(); 1239 1418 1240 1419 assert!(manage.grants(&read)); 1241 1420 assert!(manage.grants(&manage)); ··· 1246 1425 1247 1426 #[test] 1248 1427 fn test_identity_scope_grants() { 1249 - let all = Scope::parse("identity:*").unwrap(); 1250 - let handle = Scope::parse("identity:handle").unwrap(); 1428 + let all: Scope = Scope::parse("identity:*").unwrap(); 1429 + let handle: Scope = Scope::parse("identity:handle").unwrap(); 1251 1430 1252 1431 assert!(all.grants(&handle)); 1253 1432 assert!(all.grants(&all)); ··· 1257 1436 1258 1437 #[test] 1259 1438 fn test_blob_scope_grants() { 1260 - let all = Scope::parse("blob:*/*").unwrap(); 1261 - let image_all = Scope::parse("blob:image/*").unwrap(); 1262 - let image_png = Scope::parse("blob:image/png").unwrap(); 1263 - let text_plain = Scope::parse("blob:text/plain").unwrap(); 1439 + let all: Scope = Scope::parse("blob:*/*").unwrap(); 1440 + let image_all: Scope = Scope::parse("blob:image/*").unwrap(); 1441 + let image_png: Scope = Scope::parse("blob:image/png").unwrap(); 1442 + let text_plain: Scope = Scope::parse("blob:text/plain").unwrap(); 1264 1443 1265 1444 assert!(all.grants(&image_all)); 1266 1445 assert!(all.grants(&image_png)); ··· 1272 1451 1273 1452 #[test] 1274 1453 fn test_repo_scope_grants() { 1275 - let all_all = Scope::parse("repo:*").unwrap(); 1276 - let all_create = Scope::parse("repo:*?action=create").unwrap(); 1277 - let specific_all = Scope::parse("repo:app.bsky.feed.post").unwrap(); 1278 - let specific_create = Scope::parse("repo:app.bsky.feed.post?action=create").unwrap(); 1279 - let other_create = Scope::parse("repo:pub.leaflet.publication?action=create").unwrap(); 1454 + let all_all: Scope = Scope::parse("repo:*").unwrap(); 1455 + let all_create: Scope = Scope::parse("repo:*?action=create").unwrap(); 1456 + let specific_all: Scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); 1457 + let specific_create: Scope = Scope::parse("repo:app.bsky.feed.post?action=create").unwrap(); 1458 + let other_create: Scope = 1459 + Scope::parse("repo:pub.leaflet.publication?action=create").unwrap(); 1280 1460 1281 1461 assert!(all_all.grants(&all_create)); 1282 1462 assert!(all_all.grants(&specific_all)); ··· 1290 1470 1291 1471 #[test] 1292 1472 fn test_rpc_scope_grants() { 1293 - let all = Scope::parse("rpc:*").unwrap(); 1294 - let specific_lxm = Scope::parse("rpc:com.example.service").unwrap(); 1295 - let specific_both = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(); 1473 + let all: Scope = Scope::parse("rpc:*").unwrap(); 1474 + let specific_lxm: Scope = Scope::parse("rpc:com.example.service").unwrap(); 1475 + let specific_both: Scope = 1476 + Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(); 1296 1477 1297 1478 assert!(all.grants(&specific_lxm)); 1298 1479 assert!(all.grants(&specific_both)); ··· 1303 1484 1304 1485 #[test] 1305 1486 fn test_cross_scope_grants() { 1306 - let account = Scope::parse("account:email").unwrap(); 1307 - let identity = Scope::parse("identity:handle").unwrap(); 1487 + let account: Scope = Scope::parse("account:email").unwrap(); 1488 + let identity: Scope = Scope::parse("identity:handle").unwrap(); 1308 1489 1309 1490 assert!(!account.grants(&identity)); 1310 1491 assert!(!identity.grants(&account)); ··· 1313 1494 #[test] 1314 1495 fn test_parse_errors() { 1315 1496 assert!(matches!( 1316 - Scope::parse("unknown:test"), 1497 + Scope::<SmolStr>::parse("unknown:test"), 1317 1498 Err(ParseError::UnknownPrefix(_)) 1318 1499 )); 1319 1500 1320 1501 assert!(matches!( 1321 - Scope::parse("account"), 1502 + Scope::<SmolStr>::parse("account"), 1322 1503 Err(ParseError::MissingResource) 1323 1504 )); 1324 1505 1325 1506 assert!(matches!( 1326 - Scope::parse("account:invalid"), 1507 + Scope::<SmolStr>::parse("account:invalid"), 1327 1508 Err(ParseError::InvalidResource(_)) 1328 1509 )); 1329 1510 1330 1511 assert!(matches!( 1331 - Scope::parse("account:email?action=invalid"), 1512 + Scope::<SmolStr>::parse("account:email?action=invalid"), 1332 1513 Err(ParseError::InvalidAction(_)) 1333 1514 )); 1334 1515 } 1335 1516 1336 1517 #[test] 1337 1518 fn test_query_parameter_sorting() { 1338 - let scope = 1339 - Scope::parse("blob?accept=image/png&accept=application/pdf&accept=image/jpeg").unwrap(); 1519 + let scope = Scope::<SmolStr>::parse( 1520 + "blob?accept=image/png&accept=application/pdf&accept=image/jpeg", 1521 + ) 1522 + .unwrap(); 1340 1523 let normalized = scope.to_string_normalized(); 1341 1524 assert!(normalized.contains("accept=application/pdf")); 1342 1525 assert!(normalized.contains("accept=image/jpeg")); ··· 1350 1533 1351 1534 #[test] 1352 1535 fn test_repo_action_wildcard() { 1353 - let scope = Scope::parse("repo:app.bsky.feed.post?action=*").unwrap(); 1536 + let scope = Scope::<SmolStr>::parse("repo:app.bsky.feed.post?action=*").unwrap(); 1354 1537 let mut actions = BTreeSet::new(); 1355 1538 actions.insert(RepoAction::Create); 1356 1539 actions.insert(RepoAction::Update); ··· 1358 1541 assert_eq!( 1359 1542 scope, 1360 1543 Scope::Repo(RepoScope { 1361 - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), 1544 + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), 1362 1545 actions, 1363 1546 }) 1364 1547 ); ··· 1366 1549 1367 1550 #[test] 1368 1551 fn test_multiple_blob_accepts() { 1369 - let scope = Scope::parse("blob?accept=image/*&accept=text/plain").unwrap(); 1370 - assert!(scope.grants(&Scope::parse("blob:image/png").unwrap())); 1371 - assert!(scope.grants(&Scope::parse("blob:text/plain").unwrap())); 1372 - assert!(!scope.grants(&Scope::parse("blob:application/json").unwrap())); 1552 + let scope = Scope::<SmolStr>::parse("blob?accept=image/*&accept=text/plain").unwrap(); 1553 + assert!(scope.grants(&Scope::<SmolStr>::parse("blob:image/png").unwrap())); 1554 + assert!(scope.grants(&Scope::<SmolStr>::parse("blob:text/plain").unwrap())); 1555 + assert!(!scope.grants(&Scope::<SmolStr>::parse("blob:application/json").unwrap())); 1373 1556 } 1374 1557 1375 1558 #[test] 1376 1559 fn test_rpc_default_wildcards() { 1377 - let scope = Scope::parse("rpc").unwrap(); 1560 + let scope = Scope::<SmolStr>::parse("rpc").unwrap(); 1378 1561 let mut lxm = BTreeSet::new(); 1379 1562 let mut aud = BTreeSet::new(); 1380 1563 lxm.insert(RpcLexicon::All); ··· 1384 1567 1385 1568 #[test] 1386 1569 fn test_atproto_scope_parsing() { 1387 - let scope = Scope::parse("atproto").unwrap(); 1570 + let scope = Scope::<SmolStr>::parse("atproto").unwrap(); 1388 1571 assert_eq!(scope, Scope::Atproto); 1389 1572 1390 1573 // Atproto should not accept suffixes 1391 - assert!(Scope::parse("atproto:something").is_err()); 1392 - assert!(Scope::parse("atproto?param=value").is_err()); 1574 + assert!(Scope::<SmolStr>::parse("atproto:something").is_err()); 1575 + assert!(Scope::<SmolStr>::parse("atproto?param=value").is_err()); 1393 1576 } 1394 1577 1395 1578 #[test] 1396 1579 fn test_transition_scope_parsing() { 1397 - let scope = Scope::parse("transition:generic").unwrap(); 1580 + let scope = Scope::<SmolStr>::parse("transition:generic").unwrap(); 1398 1581 assert_eq!(scope, Scope::Transition(TransitionScope::Generic)); 1399 1582 1400 - let scope = Scope::parse("transition:email").unwrap(); 1583 + let scope = Scope::<SmolStr>::parse("transition:email").unwrap(); 1401 1584 assert_eq!(scope, Scope::Transition(TransitionScope::Email)); 1402 1585 1403 1586 // Test invalid transition types 1404 1587 assert!(matches!( 1405 - Scope::parse("transition:invalid"), 1588 + Scope::<SmolStr>::parse("transition:invalid"), 1406 1589 Err(ParseError::InvalidResource(_)) 1407 1590 )); 1408 1591 1409 1592 // Test missing suffix 1410 1593 assert!(matches!( 1411 - Scope::parse("transition"), 1594 + Scope::<SmolStr>::parse("transition"), 1412 1595 Err(ParseError::MissingResource) 1413 1596 )); 1414 1597 1415 1598 // Test transition doesn't accept query parameters 1416 1599 assert!(matches!( 1417 - Scope::parse("transition:generic?param=value"), 1600 + Scope::<SmolStr>::parse("transition:generic?param=value"), 1418 1601 Err(ParseError::InvalidResource(_)) 1419 1602 )); 1420 1603 } 1421 1604 1422 1605 #[test] 1423 1606 fn test_atproto_scope_normalization() { 1424 - let scope = Scope::parse("atproto").unwrap(); 1607 + let scope = Scope::<SmolStr>::parse("atproto").unwrap(); 1425 1608 assert_eq!(scope.to_string_normalized(), "atproto"); 1426 1609 } 1427 1610 ··· 1433 1616 ]; 1434 1617 1435 1618 for (input, expected) in tests { 1436 - let scope = Scope::parse(input).unwrap(); 1619 + let scope = Scope::<SmolStr>::parse(input).unwrap(); 1437 1620 assert_eq!(scope.to_string_normalized(), expected); 1438 1621 } 1439 1622 } 1440 1623 1441 1624 #[test] 1442 1625 fn test_atproto_scope_grants() { 1443 - let atproto = Scope::parse("atproto").unwrap(); 1444 - let account = Scope::parse("account:email").unwrap(); 1445 - let identity = Scope::parse("identity:handle").unwrap(); 1446 - let blob = Scope::parse("blob:image/png").unwrap(); 1447 - let repo = Scope::parse("repo:app.bsky.feed.post").unwrap(); 1448 - let rpc = Scope::parse("rpc:com.example.service").unwrap(); 1449 - let transition_generic = Scope::parse("transition:generic").unwrap(); 1450 - let transition_email = Scope::parse("transition:email").unwrap(); 1626 + let atproto = Scope::<SmolStr>::parse("atproto").unwrap(); 1627 + let account = Scope::<SmolStr>::parse("account:email").unwrap(); 1628 + let identity = Scope::<SmolStr>::parse("identity:handle").unwrap(); 1629 + let blob = Scope::<SmolStr>::parse("blob:image/png").unwrap(); 1630 + let repo = Scope::<SmolStr>::parse("repo:app.bsky.feed.post").unwrap(); 1631 + let rpc = Scope::<SmolStr>::parse("rpc:com.example.service").unwrap(); 1632 + let transition_generic = Scope::<SmolStr>::parse("transition:generic").unwrap(); 1633 + let transition_email = Scope::<SmolStr>::parse("transition:email").unwrap(); 1451 1634 1452 1635 // Atproto only grants itself (it's a required scope, not a permission grant) 1453 1636 assert!(atproto.grants(&atproto)); ··· 1471 1654 1472 1655 #[test] 1473 1656 fn test_transition_scope_grants() { 1474 - let transition_generic = Scope::parse("transition:generic").unwrap(); 1475 - let transition_email = Scope::parse("transition:email").unwrap(); 1476 - let account = Scope::parse("account:email").unwrap(); 1657 + let transition_generic = Scope::<SmolStr>::parse("transition:generic").unwrap(); 1658 + let transition_email = Scope::<SmolStr>::parse("transition:email").unwrap(); 1659 + let account = Scope::<SmolStr>::parse("account:email").unwrap(); 1477 1660 1478 1661 // Transition scopes only grant themselves 1479 1662 assert!(transition_generic.grants(&transition_generic)); ··· 1493 1676 #[test] 1494 1677 fn test_parse_multiple() { 1495 1678 // Test parsing multiple scopes 1496 - let scopes = Scope::parse_multiple("atproto repo:*").unwrap(); 1679 + let scopes = Scope::<SmolStr>::parse_multiple("atproto repo:*").unwrap(); 1497 1680 assert_eq!(scopes.len(), 2); 1498 1681 assert_eq!(scopes[0], Scope::Atproto); 1499 1682 assert_eq!( ··· 1511 1694 ); 1512 1695 1513 1696 // Test with more scopes 1514 - let scopes = Scope::parse_multiple("account:email identity:handle blob:image/png").unwrap(); 1697 + let scopes = 1698 + Scope::<SmolStr>::parse_multiple("account:email identity:handle blob:image/png") 1699 + .unwrap(); 1515 1700 assert_eq!(scopes.len(), 3); 1516 1701 assert!(matches!(scopes[0], Scope::Account(_))); 1517 1702 assert!(matches!(scopes[1], Scope::Identity(_))); 1518 1703 assert!(matches!(scopes[2], Scope::Blob(_))); 1519 1704 1520 1705 // Test with complex scopes 1521 - let scopes = Scope::parse_multiple( 1706 + let scopes = Scope::<SmolStr>::parse_multiple( 1522 1707 "account:email?action=manage repo:app.bsky.feed.post?action=create transition:email", 1523 1708 ) 1524 1709 .unwrap(); 1525 1710 assert_eq!(scopes.len(), 3); 1526 1711 1527 1712 // Test empty string 1528 - let scopes = Scope::parse_multiple("").unwrap(); 1713 + let scopes = Scope::<SmolStr>::parse_multiple("").unwrap(); 1529 1714 assert_eq!(scopes.len(), 0); 1530 1715 1531 1716 // Test whitespace only 1532 - let scopes = Scope::parse_multiple(" ").unwrap(); 1717 + let scopes = Scope::<SmolStr>::parse_multiple(" ").unwrap(); 1533 1718 assert_eq!(scopes.len(), 0); 1534 1719 1535 1720 // Test with extra whitespace 1536 - let scopes = Scope::parse_multiple(" atproto repo:* ").unwrap(); 1721 + let scopes = Scope::<SmolStr>::parse_multiple(" atproto repo:* ").unwrap(); 1537 1722 assert_eq!(scopes.len(), 2); 1538 1723 1539 1724 // Test single scope 1540 - let scopes = Scope::parse_multiple("atproto").unwrap(); 1725 + let scopes = Scope::<SmolStr>::parse_multiple("atproto").unwrap(); 1541 1726 assert_eq!(scopes.len(), 1); 1542 1727 assert_eq!(scopes[0], Scope::Atproto); 1543 1728 1544 1729 // Test error propagation 1545 - assert!(Scope::parse_multiple("atproto invalid:scope").is_err()); 1546 - assert!(Scope::parse_multiple("account:invalid repo:*").is_err()); 1730 + assert!(Scope::<SmolStr>::parse_multiple("atproto invalid:scope").is_err()); 1731 + assert!(Scope::<SmolStr>::parse_multiple("account:invalid repo:*").is_err()); 1547 1732 } 1548 1733 1549 1734 #[test] 1550 1735 fn test_parse_multiple_reduced() { 1551 1736 // Test repo scope reduction - wildcard grants specific 1552 1737 let scopes = 1553 - Scope::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*").unwrap(); 1738 + Scope::<SmolStr>::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*") 1739 + .unwrap(); 1554 1740 assert_eq!(scopes.len(), 2); 1555 1741 assert!(scopes.contains(&Scope::Atproto)); 1556 1742 assert!(scopes.contains(&Scope::Repo(RepoScope { ··· 1566 1752 1567 1753 // Test reverse order - should get same result 1568 1754 let scopes = 1569 - Scope::parse_multiple_reduced("atproto repo:* repo:app.bsky.feed.post").unwrap(); 1755 + Scope::<SmolStr>::parse_multiple_reduced("atproto repo:* repo:app.bsky.feed.post") 1756 + .unwrap(); 1570 1757 assert_eq!(scopes.len(), 2); 1571 1758 assert!(scopes.contains(&Scope::Atproto)); 1572 1759 assert!(scopes.contains(&Scope::Repo(RepoScope { ··· 1582 1769 1583 1770 // Test account scope reduction - manage grants read 1584 1771 let scopes = 1585 - Scope::parse_multiple_reduced("account:email account:email?action=manage").unwrap(); 1772 + Scope::<SmolStr>::parse_multiple_reduced("account:email account:email?action=manage") 1773 + .unwrap(); 1586 1774 assert_eq!(scopes.len(), 1); 1587 1775 assert_eq!( 1588 1776 scopes[0], ··· 1593 1781 ); 1594 1782 1595 1783 // Test identity scope reduction - wildcard grants specific 1596 - let scopes = Scope::parse_multiple_reduced("identity:handle identity:*").unwrap(); 1784 + let scopes = 1785 + Scope::<SmolStr>::parse_multiple_reduced("identity:handle identity:*").unwrap(); 1597 1786 assert_eq!(scopes.len(), 1); 1598 1787 assert_eq!(scopes[0], Scope::Identity(IdentityScope::All)); 1599 1788 1600 1789 // Test blob scope reduction - wildcard grants specific 1601 - let scopes = Scope::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*").unwrap(); 1790 + let scopes = 1791 + Scope::<SmolStr>::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*") 1792 + .unwrap(); 1602 1793 assert_eq!(scopes.len(), 1); 1603 1794 let mut accept = BTreeSet::new(); 1604 1795 accept.insert(MimePattern::All); 1605 1796 assert_eq!(scopes[0], Scope::Blob(BlobScope { accept })); 1606 1797 1607 1798 // Test no reduction needed - different scope types 1608 - let scopes = 1609 - Scope::parse_multiple_reduced("account:email identity:handle blob:image/png").unwrap(); 1799 + let scopes = Scope::<SmolStr>::parse_multiple_reduced( 1800 + "account:email identity:handle blob:image/png", 1801 + ) 1802 + .unwrap(); 1610 1803 assert_eq!(scopes.len(), 3); 1611 1804 1612 1805 // Test repo action reduction 1613 - let scopes = Scope::parse_multiple_reduced( 1806 + let scopes = Scope::<SmolStr>::parse_multiple_reduced( 1614 1807 "repo:app.bsky.feed.post?action=create repo:app.bsky.feed.post", 1615 1808 ) 1616 1809 .unwrap(); ··· 1618 1811 assert_eq!( 1619 1812 scopes[0], 1620 1813 Scope::Repo(RepoScope { 1621 - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), 1814 + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), 1622 1815 actions: { 1623 1816 let mut actions = BTreeSet::new(); 1624 1817 actions.insert(RepoAction::Create); ··· 1630 1823 ); 1631 1824 1632 1825 // Test RPC scope reduction 1633 - let scopes = Scope::parse_multiple_reduced( 1826 + let scopes = Scope::<SmolStr>::parse_multiple_reduced( 1634 1827 "rpc:com.example.service?aud=did:example:123 rpc:com.example.service rpc:*", 1635 1828 ) 1636 1829 .unwrap(); ··· 1652 1845 ); 1653 1846 1654 1847 // Test duplicate removal 1655 - let scopes = Scope::parse_multiple_reduced("atproto atproto atproto").unwrap(); 1848 + let scopes = Scope::<SmolStr>::parse_multiple_reduced("atproto atproto atproto").unwrap(); 1656 1849 assert_eq!(scopes.len(), 1); 1657 1850 assert_eq!(scopes[0], Scope::Atproto); 1658 1851 1659 1852 // Test transition scopes - only grant themselves 1660 - let scopes = Scope::parse_multiple_reduced("transition:generic transition:email").unwrap(); 1853 + let scopes = 1854 + Scope::<SmolStr>::parse_multiple_reduced("transition:generic transition:email") 1855 + .unwrap(); 1661 1856 assert_eq!(scopes.len(), 2); 1662 1857 assert!(scopes.contains(&Scope::Transition(TransitionScope::Generic))); 1663 1858 assert!(scopes.contains(&Scope::Transition(TransitionScope::Email))); 1664 1859 1665 1860 // Test empty input 1666 - let scopes = Scope::parse_multiple_reduced("").unwrap(); 1861 + let scopes = Scope::<SmolStr>::parse_multiple_reduced("").unwrap(); 1667 1862 assert_eq!(scopes.len(), 0); 1668 1863 1669 1864 // Test complex scenario with multiple reductions 1670 - let scopes = Scope::parse_multiple_reduced( 1865 + let scopes = Scope::<SmolStr>::parse_multiple_reduced( 1671 1866 "account:email?action=manage account:email account:repo account:repo?action=read identity:* identity:handle" 1672 1867 ).unwrap(); 1673 1868 assert_eq!(scopes.len(), 3); ··· 1683 1878 assert!(scopes.contains(&Scope::Identity(IdentityScope::All))); 1684 1879 1685 1880 // Test that atproto doesn't grant other scopes (per recent change) 1686 - let scopes = Scope::parse_multiple_reduced("atproto account:email repo:*").unwrap(); 1881 + let scopes = 1882 + Scope::<SmolStr>::parse_multiple_reduced("atproto account:email repo:*").unwrap(); 1687 1883 assert_eq!(scopes.len(), 3); 1688 1884 assert!(scopes.contains(&Scope::Atproto)); 1689 1885 assert!(scopes.contains(&Scope::Account(AccountScope { ··· 1705 1901 #[test] 1706 1902 fn test_openid_connect_scope_parsing() { 1707 1903 // Test OpenID scope 1708 - let scope = Scope::parse("openid").unwrap(); 1904 + let scope = Scope::<SmolStr>::parse("openid").unwrap(); 1709 1905 assert_eq!(scope, Scope::OpenId); 1710 1906 1711 1907 // Test Profile scope 1712 - let scope = Scope::parse("profile").unwrap(); 1908 + let scope = Scope::<SmolStr>::parse("profile").unwrap(); 1713 1909 assert_eq!(scope, Scope::Profile); 1714 1910 1715 1911 // Test Email scope 1716 - let scope = Scope::parse("email").unwrap(); 1912 + let scope = Scope::<SmolStr>::parse("email").unwrap(); 1717 1913 assert_eq!(scope, Scope::Email); 1718 1914 1719 1915 // Test that they don't accept suffixes 1720 - assert!(Scope::parse("openid:something").is_err()); 1721 - assert!(Scope::parse("profile:something").is_err()); 1722 - assert!(Scope::parse("email:something").is_err()); 1916 + assert!(Scope::<SmolStr>::parse("openid:something").is_err()); 1917 + assert!(Scope::<SmolStr>::parse("profile:something").is_err()); 1918 + assert!(Scope::<SmolStr>::parse("email:something").is_err()); 1723 1919 1724 1920 // Test that they don't accept query parameters 1725 - assert!(Scope::parse("openid?param=value").is_err()); 1726 - assert!(Scope::parse("profile?param=value").is_err()); 1727 - assert!(Scope::parse("email?param=value").is_err()); 1921 + assert!(Scope::<SmolStr>::parse("openid?param=value").is_err()); 1922 + assert!(Scope::<SmolStr>::parse("profile?param=value").is_err()); 1923 + assert!(Scope::<SmolStr>::parse("email?param=value").is_err()); 1728 1924 } 1729 1925 1730 1926 #[test] 1731 1927 fn test_openid_connect_scope_normalization() { 1732 - let scope = Scope::parse("openid").unwrap(); 1928 + let scope = Scope::<SmolStr>::parse("openid").unwrap(); 1733 1929 assert_eq!(scope.to_string_normalized(), "openid"); 1734 1930 1735 - let scope = Scope::parse("profile").unwrap(); 1931 + let scope = Scope::<SmolStr>::parse("profile").unwrap(); 1736 1932 assert_eq!(scope.to_string_normalized(), "profile"); 1737 1933 1738 - let scope = Scope::parse("email").unwrap(); 1934 + let scope = Scope::<SmolStr>::parse("email").unwrap(); 1739 1935 assert_eq!(scope.to_string_normalized(), "email"); 1740 1936 } 1741 1937 1742 1938 #[test] 1743 1939 fn test_openid_connect_scope_grants() { 1744 - let openid = Scope::parse("openid").unwrap(); 1745 - let profile = Scope::parse("profile").unwrap(); 1746 - let email = Scope::parse("email").unwrap(); 1747 - let account = Scope::parse("account:email").unwrap(); 1940 + let openid = Scope::<SmolStr>::parse("openid").unwrap(); 1941 + let profile = Scope::<SmolStr>::parse("profile").unwrap(); 1942 + let email = Scope::<SmolStr>::parse("email").unwrap(); 1943 + let account = Scope::<SmolStr>::parse("account:email").unwrap(); 1748 1944 1749 1945 // OpenID Connect scopes only grant themselves 1750 1946 assert!(openid.grants(&openid)); ··· 1770 1966 1771 1967 #[test] 1772 1968 fn test_parse_multiple_with_openid_connect() { 1773 - let scopes = Scope::parse_multiple("openid profile email atproto").unwrap(); 1969 + let scopes = Scope::<SmolStr>::parse_multiple("openid profile email atproto").unwrap(); 1774 1970 assert_eq!(scopes.len(), 4); 1775 1971 assert_eq!(scopes[0], Scope::OpenId); 1776 1972 assert_eq!(scopes[1], Scope::Profile); ··· 1778 1974 assert_eq!(scopes[3], Scope::Atproto); 1779 1975 1780 1976 // Test with mixed scopes 1781 - let scopes = Scope::parse_multiple("openid account:email profile repo:*").unwrap(); 1977 + let scopes = 1978 + Scope::<SmolStr>::parse_multiple("openid account:email profile repo:*").unwrap(); 1782 1979 assert_eq!(scopes.len(), 4); 1783 1980 assert!(scopes.contains(&Scope::OpenId)); 1784 1981 assert!(scopes.contains(&Scope::Profile)); ··· 1787 1984 #[test] 1788 1985 fn test_parse_multiple_reduced_with_openid_connect() { 1789 1986 // OpenID Connect scopes don't grant each other, so no reduction 1790 - let scopes = Scope::parse_multiple_reduced("openid profile email openid").unwrap(); 1987 + let scopes = 1988 + Scope::<SmolStr>::parse_multiple_reduced("openid profile email openid").unwrap(); 1791 1989 assert_eq!(scopes.len(), 3); 1792 1990 assert!(scopes.contains(&Scope::OpenId)); 1793 1991 assert!(scopes.contains(&Scope::Profile)); 1794 1992 assert!(scopes.contains(&Scope::Email)); 1795 1993 1796 1994 // Mixed with other scopes 1797 - let scopes = Scope::parse_multiple_reduced( 1995 + let scopes = Scope::<SmolStr>::parse_multiple_reduced( 1798 1996 "openid account:email account:email?action=manage profile", 1799 1997 ) 1800 1998 .unwrap(); ··· 1815 2013 1816 2014 // Test single scope 1817 2015 let scopes = vec![Scope::Atproto]; 1818 - assert_eq!(Scope::serialize_multiple(&scopes), "atproto"); 2016 + assert_eq!(Scope::<SmolStr>::serialize_multiple(&scopes), "atproto"); 1819 2017 1820 2018 // Test multiple scopes - should be sorted alphabetically 1821 2019 let scopes = vec![ 1822 - Scope::parse("repo:*").unwrap(), 2020 + Scope::<SmolStr>::parse("repo:*").unwrap(), 1823 2021 Scope::Atproto, 1824 2022 Scope::parse("account:email").unwrap(), 1825 2023 ]; ··· 1830 2028 1831 2029 // Test that sorting is consistent regardless of input order 1832 2030 let scopes = vec![ 1833 - Scope::parse("identity:handle").unwrap(), 2031 + Scope::<SmolStr>::parse("identity:handle").unwrap(), 1834 2032 Scope::parse("blob:image/png").unwrap(), 1835 2033 Scope::parse("account:repo?action=manage").unwrap(), 1836 2034 ]; ··· 1842 2040 // Test with OpenID Connect scopes 1843 2041 let scopes = vec![Scope::Email, Scope::OpenId, Scope::Profile, Scope::Atproto]; 1844 2042 assert_eq!( 1845 - Scope::serialize_multiple(&scopes), 2043 + Scope::<SmolStr>::serialize_multiple(&scopes), 1846 2044 "atproto email openid profile" 1847 2045 ); 1848 2046 1849 2047 // Test with complex scopes including query parameters 1850 2048 let scopes = vec![ 1851 - Scope::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g&lxm=com.example.method") 2049 + Scope::<SmolStr>::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g&lxm=com.example.method") 1852 2050 .unwrap(), 1853 2051 Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(), 1854 2052 Scope::parse("blob:image/*?accept=image/png&accept=image/jpeg").unwrap(), ··· 1869 2067 Scope::Atproto, 1870 2068 ]; 1871 2069 assert_eq!( 1872 - Scope::serialize_multiple(&scopes), 2070 + Scope::<&str>::serialize_multiple(&scopes), 1873 2071 "atproto transition:email transition:generic" 1874 2072 ); 1875 2073 ··· 1877 2075 let scopes = vec![ 1878 2076 Scope::Atproto, 1879 2077 Scope::Atproto, 1880 - Scope::parse("account:email").unwrap(), 2078 + Scope::<SmolStr>::parse("account:email").unwrap(), 1881 2079 ]; 1882 2080 assert_eq!( 1883 2081 Scope::serialize_multiple(&scopes), ··· 1885 2083 ); 1886 2084 1887 2085 // Test normalization is preserved in serialization 1888 - let scopes = vec![Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap()]; 2086 + let scopes = 2087 + vec![Scope::<SmolStr>::parse("blob?accept=image/png&accept=image/jpeg").unwrap()]; 1889 2088 // Should normalize query parameters alphabetically 1890 2089 assert_eq!( 1891 2090 Scope::serialize_multiple(&scopes), ··· 1897 2096 fn test_serialize_multiple_roundtrip() { 1898 2097 // Test that parse_multiple and serialize_multiple are inverses (when sorted) 1899 2098 let original = "account:email atproto blob:image/png identity:handle repo:*"; 1900 - let scopes = Scope::parse_multiple(original).unwrap(); 2099 + let scopes = Scope::<SmolStr>::parse_multiple(original).unwrap(); 1901 2100 let serialized = Scope::serialize_multiple(&scopes); 1902 2101 assert_eq!(serialized, original); 1903 2102 1904 2103 // Test with complex scopes 1905 2104 let original = "account:repo?action=manage blob?accept=image/jpeg&accept=image/png rpc:*"; 1906 - let scopes = Scope::parse_multiple(original).unwrap(); 2105 + let scopes = Scope::<SmolStr>::parse_multiple(original).unwrap(); 1907 2106 let serialized = Scope::serialize_multiple(&scopes); 1908 2107 // Parse again to verify it's valid 1909 2108 let reparsed = Scope::parse_multiple(&serialized).unwrap(); ··· 1911 2110 1912 2111 // Test with OpenID Connect scopes 1913 2112 let original = "email openid profile"; 1914 - let scopes = Scope::parse_multiple(original).unwrap(); 2113 + let scopes = Scope::<SmolStr>::parse_multiple(original).unwrap(); 1915 2114 let serialized = Scope::serialize_multiple(&scopes); 1916 2115 assert_eq!(serialized, original); 1917 2116 } ··· 1920 2119 fn test_remove_scope() { 1921 2120 // Test removing a scope that exists 1922 2121 let scopes = vec![ 1923 - Scope::parse("repo:*").unwrap(), 2122 + Scope::<SmolStr>::parse("repo:*").unwrap(), 1924 2123 Scope::Atproto, 1925 2124 Scope::parse("account:email").unwrap(), 1926 2125 ]; ··· 1933 2132 1934 2133 // Test removing a scope that doesn't exist 1935 2134 let scopes = vec![ 1936 - Scope::parse("repo:*").unwrap(), 2135 + Scope::<SmolStr>::parse("repo:*").unwrap(), 1937 2136 Scope::parse("account:email").unwrap(), 1938 2137 ]; 1939 2138 let to_remove = Scope::parse("identity:handle").unwrap(); ··· 1950 2149 // Test removing all instances of a duplicate scope 1951 2150 let scopes = vec![ 1952 2151 Scope::Atproto, 1953 - Scope::parse("account:email").unwrap(), 2152 + Scope::<SmolStr>::parse("account:email").unwrap(), 1954 2153 Scope::Atproto, 1955 2154 Scope::parse("repo:*").unwrap(), 1956 2155 Scope::Atproto, ··· 1964 2163 1965 2164 // Test removing complex scopes with query parameters 1966 2165 let scopes = vec![ 1967 - Scope::parse("account:email?action=manage").unwrap(), 2166 + Scope::<SmolStr>::parse("account:email?action=manage").unwrap(), 1968 2167 Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(), 1969 2168 Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(), 1970 2169 ]; ··· 1976 2175 // Test with OpenID Connect scopes 1977 2176 let scopes = vec![Scope::OpenId, Scope::Profile, Scope::Email, Scope::Atproto]; 1978 2177 let to_remove = Scope::Profile; 1979 - let result = Scope::remove_scope(&scopes, &to_remove); 2178 + let result = Scope::<&str>::remove_scope(&scopes, &to_remove); 1980 2179 assert_eq!(result.len(), 3); 1981 2180 assert!(!result.contains(&to_remove)); 1982 2181 assert!(result.contains(&Scope::OpenId)); ··· 1990 2189 Scope::Atproto, 1991 2190 ]; 1992 2191 let to_remove = Scope::Transition(TransitionScope::Email); 1993 - let result = Scope::remove_scope(&scopes, &to_remove); 2192 + let result = Scope::<&str>::remove_scope(&scopes, &to_remove); 1994 2193 assert_eq!(result.len(), 2); 1995 2194 assert!(!result.contains(&to_remove)); 1996 2195 assert!(result.contains(&Scope::Transition(TransitionScope::Generic))); ··· 1998 2197 1999 2198 // Test that only exact matches are removed 2000 2199 let scopes = vec![ 2001 - Scope::parse("account:email").unwrap(), 2200 + Scope::<SmolStr>::parse("account:email").unwrap(), 2002 2201 Scope::parse("account:email?action=manage").unwrap(), 2003 2202 Scope::parse("account:repo").unwrap(), 2004 2203 ];
+149 -117
crates/jacquard-oauth/src/session.rs
··· 1 - use std::sync::Arc; 1 + use std::{str::FromStr, sync::Arc}; 2 2 3 3 use chrono::TimeDelta; 4 4 ··· 15 15 16 16 use dashmap::DashMap; 17 17 use jacquard_common::{ 18 - CowStr, IntoStatic, 18 + IntoStatic, 19 + bos::{BosStr, DefaultStr}, 19 20 deps::fluent_uri::Uri, 20 21 http_client::HttpClient, 21 22 session::SessionStoreError, ··· 37 38 /// Return the private JWK used to sign DPoP proofs. 38 39 fn key(&self) -> &Key; 39 40 /// Return the most recently observed nonce from the authorization server, if any. 40 - fn authserver_nonce(&self) -> Option<CowStr<'_>>; 41 + fn authserver_nonce(&self) -> Option<&str>; 41 42 /// Persist a new nonce received from the authorization server. 42 - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>); 43 + fn set_authserver_nonce(&mut self, nonce: SmolStr); 43 44 /// Return the most recently observed nonce from the resource server (PDS), if any. 44 - fn host_nonce(&self) -> Option<CowStr<'_>>; 45 + fn host_nonce(&self) -> Option<&str>; 45 46 /// Persist a new nonce received from the resource server (PDS). 46 - fn set_host_nonce(&mut self, nonce: CowStr<'_>); 47 + fn set_host_nonce(&mut self, nonce: SmolStr); 47 48 } 48 49 49 50 /// Persisted information about an OAuth session. Used to resume an active session. 50 51 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 51 - pub struct ClientSessionData<'s> { 52 + #[serde(bound( 53 + serialize = "S: serde::Serialize + BosStr + Ord", 54 + deserialize = "S: serde::Deserialize<'de> + BosStr, Scope<S>: serde::Deserialize<'de>" 55 + ))] 56 + pub struct ClientSessionData<S: BosStr = DefaultStr> { 52 57 /// DID of the authenticated account; serves as the primary key for session storage 53 58 /// because only one active session per account is assumed. 54 - #[serde(borrow)] 55 - pub account_did: Did<'s>, 59 + pub account_did: Did<S>, 56 60 57 61 /// Opaque identifier that distinguishes this session from other sessions for the same account. 58 62 /// 59 63 /// Reuses the random `state` token generated during the PAR flow. 60 - pub session_id: CowStr<'s>, 64 + pub session_id: S, 61 65 62 66 /// Base URL of the resource server (PDS): scheme, host, and port only 63 67 pub host_url: Uri<String>, 64 68 65 69 /// Base URL of the authorization server (PDS or entryway): scheme, host, and port only 66 - pub authserver_url: CowStr<'s>, 70 + pub authserver_url: S, 67 71 68 72 /// Full URL of the authorization server's token endpoint. 69 - pub authserver_token_endpoint: CowStr<'s>, 73 + pub authserver_token_endpoint: S, 70 74 71 75 /// Full URL of the authorization server's revocation endpoint, if advertised. 72 76 #[serde(skip_serializing_if = "std::option::Option::is_none")] 73 - pub authserver_revocation_endpoint: Option<CowStr<'s>>, 77 + pub authserver_revocation_endpoint: Option<S>, 74 78 75 79 /// The set of OAuth scopes approved for this session, as returned in the initial token response. 76 - pub scopes: Vec<Scope<'s>>, 80 + pub scopes: Vec<Scope<S>>, 77 81 78 82 /// DPoP key and nonce state for ongoing requests in this session. 79 83 #[serde(flatten)] 80 - pub dpop_data: DpopClientData<'s>, 84 + pub dpop_data: DpopClientData, 81 85 82 86 /// Current token set (access token, refresh token, expiry, etc.). 83 87 #[serde(flatten)] 84 - pub token_set: TokenSet<'s>, 88 + pub token_set: TokenSet<S>, 85 89 } 86 90 87 - impl IntoStatic for ClientSessionData<'_> { 88 - type Output = ClientSessionData<'static>; 91 + impl<S: BosStr + Ord + IntoStatic> IntoStatic for ClientSessionData<S> 92 + where 93 + S::Output: BosStr + Ord, 94 + { 95 + type Output = ClientSessionData<S::Output>; 89 96 90 97 fn into_static(self) -> Self::Output { 91 98 ClientSessionData { ··· 95 102 .authserver_revocation_endpoint 96 103 .map(IntoStatic::into_static), 97 104 scopes: self.scopes.into_static(), 98 - dpop_data: self.dpop_data.into_static(), 105 + dpop_data: self.dpop_data, 99 106 token_set: self.token_set.into_static(), 100 107 account_did: self.account_did.into_static(), 101 108 session_id: self.session_id.into_static(), ··· 104 111 } 105 112 } 106 113 107 - impl ClientSessionData<'_> { 114 + impl<S: BosStr + Ord> ClientSessionData<S> { 108 115 /// Update this session's token set and, if the new token set includes scopes, replace the scope list. 109 116 /// 110 117 /// Called after a successful token refresh so that any scope changes returned by the server 111 118 /// are reflected in the persisted session without requiring a full re-authentication. 112 - pub fn update_with_tokens(&mut self, token_set: TokenSet<'_>) { 119 + /// 120 + /// This method is only available on `DefaultStr`-backed sessions (the common case for 121 + /// in-memory sessions). Zero-copy borrowed sessions are read-only by nature and would 122 + /// not be refreshed in place. 123 + pub fn update_with_tokens(&mut self, token_set: &TokenSet<S>) 124 + where 125 + S: FromStr + Clone, 126 + S::Err: std::fmt::Debug, 127 + { 113 128 if let Some(Ok(scopes)) = token_set 114 129 .scope 115 130 .as_ref() 116 - .map(|scope| Scope::parse_multiple_reduced(&scope).map(IntoStatic::into_static)) 131 + .map(|scope| Scope::<S>::parse_multiple_reduced(scope.as_ref())) 117 132 { 118 - self.scopes = scopes; 133 + self.scopes = scopes.into_iter().map(|s| s.convert()).collect(); 119 134 } 120 - self.token_set = token_set.into_static(); 135 + self.token_set = token_set.clone(); 121 136 } 122 137 } 123 138 ··· 126 141 /// Both nonces must be written back to the store after each request so that the next 127 142 /// request to the same server includes the correct replay-protection nonce. 128 143 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 129 - pub struct DpopClientData<'s> { 144 + pub struct DpopClientData { 130 145 /// The private JWK bound to this session; used to sign all DPoP proofs. 131 146 pub dpop_key: Key, 132 147 /// Most recently observed DPoP nonce from the authorization server. 133 - #[serde(borrow)] 134 - pub dpop_authserver_nonce: CowStr<'s>, 148 + pub dpop_authserver_nonce: SmolStr, 135 149 /// Most recently observed DPoP nonce from the resource server (PDS). 136 - pub dpop_host_nonce: CowStr<'s>, 150 + pub dpop_host_nonce: SmolStr, 137 151 } 138 152 139 - impl IntoStatic for DpopClientData<'_> { 140 - type Output = DpopClientData<'static>; 141 - 142 - fn into_static(self) -> Self::Output { 143 - DpopClientData { 144 - dpop_key: self.dpop_key, 145 - dpop_authserver_nonce: self.dpop_authserver_nonce.into_static(), 146 - dpop_host_nonce: self.dpop_host_nonce.into_static(), 147 - } 148 - } 149 - } 150 - 151 - impl DpopDataSource for DpopClientData<'_> { 153 + impl DpopDataSource for DpopClientData { 152 154 fn key(&self) -> &Key { 153 155 &self.dpop_key 154 156 } 155 - fn authserver_nonce(&self) -> Option<CowStr<'_>> { 156 - Some(self.dpop_authserver_nonce.clone()) 157 + 158 + fn authserver_nonce(&self) -> Option<&str> { 159 + Some(self.dpop_authserver_nonce.as_ref()) 157 160 } 158 161 159 - fn host_nonce(&self) -> Option<CowStr<'_>> { 160 - Some(self.dpop_host_nonce.clone()) 162 + fn host_nonce(&self) -> Option<&str> { 163 + Some(self.dpop_host_nonce.as_ref()) 161 164 } 162 165 163 - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>) { 164 - self.dpop_authserver_nonce = nonce.into_static(); 166 + fn set_authserver_nonce(&mut self, nonce: SmolStr) { 167 + self.dpop_authserver_nonce = nonce; 165 168 } 166 169 167 - fn set_host_nonce(&mut self, nonce: CowStr<'_>) { 168 - self.dpop_host_nonce = nonce.into_static(); 170 + fn set_host_nonce(&mut self, nonce: SmolStr) { 171 + self.dpop_host_nonce = nonce; 169 172 } 170 173 } 171 174 ··· 175 178 /// [`crate::client::OAuthClient::callback`] so that the callback can verify the 176 179 /// `state`, reconstruct the token exchange, and create a full [`ClientSessionData`]. 177 180 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 178 - pub struct AuthRequestData<'s> { 181 + #[serde(bound( 182 + serialize = "S: serde::Serialize + BosStr + Ord", 183 + deserialize = "S: serde::Deserialize<'de> + BosStr, Scope<S>: serde::Deserialize<'de>" 184 + ))] 185 + pub struct AuthRequestData<S: BosStr = DefaultStr> { 179 186 /// Random identifier generated for this authorization request; used as the primary key 180 187 /// for storing and looking up this record during the callback. 181 - #[serde(borrow)] 182 - pub state: CowStr<'s>, 188 + pub state: S, 183 189 184 190 /// Base URL of the authorization server that was selected for this flow. 185 - pub authserver_url: CowStr<'s>, 191 + pub authserver_url: S, 186 192 187 193 /// If the flow was initiated with a DID or handle, the resolved DID is stored here 188 194 /// so it can be compared against the `sub` in the token response. 189 195 #[serde(skip_serializing_if = "std::option::Option::is_none")] 190 - pub account_did: Option<Did<'s>>, 196 + pub account_did: Option<Did<S>>, 191 197 192 198 /// OAuth scopes requested for this authorization. 193 - pub scopes: Vec<Scope<'s>>, 199 + pub scopes: Vec<Scope<S>>, 194 200 195 201 /// The PAR `request_uri` returned by the authorization server; included in the redirect URL. 196 - pub request_uri: CowStr<'s>, 202 + pub request_uri: S, 197 203 198 204 /// Full URL of the authorization server's token endpoint. 199 - pub authserver_token_endpoint: CowStr<'s>, 205 + pub authserver_token_endpoint: S, 200 206 201 207 /// Full URL of the authorization server's revocation endpoint, if advertised. 202 208 #[serde(skip_serializing_if = "std::option::Option::is_none")] 203 - pub authserver_revocation_endpoint: Option<CowStr<'s>>, 209 + pub authserver_revocation_endpoint: Option<S>, 204 210 205 211 /// The PKCE code verifier whose SHA-256 hash was sent as the code challenge; required 206 212 /// at the token exchange step to prove the initiator of the auth request. 207 - pub pkce_verifier: CowStr<'s>, 213 + pub pkce_verifier: S, 208 214 209 215 /// DPoP key and any authserver nonce observed during the PAR request. 210 216 #[serde(flatten)] 211 - pub dpop_data: DpopReqData<'s>, 217 + pub dpop_data: DpopReqData, 212 218 } 213 219 214 - impl IntoStatic for AuthRequestData<'_> { 215 - type Output = AuthRequestData<'static>; 216 - fn into_static(self) -> AuthRequestData<'static> { 220 + impl<S: BosStr + Ord + IntoStatic> IntoStatic for AuthRequestData<S> 221 + where 222 + S::Output: BosStr + Ord, 223 + { 224 + type Output = AuthRequestData<S::Output>; 225 + 226 + fn into_static(self) -> AuthRequestData<S::Output> { 217 227 AuthRequestData { 218 228 request_uri: self.request_uri.into_static(), 219 229 authserver_token_endpoint: self.authserver_token_endpoint.into_static(), ··· 221 231 .authserver_revocation_endpoint 222 232 .map(|s| s.into_static()), 223 233 pkce_verifier: self.pkce_verifier.into_static(), 224 - dpop_data: self.dpop_data.into_static(), 234 + dpop_data: self.dpop_data, 225 235 state: self.state.into_static(), 226 236 authserver_url: self.authserver_url.into_static(), 227 237 account_did: self.account_did.into_static(), ··· 235 245 /// Unlike [`DpopClientData`], this struct only tracks the authserver nonce—no resource-server 236 246 /// nonce is needed until a full session is established. 237 247 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 238 - pub struct DpopReqData<'s> { 248 + pub struct DpopReqData { 239 249 /// The private JWK generated fresh for this authorization request and session. 240 250 pub dpop_key: Key, 241 251 /// DPoP nonce received from the authorization server during the PAR exchange, if any. 242 - #[serde(borrow)] 243 - pub dpop_authserver_nonce: Option<CowStr<'s>>, 244 - } 245 - 246 - impl IntoStatic for DpopReqData<'_> { 247 - type Output = DpopReqData<'static>; 248 - fn into_static(self) -> DpopReqData<'static> { 249 - DpopReqData { 250 - dpop_key: self.dpop_key, 251 - dpop_authserver_nonce: self.dpop_authserver_nonce.into_static(), 252 - } 253 - } 252 + pub dpop_authserver_nonce: Option<SmolStr>, 254 253 } 255 254 256 - impl DpopDataSource for DpopReqData<'_> { 255 + impl DpopDataSource for DpopReqData { 257 256 fn key(&self) -> &Key { 258 257 &self.dpop_key 259 258 } 260 - fn authserver_nonce(&self) -> Option<CowStr<'_>> { 261 - self.dpop_authserver_nonce.clone() 259 + 260 + fn authserver_nonce(&self) -> Option<&str> { 261 + self.dpop_authserver_nonce.as_ref().map(|n| n.as_ref()) 262 262 } 263 263 264 - fn host_nonce(&self) -> Option<CowStr<'_>> { 264 + fn host_nonce(&self) -> Option<&str> { 265 265 None 266 266 } 267 267 268 - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>) { 269 - self.dpop_authserver_nonce = Some(nonce.into_static()); 268 + fn set_authserver_nonce(&mut self, nonce: SmolStr) { 269 + self.dpop_authserver_nonce = Some(nonce); 270 270 } 271 271 272 - fn set_host_nonce(&mut self, _nonce: CowStr<'_>) {} 272 + fn set_host_nonce(&mut self, _nonce: SmolStr) {} 273 273 } 274 274 275 275 /// Static configuration for an OAuth client: the signing keyset and registered client metadata. ··· 277 277 /// `ClientData` is constructed once at startup and shared (via `Arc`) across all sessions 278 278 /// managed by the same [`crate::client::OAuthClient`]. 279 279 #[derive(Clone, Debug)] 280 - pub struct ClientData<'s> { 280 + pub struct ClientData<S> 281 + where 282 + S: BosStr + FromStr + Ord, 283 + <S as FromStr>::Err: core::fmt::Debug, 284 + { 281 285 /// Optional private key set used for `private_key_jwt` client authentication. 282 286 /// When `None`, the `none` authentication method is used instead. 283 287 pub keyset: Option<Keyset>, 284 288 /// AT Protocol-specific client registration metadata (redirect URIs, scopes, etc.). 285 - pub config: AtprotoClientMetadata<'s>, 289 + pub config: AtprotoClientMetadata<S>, 286 290 } 287 291 288 - impl<'s> IntoStatic for ClientData<'s> { 289 - type Output = ClientData<'static>; 290 - fn into_static(self) -> ClientData<'static> { 292 + impl<S> IntoStatic for ClientData<S> 293 + where 294 + S: BosStr + FromStr + Ord + IntoStatic, 295 + S::Output: BosStr + Ord + FromStr, 296 + <S as FromStr>::Err: core::fmt::Debug, 297 + <S::Output as FromStr>::Err: core::fmt::Debug, 298 + { 299 + type Output = ClientData<S::Output>; 300 + fn into_static(self) -> ClientData<S::Output> { 291 301 ClientData { 292 302 keyset: self.keyset, 293 303 config: self.config.into_static(), ··· 295 305 } 296 306 } 297 307 298 - impl<'s> ClientData<'s> { 308 + impl<S: BosStr + FromStr + Ord> ClientData<S> 309 + where 310 + <S as FromStr>::Err: core::fmt::Debug, 311 + { 299 312 /// Create `ClientData` with an optional signing keyset and the given client metadata. 300 - pub fn new(keyset: Option<Keyset>, config: AtprotoClientMetadata<'s>) -> Self { 313 + pub fn new(keyset: Option<Keyset>, config: AtprotoClientMetadata<S>) -> Self { 301 314 Self { keyset, config } 302 315 } 303 316 ··· 305 318 /// 306 319 /// Suitable for public clients (e.g., single-page applications or native apps) that 307 320 /// cannot securely store a private key. 308 - pub fn new_public(config: AtprotoClientMetadata<'s>) -> Self { 321 + pub fn new_public(config: AtprotoClientMetadata<S>) -> Self { 309 322 Self { 310 323 keyset: None, 311 324 config, ··· 318 331 /// `ClientSession` is a convenience type that pairs a [`ClientData`] with a 319 332 /// [`ClientSessionData`] so that methods like `metadata` can access both without requiring 320 333 /// callers to pass them separately. 321 - pub struct ClientSession<'s> { 334 + pub struct ClientSession<S: BosStr = DefaultStr> 335 + where 336 + S: FromStr + Ord, 337 + <S as FromStr>::Err: core::fmt::Debug, 338 + { 322 339 /// Optional signing keyset, forwarded from [`ClientData`]. 323 340 pub keyset: Option<Keyset>, 324 341 /// Client registration metadata, forwarded from [`ClientData`]. 325 - pub config: AtprotoClientMetadata<'s>, 342 + pub config: AtprotoClientMetadata<S>, 326 343 /// The session state for the authenticated account. 327 - pub session_data: ClientSessionData<'s>, 344 + pub session_data: ClientSessionData<S>, 328 345 } 329 346 330 - impl<'s> ClientSession<'s> { 347 + impl<S: BosStr> ClientSession<S> 348 + where 349 + S: FromStr + Ord + Clone, 350 + <S as FromStr>::Err: core::fmt::Debug, 351 + { 331 352 /// Construct a `ClientSession` from a [`ClientData`] and an active session. 332 353 pub fn new( 333 - ClientData { keyset, config }: ClientData<'s>, 334 - session_data: ClientSessionData<'s>, 354 + ClientData { keyset, config }: ClientData<S>, 355 + session_data: ClientSessionData<S>, 335 356 ) -> Self { 336 357 Self { 337 358 keyset, ··· 344 365 pub async fn metadata<T: HttpClient + OAuthResolver + Send + Sync>( 345 366 &self, 346 367 client: &T, 347 - ) -> Result<OAuthMetadata, Error> { 368 + ) -> Result<OAuthMetadata<S>, Error> 369 + where 370 + S: IntoStatic, 371 + { 348 372 Ok(OAuthMetadata { 349 373 server_metadata: client 350 - .get_authorization_server_metadata(&self.session_data.authserver_url) 374 + .get_authorization_server_metadata(self.session_data.authserver_url.as_ref()) 351 375 .await 352 376 .map_err(|e| Error::ServerAgent(crate::request::RequestError::resolver(e)))?, 353 - client_metadata: atproto_client_metadata(self.config.clone(), &self.keyset) 354 - .unwrap() 355 - .into_static(), 377 + client_metadata: atproto_client_metadata(&self.config, &self.keyset).unwrap(), 356 378 keyset: self.keyset.clone(), 357 379 }) 358 380 } ··· 403 425 /// concurrent refresh attempts for the same `(DID, session_id)` pair are coalesced behind 404 426 /// a per-key `Mutex` stored in `pending`, so only one refresh request is issued to the 405 427 /// authorization server even when many concurrent requests detect an expired token. 406 - pub struct SessionRegistry<T, S> 428 + pub struct SessionRegistry<T, S, Str> 407 429 where 408 430 T: OAuthResolver, 409 431 S: ClientAuthStore, 432 + Str: BosStr + FromStr + Ord, 433 + <Str as FromStr>::Err: core::fmt::Debug, 410 434 { 411 435 /// Backing store for persisting session data across process restarts. 412 436 pub store: Arc<S>, 413 437 /// Shared resolver used to fetch authorization server metadata during refresh. 414 438 pub client: Arc<T>, 415 439 /// Static client configuration (keyset and registration metadata). 416 - pub client_data: ClientData<'static>, 440 + pub client_data: ClientData<Str>, 417 441 /// Per-`(DID, session_id)` mutex that serializes concurrent refresh attempts. 418 442 pending: DashMap<SmolStr, Arc<Mutex<()>>>, 419 443 } 420 444 421 - impl<T, S> SessionRegistry<T, S> 445 + impl<T, S, Str> SessionRegistry<T, S, Str> 422 446 where 423 447 S: ClientAuthStore, 424 448 T: OAuthResolver, 449 + Str: BosStr + FromStr + Ord, 450 + <Str as FromStr>::Err: core::fmt::Debug, 425 451 { 426 452 /// Create a new registry, taking ownership of the store. 427 - pub fn new(store: S, client: Arc<T>, client_data: ClientData<'static>) -> Self { 453 + pub fn new(store: S, client: Arc<T>, client_data: ClientData<Str>) -> Self { 428 454 let store = Arc::new(store); 429 455 Self { 430 456 store: Arc::clone(&store), ··· 438 464 /// 439 465 /// Use this variant when the store needs to be accessed from outside the registry, 440 466 /// for example to expose session listing or administration functionality. 441 - pub fn new_shared(store: Arc<S>, client: Arc<T>, client_data: ClientData<'static>) -> Self { 467 + pub fn new_shared(store: Arc<S>, client: Arc<T>, client_data: ClientData<Str>) -> Self { 442 468 Self { 443 469 store, 444 470 client, ··· 448 474 } 449 475 } 450 476 451 - impl<T, S> SessionRegistry<T, S> 477 + impl<T, S, Str> SessionRegistry<T, S, Str> 452 478 where 453 479 S: ClientAuthStore + Send + Sync + 'static, 454 480 T: OAuthResolver + DpopExt + Send + Sync + 'static, 481 + Str: BosStr + FromStr + Ord + Clone, 482 + <Str as FromStr>::Err: core::fmt::Debug, 455 483 { 456 - async fn get_refreshed( 484 + async fn get_refreshed<D: BosStr + Send + Sync>( 457 485 &self, 458 - did: &Did<'_>, 486 + did: &Did<D>, 459 487 session_id: &str, 460 - ) -> Result<ClientSessionData<'_>, Error> { 488 + ) -> Result<ClientSessionData, Error> { 461 489 let key = format_smolstr!("{}_{}", did, session_id); 462 490 let lock = self 463 491 .pending ··· 506 534 /// When `refresh` is `true`, proactively 507 535 /// renews the token if it is within 60 seconds of expiry. When `false`, returns the session 508 536 /// data as-is without contacting the authorization server. 509 - pub async fn get( 537 + pub async fn get<D: BosStr + Send + Sync>( 510 538 &self, 511 - did: &Did<'_>, 539 + did: &Did<D>, 512 540 session_id: &str, 513 541 refresh: bool, 514 - ) -> Result<ClientSessionData<'_>, Error> { 542 + ) -> Result<ClientSessionData, Error> { 515 543 if refresh { 516 544 self.get_refreshed(did, session_id).await 517 545 } else { ··· 523 551 } 524 552 } 525 553 /// Persist an updated session to the backing store. 526 - pub async fn set(&self, value: ClientSessionData<'_>) -> Result<(), Error> { 554 + pub async fn set(&self, value: ClientSessionData) -> Result<(), Error> { 527 555 self.store.upsert_session(value).await?; 528 556 Ok(()) 529 557 } 530 558 /// Delete a session from the backing store. 531 - pub async fn del(&self, did: &Did<'_>, session_id: &str) -> Result<(), Error> { 559 + pub async fn del<D: BosStr + Send + Sync>( 560 + &self, 561 + did: &Did<D>, 562 + session_id: &str, 563 + ) -> Result<(), Error> { 532 564 self.store.delete_session(did, session_id).await?; 533 565 Ok(()) 534 566 }
+37 -20
crates/jacquard-oauth/src/types.rs
··· 13 13 pub use self::token::*; 14 14 use jacquard_common::CowStr; 15 15 use jacquard_common::IntoStatic; 16 + use jacquard_common::bos::{BosStr, DefaultStr}; 16 17 use jacquard_common::deps::fluent_uri::Uri; 17 18 use serde::Deserialize; 19 + use smol_str::SmolStr; 18 20 19 21 /// The `prompt` parameter for an OAuth authorization request. 20 22 /// ··· 34 36 35 37 impl From<AuthorizeOptionPrompt> for CowStr<'static> { 36 38 fn from(value: AuthorizeOptionPrompt) -> Self { 39 + CowStr::new_static(value.into()) 40 + } 41 + } 42 + 43 + impl From<AuthorizeOptionPrompt> for SmolStr { 44 + fn from(value: AuthorizeOptionPrompt) -> Self { 45 + SmolStr::new_static(value.into()) 46 + } 47 + } 48 + 49 + impl From<AuthorizeOptionPrompt> for &'static str { 50 + fn from(value: AuthorizeOptionPrompt) -> Self { 37 51 match value { 38 - AuthorizeOptionPrompt::Login => CowStr::new_static("login"), 39 - AuthorizeOptionPrompt::None => CowStr::new_static("none"), 40 - AuthorizeOptionPrompt::Consent => CowStr::new_static("consent"), 41 - AuthorizeOptionPrompt::SelectAccount => CowStr::new_static("select_account"), 52 + AuthorizeOptionPrompt::Login => "login", 53 + AuthorizeOptionPrompt::None => "none", 54 + AuthorizeOptionPrompt::Consent => "consent", 55 + AuthorizeOptionPrompt::SelectAccount => "select_account", 42 56 } 43 57 } 44 58 } 45 59 46 60 /// Options for initiating an OAuth authorization request. 47 61 #[derive(Debug)] 48 - pub struct AuthorizeOptions<'s> { 62 + pub struct AuthorizeOptions<S: BosStr = DefaultStr> { 49 63 /// Override the redirect URI registered in the client metadata. 50 64 pub redirect_uri: Option<Uri<String>>, 51 65 /// Scopes to request. Defaults to an empty list (server-defined defaults apply). 52 - pub scopes: Vec<Scope<'s>>, 66 + pub scopes: Vec<Scope<S>>, 53 67 /// Optional prompt hint for the authorization server's UI. 54 68 pub prompt: Option<AuthorizeOptionPrompt>, 55 69 /// Opaque client-provided state value, echoed back in the callback for CSRF protection. 56 - pub state: Option<CowStr<'s>>, 70 + pub state: Option<S>, 57 71 } 58 72 59 - impl Default for AuthorizeOptions<'_> { 73 + impl<S: BosStr> Default for AuthorizeOptions<S> { 60 74 fn default() -> Self { 61 75 Self { 62 76 redirect_uri: None, ··· 67 81 } 68 82 } 69 83 70 - impl<'s> AuthorizeOptions<'s> { 84 + impl<S: BosStr> AuthorizeOptions<S> { 71 85 /// Set the `prompt` parameter sent to the authorization server. 72 86 pub fn with_prompt(mut self, prompt: AuthorizeOptionPrompt) -> Self { 73 87 self.prompt = Some(prompt); ··· 75 89 } 76 90 77 91 /// Set a CSRF-protection `state` value to be echoed in the callback. 78 - pub fn with_state(mut self, state: CowStr<'s>) -> Self { 92 + pub fn with_state(mut self, state: S) -> Self { 79 93 self.state = Some(state); 80 94 self 81 95 } ··· 87 101 } 88 102 89 103 /// Set the OAuth scopes to request. 90 - pub fn with_scopes(mut self, scopes: Vec<Scope<'s>>) -> Self { 104 + pub fn with_scopes(mut self, scopes: Vec<Scope<S>>) -> Self { 91 105 self.scopes = scopes; 92 106 self 93 107 } ··· 95 109 96 110 /// Query parameters delivered to the OAuth redirect URI after user authorization. 97 111 #[derive(Debug, Deserialize)] 98 - pub struct CallbackParams<'s> { 112 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 113 + pub struct CallbackParams<S: BosStr = DefaultStr> { 99 114 /// The authorization code issued by the authorization server. 100 - #[serde(borrow)] 101 - pub code: CowStr<'s>, 115 + pub code: S, 102 116 /// The `state` value originally sent in the authorization request, used to 103 117 /// verify the response belongs to this session. 104 - pub state: Option<CowStr<'s>>, 118 + pub state: Option<S>, 105 119 /// The `iss` (issuer) parameter, required by RFC 9207 to prevent mix-up attacks. 106 - pub iss: Option<CowStr<'s>>, 120 + pub iss: Option<S>, 107 121 } 108 122 109 - impl IntoStatic for CallbackParams<'_> { 110 - type Output = CallbackParams<'static>; 123 + impl<S: BosStr + IntoStatic> IntoStatic for CallbackParams<S> 124 + where 125 + S::Output: BosStr, 126 + { 127 + type Output = CallbackParams<S::Output>; 111 128 112 129 fn into_static(self) -> Self::Output { 113 130 CallbackParams { 114 131 code: self.code.into_static(), 115 - state: self.state.map(|s| s.into_static()), 116 - iss: self.iss.map(|s| s.into_static()), 132 + state: self.state.into_static(), 133 + iss: self.iss.into_static(), 117 134 } 118 135 } 119 136 }
+30 -31
crates/jacquard-oauth/src/types/client_metadata.rs
··· 1 - use jacquard_common::{CowStr, IntoStatic}; 1 + use jacquard_common::IntoStatic; 2 + use jacquard_common::bos::{BosStr, DefaultStr}; 2 3 use jose_jwk::JwkSet; 3 4 use serde::{Deserialize, Serialize}; 4 - use smol_str::SmolStr; 5 5 6 6 /// OAuth 2.1 client metadata, used in the ATProto client ID metadata document. 7 7 /// ··· 11 11 /// 12 12 /// <https://atproto.com/specs/oauth> 13 13 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 14 - pub struct OAuthClientMetadata<'c> { 14 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 15 + pub struct OAuthClientMetadata<S: BosStr = DefaultStr> { 15 16 /// The client identifier, typically a URL pointing to this metadata document. 16 - pub client_id: CowStr<'c>, 17 + pub client_id: S, 17 18 /// URL of the client's home page, used for display purposes. 18 19 #[serde(skip_serializing_if = "Option::is_none")] 19 - pub client_uri: Option<CowStr<'c>>, 20 + pub client_uri: Option<S>, 20 21 /// List of redirect URIs the authorization server may send callbacks to. 21 - pub redirect_uris: Vec<CowStr<'c>>, 22 + pub redirect_uris: Vec<S>, 22 23 /// Space-separated list of scopes the client is allowed to request. 23 24 #[serde(skip_serializing_if = "Option::is_none")] 24 - #[serde(borrow)] 25 - pub scope: Option<CowStr<'c>>, 25 + pub scope: Option<S>, 26 26 /// Application type (`web` or `native`), used to enforce redirect URI constraints. 27 27 #[serde(skip_serializing_if = "Option::is_none")] 28 - pub application_type: Option<CowStr<'c>>, 28 + pub application_type: Option<S>, 29 29 /// OAuth 2.0 grant types the client will use. 30 30 #[serde(skip_serializing_if = "Option::is_none")] 31 - pub grant_types: Option<Vec<CowStr<'c>>>, 31 + pub grant_types: Option<Vec<S>>, 32 32 /// Authentication method the client uses at the token endpoint. 33 33 #[serde(skip_serializing_if = "Option::is_none")] 34 - pub token_endpoint_auth_method: Option<CowStr<'c>>, 34 + pub token_endpoint_auth_method: Option<S>, 35 35 /// Response types the client will use in authorization requests. 36 - pub response_types: Vec<CowStr<'c>>, 36 + pub response_types: Vec<S>, 37 37 /// If `true`, the client requires DPoP-bound access tokens (RFC 9449 §5.2). 38 38 /// 39 39 /// <https://datatracker.ietf.org/doc/html/rfc9449#section-5.2> ··· 43 43 /// 44 44 /// <https://datatracker.ietf.org/doc/html/rfc7591#section-2> 45 45 #[serde(skip_serializing_if = "Option::is_none")] 46 - pub jwks_uri: Option<CowStr<'c>>, 46 + pub jwks_uri: Option<S>, 47 47 /// Inline JWK Set for verifying signed requests, alternative to `jwks_uri`. 48 48 #[serde(skip_serializing_if = "Option::is_none")] 49 49 pub jwks: Option<JwkSet>, ··· 51 51 /// 52 52 /// <https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata> 53 53 #[serde(skip_serializing_if = "Option::is_none")] 54 - pub token_endpoint_auth_signing_alg: Option<CowStr<'c>>, 54 + pub token_endpoint_auth_signing_alg: Option<S>, 55 55 /// Human-readable name of the client, shown to users during authorization. 56 56 #[serde(skip_serializing_if = "Option::is_none")] 57 - pub client_name: Option<SmolStr>, 57 + pub client_name: Option<S>, 58 58 /// URL of the client's logo image. 59 59 #[serde(skip_serializing_if = "Option::is_none")] 60 - pub logo_uri: Option<CowStr<'c>>, 60 + pub logo_uri: Option<S>, 61 61 /// URL of the client's terms of service. 62 62 #[serde(skip_serializing_if = "Option::is_none")] 63 - pub tos_uri: Option<CowStr<'c>>, 63 + pub tos_uri: Option<S>, 64 64 /// URL of the client's privacy policy. 65 65 #[serde(skip_serializing_if = "Option::is_none")] 66 - pub privacy_policy_uri: Option<CowStr<'c>>, 66 + pub privacy_policy_uri: Option<S>, 67 67 } 68 68 69 - impl OAuthClientMetadata<'_> {} 69 + impl<S: BosStr> OAuthClientMetadata<S> {} 70 70 71 - impl IntoStatic for OAuthClientMetadata<'_> { 72 - type Output = OAuthClientMetadata<'static>; 71 + impl<S: BosStr + IntoStatic> IntoStatic for OAuthClientMetadata<S> 72 + where 73 + S::Output: BosStr, 74 + { 75 + type Output = OAuthClientMetadata<S::Output>; 73 76 74 77 fn into_static(self) -> Self::Output { 75 78 OAuthClientMetadata { 76 79 client_id: self.client_id.into_static(), 77 80 client_uri: self.client_uri.into_static(), 78 81 redirect_uris: self.redirect_uris.into_static(), 79 - scope: self.scope.map(|scope| scope.into_static()), 80 - application_type: self.application_type.map(|app_type| app_type.into_static()), 81 - grant_types: self.grant_types.map(|types| types.into_static()), 82 + scope: self.scope.into_static(), 83 + application_type: self.application_type.into_static(), 84 + grant_types: self.grant_types.into_static(), 82 85 response_types: self.response_types.into_static(), 83 - token_endpoint_auth_method: self 84 - .token_endpoint_auth_method 85 - .map(|method| method.into_static()), 86 + token_endpoint_auth_method: self.token_endpoint_auth_method.into_static(), 86 87 dpop_bound_access_tokens: self.dpop_bound_access_tokens, 87 88 jwks_uri: self.jwks_uri.into_static(), 88 89 jwks: self.jwks, 89 - token_endpoint_auth_signing_alg: self 90 - .token_endpoint_auth_signing_alg 91 - .map(|alg| alg.into_static()), 92 - client_name: self.client_name, 90 + token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg.into_static(), 91 + client_name: self.client_name.into_static(), 93 92 logo_uri: self.logo_uri.into_static(), 94 93 tos_uri: self.tos_uri.into_static(), 95 94 privacy_policy_uri: self.privacy_policy_uri.into_static(),
+115 -60
crates/jacquard-oauth/src/types/metadata.rs
··· 1 - use jacquard_common::{CowStr, IntoStatic, types::string::Language}; 1 + use jacquard_common::bos::{BosStr, DefaultStr}; 2 + use jacquard_common::{IntoStatic, types::string::Language}; 2 3 use serde::{Deserialize, Serialize}; 3 4 4 5 /// Authorization server metadata, as returned from the ··· 7 8 /// Defined by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2) 8 9 /// with extensions from OpenID Connect Discovery, RFC 9126 (PAR), RFC 9207, 9 10 /// RFC 9449 (DPoP), and the ATProto client ID metadata document draft. 10 - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] 11 - pub struct OAuthAuthorizationServerMetadata<'s> { 11 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 12 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 13 + pub struct OAuthAuthorizationServerMetadata<S: BosStr = DefaultStr> { 12 14 /// The issuer identifier URL of the authorization server. 13 15 /// 14 16 /// <https://datatracker.ietf.org/doc/html/rfc8414#section-2> 15 - #[serde(borrow)] 16 - pub issuer: CowStr<'s>, 17 + pub issuer: S, 17 18 /// The URL of the authorization endpoint. 18 - pub authorization_endpoint: CowStr<'s>, // optional? 19 + pub authorization_endpoint: S, // optional? 19 20 /// The URL of the token endpoint. 20 - pub token_endpoint: CowStr<'s>, // optional? 21 + pub token_endpoint: S, // optional? 21 22 /// URL of the authorization server's JWK Set document. 22 - pub jwks_uri: Option<CowStr<'s>>, 23 + pub jwks_uri: Option<S>, 23 24 /// URL of the dynamic client registration endpoint, if supported. 24 - pub registration_endpoint: Option<CowStr<'s>>, 25 + pub registration_endpoint: Option<S>, 25 26 /// List of OAuth 2.0 scope values the server supports. 26 - pub scopes_supported: Vec<CowStr<'s>>, 27 + pub scopes_supported: Vec<S>, 27 28 /// List of OAuth 2.0 response type values the server supports. 28 - pub response_types_supported: Vec<CowStr<'s>>, 29 + pub response_types_supported: Vec<S>, 29 30 /// List of OAuth 2.0 response mode values the server supports. 30 - pub response_modes_supported: Option<Vec<CowStr<'s>>>, 31 + pub response_modes_supported: Option<Vec<S>>, 31 32 /// List of OAuth 2.0 grant type values the server supports. 32 - pub grant_types_supported: Option<Vec<CowStr<'s>>>, 33 + pub grant_types_supported: Option<Vec<S>>, 33 34 /// List of client authentication methods supported at the token endpoint. 34 - pub token_endpoint_auth_methods_supported: Option<Vec<CowStr<'s>>>, 35 + pub token_endpoint_auth_methods_supported: Option<Vec<S>>, 35 36 /// List of JWS signing algorithms supported for token endpoint auth. 36 - pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<CowStr<'s>>>, 37 + pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<S>>, 37 38 /// URL of a page with human-readable information about the server. 38 - pub service_documentation: Option<CowStr<'s>>, 39 + pub service_documentation: Option<S>, 39 40 /// BCP 47 language tags for UI locales the server supports. 40 41 pub ui_locales_supported: Option<Vec<Language>>, 41 42 /// URL of the authorization server's privacy policy. 42 - pub op_policy_uri: Option<CowStr<'s>>, 43 + pub op_policy_uri: Option<S>, 43 44 /// URL of the authorization server's terms of service. 44 - pub op_tos_uri: Option<CowStr<'s>>, 45 + pub op_tos_uri: Option<S>, 45 46 /// URL of the token revocation endpoint (RFC 7009). 46 - pub revocation_endpoint: Option<CowStr<'s>>, 47 + pub revocation_endpoint: Option<S>, 47 48 /// List of client authentication methods supported at the revocation endpoint. 48 - pub revocation_endpoint_auth_methods_supported: Option<Vec<CowStr<'s>>>, 49 + pub revocation_endpoint_auth_methods_supported: Option<Vec<S>>, 49 50 /// List of JWS signing algorithms supported for revocation endpoint auth. 50 - pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<CowStr<'s>>>, 51 + pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<S>>, 51 52 /// URL of the token introspection endpoint (RFC 7662). 52 - pub introspection_endpoint: Option<CowStr<'s>>, 53 + pub introspection_endpoint: Option<S>, 53 54 /// List of client authentication methods supported at the introspection endpoint. 54 - pub introspection_endpoint_auth_methods_supported: Option<Vec<CowStr<'s>>>, 55 + pub introspection_endpoint_auth_methods_supported: Option<Vec<S>>, 55 56 /// List of JWS signing algorithms supported for introspection endpoint auth. 56 - pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<CowStr<'s>>>, 57 + pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<S>>, 57 58 /// PKCE code challenge methods supported by the server. 58 - pub code_challenge_methods_supported: Option<Vec<CowStr<'s>>>, 59 + pub code_challenge_methods_supported: Option<Vec<S>>, 59 60 60 61 /// Subject identifier types supported (`public` or `pairwise`). 61 62 /// 62 63 /// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata> 63 - pub subject_types_supported: Option<Vec<CowStr<'s>>>, 64 + pub subject_types_supported: Option<Vec<S>>, 64 65 /// If `true`, clients must pre-register `request_uri` values. 65 66 pub require_request_uri_registration: Option<bool>, 66 67 67 68 /// URL of the Pushed Authorization Request (PAR) endpoint (RFC 9126). 68 69 /// 69 70 /// <https://datatracker.ietf.org/doc/html/rfc9126#section-5> 70 - pub pushed_authorization_request_endpoint: Option<CowStr<'s>>, 71 + pub pushed_authorization_request_endpoint: Option<S>, 71 72 /// If `true`, all authorization requests must use PAR. 72 73 pub require_pushed_authorization_requests: Option<bool>, 73 74 ··· 79 80 /// DPoP JWS signing algorithms supported by this server (RFC 9449). 80 81 /// 81 82 /// <https://datatracker.ietf.org/doc/html/rfc9449#section-5.1> 82 - pub dpop_signing_alg_values_supported: Option<Vec<CowStr<'s>>>, 83 + pub dpop_signing_alg_values_supported: Option<Vec<S>>, 83 84 84 85 /// If `true`, the server supports the ATProto client ID metadata document extension. 85 86 /// ··· 89 90 /// Protected resources associated with this authorization server. 90 91 /// 91 92 /// <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada> 92 - pub protected_resources: Option<Vec<CowStr<'s>>>, 93 + pub protected_resources: Option<Vec<S>>, 94 + } 95 + 96 + impl<S: BosStr> Default for OAuthAuthorizationServerMetadata<S> { 97 + fn default() -> Self { 98 + OAuthAuthorizationServerMetadata { 99 + issuer: S::from_static(""), 100 + authorization_endpoint: S::from_static(""), 101 + token_endpoint: S::from_static(""), 102 + jwks_uri: None, 103 + registration_endpoint: None, 104 + scopes_supported: Vec::new(), 105 + response_types_supported: Vec::new(), 106 + response_modes_supported: None, 107 + grant_types_supported: None, 108 + token_endpoint_auth_methods_supported: None, 109 + token_endpoint_auth_signing_alg_values_supported: None, 110 + service_documentation: None, 111 + ui_locales_supported: None, 112 + op_policy_uri: None, 113 + op_tos_uri: None, 114 + revocation_endpoint: None, 115 + revocation_endpoint_auth_methods_supported: None, 116 + revocation_endpoint_auth_signing_alg_values_supported: None, 117 + introspection_endpoint: None, 118 + introspection_endpoint_auth_methods_supported: None, 119 + introspection_endpoint_auth_signing_alg_values_supported: None, 120 + code_challenge_methods_supported: None, 121 + subject_types_supported: None, 122 + require_request_uri_registration: None, 123 + pushed_authorization_request_endpoint: None, 124 + require_pushed_authorization_requests: None, 125 + authorization_response_iss_parameter_supported: None, 126 + dpop_signing_alg_values_supported: None, 127 + client_id_metadata_document_supported: None, 128 + protected_resources: None, 129 + } 130 + } 93 131 } 94 132 95 133 /// Protected resource metadata, returned from `.well-known/oauth-protected-resource`. ··· 97 135 /// Allows clients to discover which authorization servers protect a given resource 98 136 /// and what scopes and bearer methods are accepted. Defined by 99 137 /// [draft-ietf-oauth-resource-metadata](https://datatracker.ietf.org/doc/draft-ietf-oauth-resource-metadata/). 100 - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] 101 - pub struct OAuthProtectedResourceMetadata<'s> { 138 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 139 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 140 + pub struct OAuthProtectedResourceMetadata<S: BosStr = DefaultStr> { 102 141 /// The URL of the protected resource itself. 103 - #[serde(borrow)] 104 - pub resource: CowStr<'s>, 142 + pub resource: S, 105 143 /// URLs of authorization servers that can issue tokens for this resource. 106 - pub authorization_servers: Option<Vec<CowStr<'s>>>, 144 + pub authorization_servers: Option<Vec<S>>, 107 145 /// URL of the resource server's JWK Set document. 108 - pub jwks_uri: Option<CowStr<'s>>, 146 + pub jwks_uri: Option<S>, 109 147 /// List of OAuth 2.0 scope values the resource server supports. 110 - pub scopes_supported: Vec<CowStr<'s>>, 148 + pub scopes_supported: Vec<S>, 111 149 /// Bearer token presentation methods supported (`header`, `body`, `query`). 112 - pub bearer_methods_supported: Option<Vec<CowStr<'s>>>, 150 + pub bearer_methods_supported: Option<Vec<S>>, 113 151 /// JWS signing algorithms supported for resource-bound tokens. 114 - pub resource_signing_alg_values_supported: Option<Vec<CowStr<'s>>>, 152 + pub resource_signing_alg_values_supported: Option<Vec<S>>, 115 153 /// URL of a page with human-readable information about the resource. 116 - pub resource_documentation: Option<CowStr<'s>>, 154 + pub resource_documentation: Option<S>, 117 155 /// URL of the resource server's privacy policy. 118 - pub resource_policy_uri: Option<CowStr<'s>>, 156 + pub resource_policy_uri: Option<S>, 119 157 /// URL of the resource server's terms of service. 120 - pub resource_tos_uri: Option<CowStr<'s>>, 158 + pub resource_tos_uri: Option<S>, 159 + } 160 + 161 + impl<S: BosStr> Default for OAuthProtectedResourceMetadata<S> { 162 + fn default() -> Self { 163 + OAuthProtectedResourceMetadata { 164 + resource: S::from_static(""), 165 + authorization_servers: None, 166 + jwks_uri: None, 167 + scopes_supported: Vec::new(), 168 + bearer_methods_supported: None, 169 + resource_signing_alg_values_supported: None, 170 + resource_documentation: None, 171 + resource_policy_uri: None, 172 + resource_tos_uri: None, 173 + } 174 + } 121 175 } 122 176 123 - impl IntoStatic for OAuthProtectedResourceMetadata<'_> { 124 - type Output = OAuthProtectedResourceMetadata<'static>; 177 + impl<S: BosStr + IntoStatic> IntoStatic for OAuthProtectedResourceMetadata<S> 178 + where 179 + S::Output: BosStr, 180 + { 181 + type Output = OAuthProtectedResourceMetadata<S::Output>; 125 182 fn into_static(self) -> Self::Output { 126 183 OAuthProtectedResourceMetadata { 127 184 resource: self.resource.into_static(), 128 185 authorization_servers: self.authorization_servers.into_static(), 129 - jwks_uri: self.jwks_uri.map(|v| v.into_static()), 186 + jwks_uri: self.jwks_uri.into_static(), 130 187 scopes_supported: self.scopes_supported.into_static(), 131 - bearer_methods_supported: self.bearer_methods_supported.map(|v| v.into_static()), 188 + bearer_methods_supported: self.bearer_methods_supported.into_static(), 132 189 resource_signing_alg_values_supported: self 133 190 .resource_signing_alg_values_supported 134 - .map(|v| v.into_static()), 135 - resource_documentation: self.resource_documentation.map(|v| v.into_static()), 136 - resource_policy_uri: self.resource_policy_uri.map(|v| v.into_static()), 137 - resource_tos_uri: self.resource_tos_uri.map(|v| v.into_static()), 191 + .into_static(), 192 + resource_documentation: self.resource_documentation.into_static(), 193 + resource_policy_uri: self.resource_policy_uri.into_static(), 194 + resource_tos_uri: self.resource_tos_uri.into_static(), 138 195 } 139 196 } 140 197 } 141 198 142 - impl IntoStatic for OAuthAuthorizationServerMetadata<'_> { 143 - type Output = OAuthAuthorizationServerMetadata<'static>; 199 + impl<S: BosStr + IntoStatic> IntoStatic for OAuthAuthorizationServerMetadata<S> 200 + where 201 + S::Output: BosStr, 202 + { 203 + type Output = OAuthAuthorizationServerMetadata<S::Output>; 144 204 fn into_static(self) -> Self::Output { 145 205 OAuthAuthorizationServerMetadata { 146 206 issuer: self.issuer.into_static(), ··· 178 238 .into_static(), 179 239 code_challenge_methods_supported: self.code_challenge_methods_supported.into_static(), 180 240 subject_types_supported: self.subject_types_supported.into_static(), 181 - require_request_uri_registration: self.require_request_uri_registration.into_static(), 241 + require_request_uri_registration: self.require_request_uri_registration, 182 242 pushed_authorization_request_endpoint: self 183 243 .pushed_authorization_request_endpoint 184 244 .into_static(), 185 - require_pushed_authorization_requests: self 186 - .require_pushed_authorization_requests 187 - .into_static(), 245 + require_pushed_authorization_requests: self.require_pushed_authorization_requests, 188 246 authorization_response_iss_parameter_supported: self 189 - .authorization_response_iss_parameter_supported 190 - .into_static(), 247 + .authorization_response_iss_parameter_supported, 191 248 dpop_signing_alg_values_supported: self.dpop_signing_alg_values_supported.into_static(), 192 - client_id_metadata_document_supported: self 193 - .client_id_metadata_document_supported 194 - .into_static(), 249 + client_id_metadata_document_supported: self.client_id_metadata_document_supported, 195 250 protected_resources: self.protected_resources.into_static(), 196 251 } 197 252 }
+43 -30
crates/jacquard-oauth/src/types/request.rs
··· 1 - use jacquard_common::{CowStr, IntoStatic}; 1 + use jacquard_common::IntoStatic; 2 + use jacquard_common::bos::{BosStr, DefaultStr}; 2 3 use serde::{Deserialize, Serialize}; 3 4 4 5 /// The `response_type` parameter for an OAuth 2.0 authorization request. ··· 51 52 /// authorization server before redirecting the user, improving security by keeping 52 53 /// parameters out of the browser URL. 53 54 #[derive(Serialize, Deserialize, Debug)] 54 - pub struct ParParameters<'a> { 55 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 56 + pub struct ParParameters<S: BosStr = DefaultStr> { 55 57 /// The response type to request (e.g. `code`). 56 58 /// 57 59 /// <https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1> 58 60 pub response_type: AuthorizationResponseType, 59 61 /// The redirect URI where the authorization response will be sent. 60 - #[serde(borrow)] 61 - pub redirect_uri: CowStr<'a>, 62 + pub redirect_uri: S, 62 63 /// An opaque CSRF state value to be echoed back in the callback. 63 - pub state: CowStr<'a>, 64 + pub state: S, 64 65 /// Space-separated list of requested scopes. 65 - pub scope: Option<CowStr<'a>>, 66 + pub scope: Option<S>, 66 67 /// How the authorization response parameters are delivered to the client. 67 68 /// 68 69 /// <https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes> ··· 70 71 /// The PKCE code challenge derived from the code verifier. 71 72 /// 72 73 /// <https://datatracker.ietf.org/doc/html/rfc7636#section-4.3> 73 - pub code_challenge: CowStr<'a>, 74 + pub code_challenge: S, 74 75 /// The method used to derive the code challenge. 75 76 pub code_challenge_method: AuthorizationCodeChallengeMethod, 76 77 /// Hint to pre-fill the login form with a handle or email. 77 78 /// 78 79 /// <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest> 79 - pub login_hint: Option<CowStr<'a>>, 80 + pub login_hint: Option<S>, 80 81 /// Prompt hint controlling authorization server UI behavior. 81 - pub prompt: Option<CowStr<'a>>, 82 + pub prompt: Option<S>, 82 83 } 83 84 84 85 /// The `grant_type` parameter for a token endpoint request. ··· 93 94 94 95 /// Parameters for exchanging an authorization code for tokens (RFC 6749 §4.1.3). 95 96 #[derive(Serialize, Deserialize)] 96 - pub struct TokenRequestParameters<'a> { 97 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 98 + pub struct TokenRequestParameters<S: BosStr = DefaultStr> { 97 99 /// Must be `authorization_code` for the authorization code grant. 98 100 /// 99 101 /// <https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3> 100 102 pub grant_type: TokenGrantType, 101 103 /// The authorization code received from the authorization server. 102 - #[serde(borrow)] 103 - pub code: CowStr<'a>, 104 + pub code: S, 104 105 /// The redirect URI used in the original authorization request. 105 - pub redirect_uri: CowStr<'a>, 106 + pub redirect_uri: S, 106 107 /// The PKCE code verifier that was used to generate the code challenge (RFC 7636 §4.5). 107 108 /// 108 109 /// <https://datatracker.ietf.org/doc/html/rfc7636#section-4.5> 109 - pub code_verifier: CowStr<'a>, 110 + pub code_verifier: S, 110 111 } 111 112 112 113 /// Parameters for refreshing an access token using a refresh token (RFC 6749 §6). 113 114 #[derive(Serialize, Deserialize)] 114 - pub struct RefreshRequestParameters<'a> { 115 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 116 + pub struct RefreshRequestParameters<S: BosStr = DefaultStr> { 115 117 /// Must be `refresh_token` for the refresh grant. 116 118 /// 117 119 /// <https://datatracker.ietf.org/doc/html/rfc6749#section-6> 118 120 pub grant_type: TokenGrantType, 119 121 /// The refresh token previously issued to the client. 120 - #[serde(borrow)] 121 - pub refresh_token: CowStr<'a>, 122 + pub refresh_token: S, 122 123 /// Optional scope to request; must not exceed the originally granted scope. 123 - pub scope: Option<CowStr<'a>>, 124 + pub scope: Option<S>, 124 125 } 125 126 126 127 /// Parameters for a token revocation request (RFC 7009 §2.1). ··· 130 131 /// 131 132 /// <https://datatracker.ietf.org/doc/html/rfc7009#section-2.1> 132 133 #[derive(Serialize, Deserialize)] 133 - pub struct RevocationRequestParameters<'a> { 134 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 135 + pub struct RevocationRequestParameters<S: BosStr = DefaultStr> { 134 136 /// The token to be revoked. 135 - #[serde(borrow)] 136 - pub token: CowStr<'a>, 137 + pub token: S, 137 138 // ? 138 139 // pub token_type_hint: Option<String>, 139 140 } 140 141 141 - impl IntoStatic for RevocationRequestParameters<'_> { 142 - type Output = RevocationRequestParameters<'static>; 142 + impl<S: BosStr + IntoStatic> IntoStatic for RevocationRequestParameters<S> 143 + where 144 + S::Output: BosStr, 145 + { 146 + type Output = RevocationRequestParameters<S::Output>; 143 147 144 148 fn into_static(self) -> Self::Output { 145 149 Self::Output { ··· 148 152 } 149 153 } 150 154 151 - impl IntoStatic for TokenRequestParameters<'_> { 152 - type Output = TokenRequestParameters<'static>; 155 + impl<S: BosStr + IntoStatic> IntoStatic for TokenRequestParameters<S> 156 + where 157 + S::Output: BosStr, 158 + { 159 + type Output = TokenRequestParameters<S::Output>; 153 160 154 161 fn into_static(self) -> Self::Output { 155 162 Self::Output { ··· 161 168 } 162 169 } 163 170 164 - impl IntoStatic for RefreshRequestParameters<'_> { 165 - type Output = RefreshRequestParameters<'static>; 171 + impl<S: BosStr + IntoStatic> IntoStatic for RefreshRequestParameters<S> 172 + where 173 + S::Output: BosStr, 174 + { 175 + type Output = RefreshRequestParameters<S::Output>; 166 176 167 177 fn into_static(self) -> Self::Output { 168 178 Self::Output { 169 179 grant_type: self.grant_type, 170 180 refresh_token: self.refresh_token.into_static(), 171 - scope: self.scope.map(CowStr::into_static), 181 + scope: self.scope.into_static(), 172 182 } 173 183 } 174 184 } 175 185 176 - impl IntoStatic for ParParameters<'_> { 177 - type Output = ParParameters<'static>; 186 + impl<S: BosStr + IntoStatic> IntoStatic for ParParameters<S> 187 + where 188 + S::Output: BosStr, 189 + { 190 + type Output = ParParameters<S::Output>; 178 191 179 192 fn into_static(self) -> Self::Output { 180 193 Self::Output {
+34 -14
crates/jacquard-oauth/src/types/token.rs
··· 1 1 use super::response::OAuthTokenType; 2 + use jacquard_common::IntoStatic; 3 + use jacquard_common::bos::{BosStr, DefaultStr}; 2 4 use jacquard_common::types::string::{Datetime, Did}; 3 - use jacquard_common::{CowStr, IntoStatic}; 4 5 use serde::{Deserialize, Serialize}; 5 6 6 7 /// A complete set of OAuth tokens and associated claims for an authenticated session. ··· 9 10 /// everything it needs to make authorized requests. This is stored in the session 10 11 /// and refreshed transparently by `OAuthSession`. 11 12 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 12 - pub struct TokenSet<'s> { 13 + #[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] 14 + pub struct TokenSet<S: BosStr = DefaultStr> { 13 15 /// The issuer URL of the authorization server that issued these tokens. 14 - #[serde(borrow)] 15 - pub iss: CowStr<'s>, 16 + pub iss: S, 16 17 /// The subject DID identifying the authenticated user. 17 - pub sub: Did<'s>, 18 + pub sub: Did<S>, 18 19 /// The audience (resource server URL or DID) the tokens are intended for. 19 - pub aud: CowStr<'s>, 20 + pub aud: S, 20 21 /// The scopes granted by the authorization server. 21 - pub scope: Option<CowStr<'s>>, 22 + pub scope: Option<S>, 22 23 23 24 /// A refresh token that can be exchanged for new access tokens. 24 - pub refresh_token: Option<CowStr<'s>>, 25 + pub refresh_token: Option<S>, 25 26 /// The current access token to include in API requests. 26 - pub access_token: CowStr<'s>, 27 + pub access_token: S, 27 28 /// Whether the access token must be presented as a DPoP or Bearer token. 28 29 pub token_type: OAuthTokenType, 29 30 ··· 31 32 pub expires_at: Option<Datetime>, 32 33 } 33 34 34 - impl IntoStatic for TokenSet<'_> { 35 - type Output = TokenSet<'static>; 35 + impl<S: BosStr> TokenSet<S> { 36 + /// Convert to an `Nsid` with a different backing type. 37 + pub fn convert<B: From<S> + BosStr>(self) -> TokenSet<B> { 38 + TokenSet { 39 + iss: self.iss.into(), 40 + sub: self.sub.convert(), 41 + aud: self.aud.into(), 42 + scope: self.scope.map(|s| s.into()), 43 + refresh_token: self.refresh_token.map(|t| t.into()), 44 + access_token: self.access_token.into(), 45 + token_type: self.token_type, 46 + expires_at: self.expires_at, 47 + } 48 + } 49 + } 50 + 51 + impl<S: BosStr + IntoStatic> IntoStatic for TokenSet<S> 52 + where 53 + S::Output: BosStr, 54 + { 55 + type Output = TokenSet<S::Output>; 36 56 37 57 fn into_static(self) -> Self::Output { 38 58 TokenSet { 39 59 iss: self.iss.into_static(), 40 60 sub: self.sub.into_static(), 41 61 aud: self.aud.into_static(), 42 - scope: self.scope.map(|s| s.into_static()), 43 - refresh_token: self.refresh_token.map(|s| s.into_static()), 62 + scope: self.scope.into_static(), 63 + refresh_token: self.refresh_token.into_static(), 44 64 access_token: self.access_token.into_static(), 45 65 token_type: self.token_type, 46 - expires_at: self.expires_at.map(|s| s.into_static()), 66 + expires_at: self.expires_at.into_static(), 47 67 } 48 68 } 49 69 }
+19 -13
crates/jacquard-oauth/src/utils.rs
··· 1 1 use base64::Engine; 2 2 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 3 3 use elliptic_curve::SecretKey; 4 - use jacquard_common::CowStr; 4 + use jacquard_common::BosStr; 5 5 use jose_jwk::{Key, crypto}; 6 6 use rand::{CryptoRng, RngCore, rngs::ThreadRng}; 7 7 use sha2::{Digest, Sha256}; 8 + use smol_str::SmolStr; 8 9 use std::cmp::Ordering; 9 10 10 11 use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; ··· 13 14 /// supported, returning `None` if none are supported. 14 15 /// 15 16 /// Currently only `ES256` (P-256 ECDSA) is implemented; other algorithm identifiers are skipped. 16 - pub fn generate_key(allowed_algos: &[CowStr]) -> Option<Key> { 17 + pub fn generate_key(allowed_algos: &[impl AsRef<str>]) -> Option<Key> { 17 18 for alg in allowed_algos { 18 19 #[allow(clippy::single_match)] 19 20 match alg.as_ref() { ··· 31 32 } 32 33 33 34 /// Generate a cryptographically random 16-byte nonce encoded as base64url (no padding). 34 - pub fn generate_nonce() -> CowStr<'static> { 35 + pub fn generate_nonce() -> SmolStr { 35 36 URL_SAFE_NO_PAD 36 37 .encode(get_random_values::<_, 16>(&mut ThreadRng::default())) 37 38 .into() 38 39 } 39 40 40 41 /// Generate a cryptographically random 43-byte PKCE code verifier encoded as base64url (no padding). 41 - pub fn generate_verifier() -> CowStr<'static> { 42 + pub fn generate_verifier() -> SmolStr { 42 43 URL_SAFE_NO_PAD 43 44 .encode(get_random_values::<_, 43>(&mut ThreadRng::default())) 44 45 .into() ··· 58 59 /// 59 60 /// The ordering is: ES256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other. 60 61 /// Algorithms within the same family are ordered by key length, preferring shorter (faster) keys first. 61 - pub fn compare_algos(a: &CowStr, b: &CowStr) -> Ordering { 62 - if a.as_ref() == "ES256K" { 62 + pub fn compare_algos(a: &impl AsRef<str>, b: &impl AsRef<str>) -> Ordering { 63 + let a = a.as_ref(); 64 + let b = b.as_ref(); 65 + if a == "ES256K" { 63 66 return Ordering::Less; 64 67 } 65 - if b.as_ref() == "ES256K" { 68 + if b == "ES256K" { 66 69 return Ordering::Greater; 67 70 } 68 71 for prefix in ["ES", "PS", "RS"] { ··· 89 92 /// of the verifier, per [RFC 7636 §4.1](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1). 90 93 /// The verifier must be kept secret and sent at the token endpoint; the challenge is sent at 91 94 /// the authorization endpoint. 92 - pub fn generate_pkce() -> (CowStr<'static>, CowStr<'static>) { 95 + pub fn generate_pkce() -> (SmolStr, SmolStr) { 93 96 // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 94 97 let verifier = generate_verifier(); 95 98 ( 96 99 URL_SAFE_NO_PAD 97 - .encode(Sha256::digest(&verifier.as_str())) 100 + .encode(Sha256::digest(verifier.as_str())) 98 101 .into(), 99 102 verifier, 100 103 ) ··· 105 108 /// Reads `dpop_signing_alg_values_supported` from the server metadata, sorts by preference 106 109 /// using [`compare_algos`], and attempts to generate a key for the most preferred supported 107 110 /// algorithm. Falls back to [`crate::FALLBACK_ALG`] if the server does not advertise any algorithms. 108 - pub fn generate_dpop_key(metadata: &OAuthAuthorizationServerMetadata) -> Option<Key> { 109 - let mut algs = metadata 111 + pub fn generate_dpop_key<S: BosStr>( 112 + metadata: &mut OAuthAuthorizationServerMetadata<S>, 113 + ) -> Option<Key> { 114 + let mut fallback = vec![S::from_static(FALLBACK_ALG)]; 115 + let algs = metadata 110 116 .dpop_signing_alg_values_supported 111 - .clone() 112 - .unwrap_or(vec![FALLBACK_ALG.into()]); 117 + .as_deref_mut() 118 + .unwrap_or(&mut fallback); 113 119 algs.sort_by(compare_algos); 114 120 generate_key(&algs) 115 121 }
+51 -44
crates/jacquard-repo/src/commit/firehose.rs
··· 7 7 pub use jacquard_api::com_atproto::sync::subscribe_repos::Commit as FirehoseCommit; 8 8 pub use jacquard_api::com_atproto::sync::subscribe_repos::RepoOp; 9 9 use jacquard_api::com_atproto::sync::subscribe_repos::{Commit, RepoOpAction}; 10 + use jacquard_common::BosStr; 10 11 use jacquard_common::types::crypto::PublicKey; 12 + use smol_str::SmolStr; 11 13 use smol_str::ToSmolStr; 12 14 13 15 /// Convert to VerifiedWriteOp for v1.1 validation 14 16 /// 15 17 /// Validates that all required fields are present for inversion. 16 - pub fn to_invertible_op(op: &RepoOp<'_>) -> Result<VerifiedWriteOp> { 18 + pub fn to_invertible_op<S: BosStr + core::fmt::Display>(op: &RepoOp<S>) -> Result<VerifiedWriteOp> { 17 19 let key = op.path.to_smolstr(); 18 20 match op.action { 19 21 RepoOpAction::Create => { ··· 91 93 /// 4. Verify result matches commit.data (new MST root) 92 94 /// 93 95 /// Returns the new MST root CID on success. 94 - pub async fn validate_v1_0<S: BlockStore + Sync + 'static>( 95 - fh_commit: &Commit<'_>, 96 + pub async fn validate_v1_0<B: BosStr, S: BlockStore + Sync + 'static>( 97 + fh_commit: &Commit<B>, 96 98 prev_mst_root: Option<IpldCid>, 97 99 prev_storage: Arc<S>, 98 100 pubkey: &PublicKey<'_>, ··· 115 117 .await? 116 118 .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?; 117 119 118 - let commit = super::Commit::from_cbor(&commit_bytes)?; 120 + let commit = super::Commit::<SmolStr>::from_cbor(&commit_bytes)?; 119 121 120 122 // Verify DID matches 121 123 if commit.did().as_ref() != fh_commit.repo.as_ref() { ··· 186 188 /// this is far from the most efficient possible validation function possible. The repo 187 189 /// tree struct carries extra information. However, 188 190 /// it has the virtue of making everything self-validating. 189 - pub async fn validate_v1_1(fh_commit: &Commit<'_>, pubkey: &PublicKey<'_>) -> Result<IpldCid> { 191 + pub async fn validate_v1_1<S: BosStr + core::fmt::Display>( 192 + fh_commit: &Commit<S>, 193 + pubkey: &PublicKey<'_>, 194 + ) -> Result<IpldCid> { 190 195 // 1. Require prev_data for v1.1 191 196 let prev_data_cid: IpldCid = fh_commit 192 197 .prev_data ··· 210 215 .await? 211 216 .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?; 212 217 213 - let commit = super::Commit::from_cbor(&commit_bytes)?; 218 + let commit = super::Commit::<SmolStr>::from_cbor(&commit_bytes)?; 214 219 215 220 // Verify DID matches 216 221 if commit.did().as_ref() != fh_commit.repo.as_ref() { ··· 300 305 record 301 306 } 302 307 303 - async fn create_test_repo(storage: Arc<MemoryBlockStore>) -> Repository<MemoryBlockStore> { 308 + async fn create_test_repo<'a>( 309 + storage: Arc<MemoryBlockStore>, 310 + ) -> Repository<SmolStr, MemoryBlockStore> { 304 311 let did = Did::new("did:plc:test").unwrap(); 305 312 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 306 313 ··· 332 339 let storage = Arc::new(MemoryBlockStore::new()); 333 340 let mut repo = create_test_repo(storage.clone()).await; 334 341 335 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 336 - let rkey = RecordKey(Rkey::new("test1").unwrap()); 342 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 343 + let rkey = RecordKey(Rkey::new("test1").unwrap().into_static()); 337 344 338 - let did = Did::new("did:plc:test").unwrap(); 345 + let did = Did::new("did:plc:test").unwrap().into_static(); 339 346 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 340 347 let pubkey = get_public_key(&signing_key); 341 348 ··· 381 388 let storage = Arc::new(MemoryBlockStore::new()); 382 389 let mut repo = create_test_repo(storage.clone()).await; 383 390 384 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 385 - let rkey = RecordKey(Rkey::new("test1").unwrap()); 391 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 392 + let rkey = RecordKey(Rkey::new("test1").unwrap()).into_static(); 386 393 387 - let did = Did::new("did:plc:test").unwrap(); 394 + let did = Did::new("did:plc:test").unwrap().into_static(); 388 395 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 389 396 let pubkey = get_public_key(&signing_key); 390 397 ··· 433 440 let storage = Arc::new(MemoryBlockStore::new()); 434 441 let mut repo = create_test_repo(storage.clone()).await; 435 442 436 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 437 - let did = Did::new("did:plc:test").unwrap(); 443 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 444 + let did = Did::new("did:plc:test").unwrap().into_static(); 438 445 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 439 446 let pubkey = get_public_key(&signing_key); 440 447 ··· 442 449 let ops1 = vec![ 443 450 RecordWriteOp::Create { 444 451 collection: collection.clone(), 445 - rkey: RecordKey(Rkey::new("post1").unwrap()), 452 + rkey: RecordKey(Rkey::new("post1").unwrap()).into_static(), 446 453 record: make_test_record(1), 447 454 }, 448 455 RecordWriteOp::Create { 449 456 collection: collection.clone(), 450 - rkey: RecordKey(Rkey::new("post2").unwrap()), 457 + rkey: RecordKey(Rkey::new("post2").unwrap()).into_static(), 451 458 record: make_test_record(2), 452 459 }, 453 460 ]; ··· 476 483 let storage = Arc::new(MemoryBlockStore::new()); 477 484 let mut repo = create_test_repo(storage.clone()).await; 478 485 479 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 480 - let did = Did::new("did:plc:test").unwrap(); 486 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 487 + let did = Did::new("did:plc:test").unwrap().into_static(); 481 488 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 482 489 let pubkey = get_public_key(&signing_key); 483 490 484 491 // First: create records 485 - let rkey1 = RecordKey(Rkey::new("post1").unwrap()); 486 - let rkey2 = RecordKey(Rkey::new("post2").unwrap()); 492 + let rkey1 = RecordKey(Rkey::new("post1").unwrap()).into_static(); 493 + let rkey2 = RecordKey(Rkey::new("post2").unwrap()).into_static(); 487 494 488 495 let create_ops = vec![ 489 496 RecordWriteOp::Create { ··· 552 559 let storage = Arc::new(MemoryBlockStore::new()); 553 560 let mut repo = create_test_repo(storage.clone()).await; 554 561 555 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 556 - let did = Did::new("did:plc:test").unwrap(); 562 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 563 + let did = Did::new("did:plc:test").unwrap().into_static(); 557 564 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 558 565 let pubkey = get_public_key(&signing_key); 559 566 560 567 let ops = vec![RecordWriteOp::Create { 561 568 collection: collection.clone(), 562 - rkey: RecordKey(Rkey::new("test1").unwrap()), 569 + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), 563 570 record: make_test_record(1), 564 571 }]; 565 572 ··· 607 614 let storage = Arc::new(MemoryBlockStore::new()); 608 615 let mut repo = create_test_repo(storage.clone()).await; 609 616 610 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 611 - let did = Did::new("did:plc:test").unwrap(); 617 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 618 + let did = Did::new("did:plc:test").unwrap().into_static(); 612 619 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 613 620 let pubkey = get_public_key(&signing_key); 614 621 ··· 616 623 let ops = vec![ 617 624 RecordWriteOp::Create { 618 625 collection: collection.clone(), 619 - rkey: RecordKey(Rkey::new("aaa").unwrap()), 626 + rkey: RecordKey(Rkey::new("aaa").unwrap()).into_static(), 620 627 record: make_test_record(1), 621 628 }, 622 629 RecordWriteOp::Create { 623 630 collection: collection.clone(), 624 - rkey: RecordKey(Rkey::new("zzz").unwrap()), 631 + rkey: RecordKey(Rkey::new("zzz").unwrap()).into_static(), 625 632 record: make_test_record(2), 626 633 }, 627 634 ]; ··· 669 676 let storage = Arc::new(MemoryBlockStore::new()); 670 677 let mut repo = create_test_repo(storage.clone()).await; 671 678 672 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 673 - let did = Did::new("did:plc:test").unwrap(); 679 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 680 + let did = Did::new("did:plc:test").unwrap().into_static(); 674 681 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 675 682 let pubkey = get_public_key(&signing_key); 676 683 677 684 let ops = vec![RecordWriteOp::Create { 678 685 collection: collection.clone(), 679 - rkey: RecordKey(Rkey::new("test1").unwrap()), 686 + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), 680 687 record: make_test_record(1), 681 688 }]; 682 689 ··· 730 737 let storage = Arc::new(MemoryBlockStore::new()); 731 738 let mut repo = create_test_repo(storage.clone()).await; 732 739 733 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 734 - let did = Did::new("did:plc:test").unwrap(); 735 - let wrong_did = Did::new("did:plc:wrong").unwrap(); 740 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 741 + let did = Did::new("did:plc:test").unwrap().into_static(); 742 + let wrong_did = Did::new("did:plc:wrong").unwrap().into_static(); 736 743 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 737 744 let pubkey = get_public_key(&signing_key); 738 745 739 746 let ops = vec![RecordWriteOp::Create { 740 747 collection: collection.clone(), 741 - rkey: RecordKey(Rkey::new("test1").unwrap()), 748 + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), 742 749 record: make_test_record(1), 743 750 }]; 744 751 ··· 778 785 let storage = Arc::new(MemoryBlockStore::new()); 779 786 let mut repo = create_test_repo(storage.clone()).await; 780 787 781 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 782 - let did = Did::new("did:plc:test").unwrap(); 788 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 789 + let did = Did::new("did:plc:test").unwrap().into_static(); 783 790 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 784 791 785 792 // Use a different key for verification ··· 788 795 789 796 let ops = vec![RecordWriteOp::Create { 790 797 collection: collection.clone(), 791 - rkey: RecordKey(Rkey::new("test1").unwrap()), 798 + rkey: RecordKey(Rkey::new("test1").unwrap().into_static()), 792 799 record: make_test_record(1), 793 800 }]; 794 801 ··· 819 826 let storage = Arc::new(MemoryBlockStore::new()); 820 827 let mut repo = create_test_repo(storage.clone()).await; 821 828 822 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 823 - let did = Did::new("did:plc:test").unwrap(); 829 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 830 + let did = Did::new("did:plc:test").unwrap().into_static(); 824 831 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 825 832 let pubkey = get_public_key(&signing_key); 826 833 827 834 let ops = vec![RecordWriteOp::Create { 828 835 collection: collection.clone(), 829 - rkey: RecordKey(Rkey::new("test1").unwrap()), 836 + rkey: RecordKey(Rkey::new("test1").unwrap().into_static()), 830 837 record: make_test_record(1), 831 838 }]; 832 839 ··· 866 873 let storage = Arc::new(MemoryBlockStore::new()); 867 874 let mut repo = create_test_repo(storage.clone()).await; 868 875 869 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 870 - let did = Did::new("did:plc:test").unwrap(); 876 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 877 + let did = Did::new("did:plc:test").unwrap().into_static(); 871 878 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 872 879 let pubkey = get_public_key(&signing_key); 873 880 874 881 let ops = vec![RecordWriteOp::Create { 875 882 collection: collection.clone(), 876 - rkey: RecordKey(Rkey::new("test1").unwrap()), 883 + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), 877 884 record: make_test_record(1), 878 885 }]; 879 886
+25 -21
crates/jacquard-repo/src/commit/mod.rs
··· 8 8 use crate::error::{CommitError, Result}; 9 9 use bytes::Bytes; 10 10 use cid::Cid as IpldCid; 11 - use jacquard_common::IntoStatic; 12 11 use jacquard_common::types::crypto::PublicKey; 13 12 use jacquard_common::types::string::Did; 14 13 use jacquard_common::types::tid::Tid; 14 + use jacquard_common::{BosStr, IntoStatic}; 15 15 16 16 /// Repository commit object 17 17 /// ··· 22 22 /// serialized (v2 uses it, v3 must include it even if null). This struct 23 23 /// handles both by always including `prev` in serialization. 24 24 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 25 - pub struct Commit<'a> { 25 + pub struct Commit<S: BosStr> { 26 26 /// Repository DID 27 - #[serde(borrow)] 28 - pub did: Did<'a>, 27 + pub did: Did<S>, 29 28 30 29 /// Commit version (2 or 3) 31 30 pub version: i64, ··· 48 47 /// Explicitly a separate struct minus the sig field to match deserialization/signatures 49 48 /// from other implementations. 50 49 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 51 - pub struct UnsignedCommit<'a> { 50 + pub struct UnsignedCommit<S: BosStr> { 52 51 /// Repository DID 53 - #[serde(borrow)] 54 - pub did: Did<'a>, 52 + pub did: Did<S>, 55 53 56 54 /// Commit version (2 or 3) 57 55 pub version: i64, ··· 66 64 pub prev: Option<IpldCid>, 67 65 } 68 66 69 - impl<'a> UnsignedCommit<'a> { 67 + impl<S: BosStr + serde::Serialize> UnsignedCommit<S> { 70 68 /// Create new unsigned commit (version = 3, sig empty) 71 69 pub fn new_unsigned( 72 - did: Did<'a>, 70 + did: Did<S>, 73 71 data: IpldCid, 74 72 rev: Tid, 75 73 prev: Option<IpldCid>, 76 - ) -> UnsignedCommit<'a> { 74 + ) -> UnsignedCommit<S> { 77 75 Self { 78 76 did, 79 77 version: 3, ··· 85 83 /// Get unsigned commit bytes (for signing/verification) 86 84 pub(super) fn unsigned_bytes(&self) -> Result<Vec<u8>> { 87 85 // Serialize without signature field 88 - let unsigned = self.clone(); 89 - serde_ipld_dagcbor::to_vec(&unsigned) 86 + serde_ipld_dagcbor::to_vec(self) 90 87 .map_err(|e| crate::error::CommitError::Serialization(Box::new(e)).into()) 91 88 } 92 89 93 90 /// Sign this commit with a key 94 - pub fn sign(self, key: &impl SigningKey) -> Result<Commit<'a>> { 91 + pub fn sign(self, key: &impl SigningKey) -> Result<Commit<S>> { 95 92 let unsigned = self.unsigned_bytes()?; 96 93 let sig = key.sign_bytes(&unsigned)?; 97 94 ··· 105 102 }) 106 103 } 107 104 /// Get the repository DID 108 - pub fn did(&self) -> &Did<'a> { 105 + pub fn did(&self) -> &Did<S> { 109 106 &self.did 110 107 } 111 108 ··· 125 122 } 126 123 } 127 124 128 - impl<'a> Commit<'a> { 125 + impl<S: BosStr + Clone + serde::Serialize> Commit<S> { 129 126 /// Create new unsigned commit (version = 3, sig empty) 130 127 pub fn new_unsigned( 131 - did: Did<'a>, 128 + did: Did<S>, 132 129 data: IpldCid, 133 130 rev: Tid, 134 131 prev: Option<IpldCid>, 135 - ) -> UnsignedCommit<'a> { 132 + ) -> UnsignedCommit<S> { 136 133 UnsignedCommit { 137 134 did, 138 135 version: 3, ··· 157 154 } 158 155 159 156 /// Get the repository DID 160 - pub fn did(&self) -> &Did<'a> { 157 + pub fn did(&self) -> &Did<S> { 161 158 &self.did 162 159 } 163 160 ··· 187 184 } 188 185 189 186 /// Deserialize from DAG-CBOR 190 - pub fn from_cbor(data: &'a [u8]) -> Result<Self> { 187 + pub fn from_cbor<'a>(data: &'a [u8]) -> Result<Self> 188 + where 189 + S: serde::Deserialize<'a>, 190 + { 191 191 serde_ipld_dagcbor::from_slice(data) 192 192 .map_err(|e| CommitError::Serialization(Box::new(e)).into()) 193 193 } ··· 251 251 } 252 252 } 253 253 254 - impl IntoStatic for Commit<'_> { 255 - type Output = Commit<'static>; 254 + impl<S> IntoStatic for Commit<S> 255 + where 256 + S: BosStr + IntoStatic, 257 + S::Output: BosStr, 258 + { 259 + type Output = Commit<S::Output>; 256 260 257 261 fn into_static(self) -> Self::Output { 258 262 Commit {
+9 -6
crates/jacquard-repo/src/commit/proof.rs
··· 22 22 use crate::mst::Mst; 23 23 use crate::storage::MemoryBlockStore; 24 24 use cid::Cid as IpldCid; 25 - use jacquard_common::CowStr; 26 25 use jacquard_common::types::string::Did; 27 - use smol_str::format_smolstr; 26 + use jacquard_common::{BosStr, CowStr}; 27 + use smol_str::{SmolStr, format_smolstr}; 28 28 use std::sync::Arc; 29 29 30 30 /// A claim about a record's CID at a specific path ··· 88 88 /// let result = verify_proofs(car_bytes, claims, did, pubkey).await?; 89 89 /// assert_eq!(result.verified.len(), 2); // Both claims verified 90 90 /// ``` 91 - pub async fn verify_proofs<'a>( 91 + pub async fn verify_proofs<'a, 'de, S>( 92 92 car_bytes: &[u8], 93 93 claims: Vec<RecordClaim<'a>>, 94 - did: &Did<'_>, 94 + did: &Did<S>, 95 95 pubkey: &jacquard_common::types::crypto::PublicKey<'_>, 96 - ) -> Result<VerifyProofsOutput<'a>, ProofError> { 96 + ) -> Result<VerifyProofsOutput<'a>, ProofError> 97 + where 98 + S: BosStr + Clone + serde::Serialize + serde::Deserialize<'de>, 99 + { 97 100 // 1. Parse CAR file 98 101 let parsed = 99 102 crate::car::parse_car_bytes(car_bytes) ··· 113 116 .map_err(|_| ProofError::CommitNotFound)? 114 117 .ok_or(ProofError::CommitNotFound)?; 115 118 116 - let commit = super::Commit::from_cbor(&commit_bytes).map_err(|e| { 119 + let commit = super::Commit::<SmolStr>::from_cbor(&commit_bytes).map_err(|e| { 117 120 ProofError::CommitDeserializeFailed { 118 121 source: Box::new(e), 119 122 }
+11 -7
crates/jacquard-repo/src/mst/diff.rs
··· 1 1 //! MST diff calculation 2 2 3 3 use std::collections::BTreeMap; 4 + use std::convert::{From, Infallible}; 4 5 use std::future::Future; 5 6 use std::pin::Pin; 7 + use std::str::FromStr; 6 8 7 9 use super::cursor::{CursorPosition, MstCursor}; 8 10 use super::tree::Mst; ··· 13 15 use crate::storage::BlockStore; 14 16 use bytes::Bytes; 15 17 use cid::Cid as IpldCid; 18 + use jacquard_api::com_atproto::sync::subscribe_repos::RepoOpAction; 19 + use jacquard_common::BosStr; 16 20 use jacquard_common::types::cid::CidLink; 17 21 use smol_str::SmolStr; 18 22 ··· 160 164 /// 161 165 /// Returns operations in the format used by `com.atproto.sync.subscribeRepos`. 162 166 /// All update/delete operations include prev CIDs for sync v1.1 validation. 163 - pub fn to_repo_ops(&self) -> Vec<RepoOp<'_>> { 167 + pub fn to_repo_ops<'s, S: BosStr + FromStr<Err = Infallible>>(&'s self) -> Vec<RepoOp<S>> { 164 168 let mut ops = Vec::with_capacity(self.op_count()); 165 169 166 170 // Add creates 167 171 for (key, cid) in &self.creates { 168 172 ops.push(RepoOp { 169 - action: "create".into(), 170 - path: key.as_str().into(), 173 + action: RepoOpAction::from_value(S::from_static("create")), 174 + path: S::from_str(key.as_str()).unwrap(), 171 175 cid: Some(CidLink::from(*cid)), 172 176 prev: None, 173 177 extra_data: None, ··· 177 181 // Add updates 178 182 for (key, new_cid, old_cid) in &self.updates { 179 183 ops.push(RepoOp { 180 - action: "update".into(), 181 - path: key.as_str().into(), 184 + action: RepoOpAction::from_value(S::from_static("update")), 185 + path: S::from_str(key.as_str()).unwrap(), 182 186 cid: Some(CidLink::from(*new_cid)), 183 187 prev: Some(CidLink::from(*old_cid)), 184 188 extra_data: None, ··· 188 192 // Add deletes 189 193 for (key, old_cid) in &self.deletes { 190 194 ops.push(RepoOp { 191 - action: "delete".into(), 192 - path: key.as_str().into(), 195 + action: RepoOpAction::from_value(S::from_static("delete")), 196 + path: S::from_str(key.as_str()).unwrap(), 193 197 cid: None, // null for deletes 194 198 prev: Some(CidLink::from(*old_cid)), 195 199 extra_data: None,
+11 -10
crates/jacquard-repo/src/mst/tree.rs
··· 8 8 use bytes::Bytes; 9 9 use cid::Cid as IpldCid; 10 10 use core::fmt; 11 + use jacquard_common::BosStr; 11 12 use jacquard_common::types::recordkey::Rkey; 12 13 use jacquard_common::types::string::{Nsid, RecordKey}; 13 14 use jacquard_common::types::value::RawData; ··· 63 64 /// needs to be serialized and stored. The data is a generic IPLD map 64 65 /// (similar to rsky's `RepoRecord = BTreeMap<String, Lex>`). 65 66 #[derive(Debug, Clone, PartialEq)] 66 - pub enum RecordWriteOp<'a> { 67 + pub enum RecordWriteOp<'a, S: BosStr> { 67 68 /// Create new record with data 68 69 Create { 69 70 /// Collection NSID 70 - collection: Nsid<'a>, 71 + collection: Nsid<S>, 71 72 /// Record key 72 - rkey: RecordKey<Rkey<'a>>, 73 + rkey: RecordKey<Rkey<S>>, 73 74 /// Record data (will be serialized to DAG-CBOR and CID computed) 74 75 record: std::collections::BTreeMap<SmolStr, RawData<'a>>, 75 76 }, ··· 77 78 /// Update existing record with new data 78 79 Update { 79 80 /// Collection NSID 80 - collection: Nsid<'a>, 81 + collection: Nsid<S>, 81 82 /// Record key 82 - rkey: RecordKey<Rkey<'a>>, 83 + rkey: RecordKey<Rkey<S>>, 83 84 /// New record data 84 85 record: std::collections::BTreeMap<SmolStr, RawData<'a>>, 85 86 /// Previous CID (optional for validation) ··· 89 90 /// Delete record 90 91 Delete { 91 92 /// Collection NSID 92 - collection: Nsid<'a>, 93 + collection: Nsid<S>, 93 94 /// Record key 94 - rkey: RecordKey<Rkey<'a>>, 95 + rkey: RecordKey<Rkey<S>>, 95 96 /// Previous CID (optional for validation) 96 97 prev: Option<IpldCid>, 97 98 }, 98 99 } 99 100 100 - impl<'a> RecordWriteOp<'a> { 101 + impl<'a, S: BosStr> RecordWriteOp<'a, S> { 101 102 /// Get the collection NSID for this operation 102 - pub fn collection(&self) -> &Nsid<'a> { 103 + pub fn collection(&self) -> &Nsid<S> { 103 104 match self { 104 105 RecordWriteOp::Create { collection, .. } => collection, 105 106 RecordWriteOp::Update { collection, .. } => collection, ··· 108 109 } 109 110 110 111 /// Get the record key for this operation 111 - pub fn rkey(&self) -> &RecordKey<Rkey<'a>> { 112 + pub fn rkey(&self) -> &RecordKey<Rkey<S>> { 112 113 match self { 113 114 RecordWriteOp::Create { rkey, .. } => rkey, 114 115 RecordWriteOp::Update { rkey, .. } => rkey,
+110 -98
crates/jacquard-repo/src/repo.rs
··· 10 10 use crate::storage::BlockStore; 11 11 use bytes::Bytes; 12 12 use cid::Cid as IpldCid; 13 - use jacquard_common::IntoStatic; 13 + use jacquard_common::BosStr; 14 14 use jacquard_common::types::cid::CidLink; 15 15 use jacquard_common::types::recordkey::RecordKeyType; 16 16 use jacquard_common::types::string::{Datetime, Did, Nsid, RecordKey, Tid}; 17 17 use jacquard_common::types::tid::Ticker; 18 18 use smol_str::format_smolstr; 19 19 use std::collections::BTreeMap; 20 + use std::convert::Infallible; 20 21 use std::fmt::{self, Display, Formatter}; 21 22 use std::path::Path; 23 + use std::str::FromStr; 22 24 use std::sync::Arc; 23 25 24 26 /// Commit data for repository updates ··· 65 67 /// 66 68 /// Converts this commit into a `FirehoseCommit` with `prev_data` field 67 69 /// and relevant blocks for inductive validation. 68 - pub async fn to_firehose_commit( 70 + pub async fn to_firehose_commit<S: BosStr + Clone>( 69 71 &self, 70 - repo: &Did<'_>, 72 + repo: &Did<S>, 71 73 seq: i64, 72 74 time: Datetime, 73 - ops: Vec<RepoOp<'static>>, 74 - blobs: Vec<CidLink<'static>>, 75 - ) -> Result<FirehoseCommit<'static>> { 75 + ops: Vec<RepoOp<S>>, 76 + blobs: Vec<CidLink<S>>, 77 + ) -> Result<FirehoseCommit<S>> { 76 78 let mut proof_blocks = self.blocks.clone(); 77 79 proof_blocks.append(&mut self.relevant_blocks.clone()); 78 80 // Convert relevant blocks to CAR format 79 81 let blocks_car = crate::car::write_car_bytes(self.cid, proof_blocks).await?; 80 82 81 83 Ok(FirehoseCommit { 82 - repo: repo.clone().into_static(), 84 + repo: repo.clone(), 83 85 rev: self.rev.clone(), 84 86 seq, 85 87 since: Some(self.since.clone().unwrap_or_else(|| self.rev.clone())), ··· 124 126 /// # Ok(()) 125 127 /// # } 126 128 /// ``` 127 - pub struct Repository<S: BlockStore> { 128 - mst: Mst<S>, 129 - storage: Arc<S>, 130 - commit: Commit<'static>, 129 + pub struct Repository<S: BosStr, BS: BlockStore> { 130 + mst: Mst<BS>, 131 + storage: Arc<BS>, 132 + commit: Commit<S>, 131 133 commit_cid: IpldCid, 132 134 } 133 135 134 - impl<S: BlockStore + Sync + 'static> Repository<S> { 136 + impl<S, BS: BlockStore + Sync + 'static> Repository<S, BS> 137 + where 138 + S: BosStr + Clone + serde::Serialize + serde::de::DeserializeOwned + FromStr<Err = Infallible>, 139 + { 135 140 /// Create repository from existing components 136 141 /// 137 142 /// Static constructor for when you already have the MST, commit, and CID. 138 - pub fn new(storage: Arc<S>, mst: Mst<S>, commit: Commit<'static>, commit_cid: IpldCid) -> Self { 143 + pub fn new(storage: Arc<BS>, mst: Mst<BS>, commit: Commit<S>, commit_cid: IpldCid) -> Self { 139 144 Self { 140 145 storage, 141 146 mst, ··· 145 150 } 146 151 147 152 /// Load repository from commit CID 148 - pub async fn from_commit(storage: Arc<S>, commit_cid: &IpldCid) -> Result<Self> { 153 + pub async fn from_commit(storage: Arc<BS>, commit_cid: &IpldCid) -> Result<Self> { 149 154 let commit_bytes = storage 150 155 .get(commit_cid) 151 156 .await? ··· 154 159 .with_help("Commit must be applied to storage before loading repository - use apply_commit() or ensure commit is persisted") 155 160 })?; 156 161 157 - let commit = Commit::from_cbor(&commit_bytes)?; 162 + let commit = Commit::<S>::from_cbor(&commit_bytes)?; 158 163 let mst_root = commit.data(); 159 164 160 165 let mst = Mst::load(storage.clone(), *mst_root, None); ··· 162 167 Ok(Self { 163 168 mst, 164 169 storage, 165 - commit: commit.into_static(), 170 + commit: commit, 166 171 commit_cid: *commit_cid, 167 172 }) 168 173 } ··· 174 179 /// 175 180 /// This does NOT persist to storage - use `create_from_commit` or `create` for that. 176 181 pub async fn format_init_commit<K>( 177 - storage: Arc<S>, 178 - did: Did<'static>, 182 + storage: Arc<BS>, 183 + did: Did<S>, 179 184 signing_key: &K, 180 - initial_writes: Option<&[RecordWriteOp<'_>]>, 185 + initial_writes: Option<&[RecordWriteOp<'_, S>]>, 181 186 ) -> Result<CommitData> 182 187 where 183 188 K: SigningKey, ··· 240 245 /// Create repository from CommitData 241 246 /// 242 247 /// Applies the commit to storage and loads the repository from it. 243 - pub async fn create_from_commit(storage: Arc<S>, commit_data: CommitData) -> Result<Self> { 248 + pub async fn create_from_commit(storage: Arc<BS>, commit_data: CommitData) -> Result<Self> { 244 249 let commit_cid = commit_data.cid; 245 250 storage.apply_commit(commit_data).await?; 246 251 Self::from_commit(storage, &commit_cid).await ··· 250 255 /// 251 256 /// Convenience method that formats an initial commit and applies it to storage. 252 257 pub async fn create<K>( 253 - storage: Arc<S>, 254 - did: Did<'static>, 258 + storage: Arc<BS>, 259 + did: Did<S>, 255 260 signing_key: &K, 256 - initial_writes: Option<&[RecordWriteOp<'_>]>, 261 + initial_writes: Option<&[RecordWriteOp<'_, S>]>, 257 262 ) -> Result<Self> 258 263 where 259 264 K: SigningKey, ··· 266 271 /// Get a record by collection and rkey 267 272 pub async fn get_record<T: RecordKeyType>( 268 273 &self, 269 - collection: &Nsid<'_>, 274 + collection: &Nsid<S>, 270 275 rkey: &RecordKey<T>, 271 276 ) -> Result<Option<IpldCid>> { 272 277 let key = format!("{}/{}", collection.as_ref(), rkey.as_ref()); ··· 276 281 /// Create a record (error if exists) 277 282 pub async fn create_record<T: RecordKeyType>( 278 283 &mut self, 279 - collection: &Nsid<'_>, 284 + collection: &Nsid<S>, 280 285 rkey: &RecordKey<T>, 281 286 record_cid: IpldCid, 282 287 ) -> Result<()> { ··· 293 298 /// Update a record (error if not exists, returns previous CID) 294 299 pub async fn update_record<T: RecordKeyType>( 295 300 &mut self, 296 - collection: &Nsid<'_>, 301 + collection: &Nsid<S>, 297 302 rkey: &RecordKey<T>, 298 303 record_cid: IpldCid, 299 304 ) -> Result<IpldCid> { ··· 312 317 /// Delete a record (error if not exists, returns deleted CID) 313 318 pub async fn delete_record<T: RecordKeyType>( 314 319 &mut self, 315 - collection: &Nsid<'_>, 320 + collection: &Nsid<S>, 316 321 rkey: &RecordKey<T>, 317 322 ) -> Result<IpldCid> { 318 323 let key = format!("{}/{}", collection.as_ref(), rkey.as_ref()); ··· 359 364 /// Returns `(ops, CommitData)` - ops are needed for `to_firehose_commit()`. 360 365 pub async fn create_commit<K>( 361 366 &mut self, 362 - ops: &[RecordWriteOp<'_>], 363 - did: &Did<'_>, 367 + ops: &[RecordWriteOp<'_, S>], 368 + did: &Did<S>, 364 369 prev: Option<IpldCid>, 365 370 signing_key: &K, 366 - ) -> Result<(Vec<RepoOp<'static>>, CommitData)> 371 + ) -> Result<(Vec<RepoOp<S>>, CommitData)> 367 372 where 368 373 K: SigningKey, 374 + S: FromStr<Err = Infallible>, 369 375 { 370 376 // Step 1: Apply all write operations to build new MST and collect leaf blocks 371 377 let mut updated_tree = self.mst.clone(); ··· 463 469 let diff = self.mst.diff(&updated_tree).await?; 464 470 465 471 // Step 3: Extract everything we need from diff 466 - let repo_ops = diff 467 - .to_repo_ops() 468 - .into_iter() 469 - .map(|op| op.into_static()) 470 - .collect(); 472 + let repo_ops = diff.to_repo_ops().into_iter().collect(); 471 473 472 474 // Step 4: Build blocks and relevant_blocks collections using diff tracking 473 475 // ··· 506 508 507 509 // Step 5: Create and sign commit 508 510 let rev = Ticker::new().next(Some(self.commit.rev.clone())); 509 - let commit = Commit::new_unsigned(did.clone().into_static(), data, rev.clone(), prev) 510 - .sign(signing_key)?; 511 + let commit = 512 + Commit::new_unsigned(did.clone(), data, rev.clone(), prev).sign(signing_key)?; 511 513 512 514 let commit_cbor = commit.to_cbor()?; 513 515 let commit_cid = crate::mst::util::compute_cid(&commit_cbor)?; ··· 555 557 RepoError::not_found("commit block", &commit_cid) 556 558 .with_help("Commit block should have been persisted by apply_commit() - this indicates a storage inconsistency") 557 559 })?; 558 - let commit = Commit::from_cbor(&commit_bytes)?; 560 + let commit = Commit::<S>::from_cbor(&commit_bytes)?; 559 561 560 - self.commit = commit.into_static(); 562 + self.commit = commit; 561 563 self.commit_cid = commit_cid; 562 564 563 565 // Reload MST from new root ··· 573 575 /// record operations (e.g., `create_record()`, `update_record()`, `delete_record()`). 574 576 pub async fn commit<K>( 575 577 &mut self, 576 - did: &Did<'_>, 578 + did: &Did<S>, 577 579 prev: Option<IpldCid>, 578 580 signing_key: &K, 579 - ) -> Result<(Vec<RepoOp<'static>>, IpldCid)> 581 + ) -> Result<(Vec<RepoOp<S>>, IpldCid)> 580 582 where 581 583 K: SigningKey, 582 584 { ··· 590 592 } 591 593 592 594 /// Get the underlying MST 593 - pub fn mst(&self) -> &Mst<S> { 595 + pub fn mst(&self) -> &Mst<BS> { 594 596 &self.mst 595 597 } 596 598 597 599 /// Get reference to the storage 598 - pub fn storage(&self) -> &Arc<S> { 600 + pub fn storage(&self) -> &Arc<BS> { 599 601 &self.storage 600 602 } 601 603 602 604 /// Get the current commit 603 - pub fn current_commit(&self) -> &Commit<'static> { 605 + pub fn current_commit(&self) -> &Commit<S> { 604 606 &self.commit 605 607 } 606 608 ··· 610 612 } 611 613 612 614 /// Get the DID from the current commit 613 - pub fn did(&self) -> &Did<'_> { 615 + pub fn did(&self) -> &Did<S> { 614 616 self.commit.did() 615 617 } 616 618 } 617 619 618 - impl<S: BlockStore> Display for Repository<S> { 620 + impl<S: BosStr, BS: BlockStore> Display for Repository<S, BS> { 619 621 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 620 622 use crate::mst::tree::short_cid; 621 623 622 624 writeln!(f, "Repository {{")?; 623 - writeln!(f, " DID: {}", self.commit.did())?; 625 + writeln!(f, " DID: {}", self.commit.did)?; 624 626 writeln!(f, " Commit: {}", short_cid(&self.commit_cid))?; 625 627 writeln!(f, " Rev: {}", self.commit.rev)?; 626 - writeln!(f, " Data: {}", short_cid(self.commit.data()))?; 628 + writeln!(f, " Data: {}", short_cid(&self.commit.data))?; 627 629 writeln!(f, " MST:")?; 628 630 629 631 // Format MST with indentation ··· 642 644 643 645 use super::*; 644 646 use crate::storage::MemoryBlockStore; 645 - use jacquard_common::types::{ 646 - crypto::{KeyCodec, PublicKey}, 647 - recordkey::Rkey, 648 - value::RawData, 647 + use jacquard_common::{ 648 + IntoStatic, 649 + types::{ 650 + crypto::{KeyCodec, PublicKey}, 651 + recordkey::Rkey, 652 + value::RawData, 653 + }, 649 654 }; 650 655 use smol_str::SmolStr; 651 656 ··· 676 681 record 677 682 } 678 683 679 - async fn create_test_repo(storage: Arc<MemoryBlockStore>) -> Repository<MemoryBlockStore> { 684 + async fn create_test_repo( 685 + storage: Arc<MemoryBlockStore>, 686 + ) -> Repository<SmolStr, MemoryBlockStore> { 680 687 let did = Did::new("did:plc:test").unwrap(); 681 688 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 682 689 ··· 701 708 let storage = Arc::new(MemoryBlockStore::new()); 702 709 let mut repo = create_test_repo(storage.clone()).await; 703 710 704 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 705 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 711 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 712 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 706 713 707 714 let ops = vec![RecordWriteOp::Create { 708 715 collection: collection.clone().into_static(), ··· 710 717 record: make_test_record(1), 711 718 }]; 712 719 713 - let did = Did::new("did:plc:test").unwrap(); 720 + let did = Did::new("did:plc:test").unwrap().into_static(); 714 721 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 715 722 let (repo_ops, commit_data) = repo 716 723 .create_commit( ··· 738 745 let storage = Arc::new(MemoryBlockStore::new()); 739 746 let mut repo = create_test_repo(storage).await; 740 747 741 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 742 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 748 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 749 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 743 750 let cid = make_test_cid(1); 744 751 745 752 repo.create_record(&collection, &rkey, cid).await.unwrap(); ··· 755 762 let storage = Arc::new(MemoryBlockStore::new()); 756 763 let mut repo = create_test_repo(storage).await; 757 764 758 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 759 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 765 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 766 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 760 767 let cid1 = make_test_cid(1); 761 768 let cid2 = make_test_cid(2); 762 769 ··· 774 781 let storage = Arc::new(MemoryBlockStore::new()); 775 782 let mut repo = create_test_repo(storage).await; 776 783 777 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 778 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 784 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 785 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 779 786 let cid = make_test_cid(1); 780 787 781 788 let result = repo.update_record(&collection, &rkey, cid).await; ··· 787 794 let storage = Arc::new(MemoryBlockStore::new()); 788 795 let mut repo = create_test_repo(storage).await; 789 796 790 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 791 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 797 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 798 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 792 799 let cid = make_test_cid(1); 793 800 794 801 repo.create_record(&collection, &rkey, cid).await.unwrap(); ··· 805 812 let storage = Arc::new(MemoryBlockStore::new()); 806 813 let mut repo = create_test_repo(storage).await; 807 814 808 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 809 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 815 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 816 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 810 817 811 818 let result = repo.delete_record(&collection, &rkey).await; 812 819 assert!(result.is_err()); ··· 817 824 let storage = Arc::new(MemoryBlockStore::new()); 818 825 let mut repo = create_test_repo(storage.clone()).await; 819 826 820 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 821 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 827 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 828 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 822 829 let cid = make_test_cid(1); 823 830 824 831 repo.create_record(&collection, &rkey, cid).await.unwrap(); ··· 827 834 repo.mst.persist().await.unwrap(); 828 835 829 836 // Create commit (need a signing key for this test) 830 - let did = Did::new("did:plc:test").unwrap(); 837 + let did = Did::new("did:plc:test").unwrap().into_static(); 831 838 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 832 839 let (_, commit_cid) = repo.commit(&did, None, &signing_key).await.unwrap(); 833 840 ··· 843 850 let storage = Arc::new(MemoryBlockStore::new()); 844 851 let mut repo = create_test_repo(storage.clone()).await; 845 852 846 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 847 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 853 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 854 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 848 855 let cid = make_test_cid(1); 849 856 850 857 repo.create_record(&collection, &rkey, cid).await.unwrap(); 851 858 repo.mst.persist().await.unwrap(); 852 859 853 - let did = Did::new("did:plc:test").unwrap(); 860 + let did = Did::new("did:plc:test").unwrap().into_static(); 854 861 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 855 862 let (_, commit_cid) = repo.commit(&did, None, &signing_key).await.unwrap(); 856 863 ··· 860 867 861 868 // Verify commit can be deserialized 862 869 let bytes = commit_bytes.unwrap(); 863 - let commit = Commit::from_cbor(&bytes).unwrap(); 870 + let commit = Commit::<SmolStr>::from_cbor(&bytes).unwrap(); 864 871 assert_eq!(commit.did().as_ref(), did.as_ref()); 865 872 let root_cid = repo.mst.root().await.unwrap(); 866 873 assert_eq!(commit.data(), &root_cid); ··· 871 878 let storage = Arc::new(MemoryBlockStore::new()); 872 879 let mut repo = create_test_repo(storage.clone()).await; 873 880 874 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 875 - let rkey = RecordKey(Rkey::new("test1").unwrap()); 881 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 882 + let rkey = RecordKey(Rkey::new("test1").unwrap()).into_static(); 876 883 let cid1 = make_test_cid(1); 877 884 let cid2 = make_test_cid(2); 878 885 ··· 899 906 let storage = Arc::new(MemoryBlockStore::new()); 900 907 let mut repo = create_test_repo(storage.clone()).await; 901 908 902 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 909 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 903 910 let mut ticker = Ticker::new(); 904 911 905 912 // Add 100 records 906 913 let mut records = Vec::new(); 907 914 for i in 0..100 { 908 915 let tid_str = ticker.next(None).into_static(); 909 - let rkey = RecordKey(Rkey::from_str(tid_str.as_str()).unwrap()); 916 + let rkey: RecordKey<Rkey<SmolStr>> = 917 + RecordKey(Rkey::<SmolStr>::from_str(tid_str.as_str()).unwrap()).into_static(); 910 918 let cid = make_test_cid((i % 256) as u8); 911 919 repo.create_record(&collection, &rkey, cid).await.unwrap(); 912 920 records.push((rkey, cid)); ··· 946 954 let storage = Arc::new(MemoryBlockStore::new()); 947 955 let mut repo = create_test_repo(storage.clone()).await; 948 956 949 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 950 - let rkey = RecordKey(Rkey::new("abc123").unwrap()); 957 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 958 + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); 951 959 let cid = make_test_cid(1); 952 960 953 961 repo.create_record(&collection, &rkey, cid).await.unwrap(); 954 962 repo.mst.persist().await.unwrap(); 955 963 956 - let did = Did::new("did:plc:test").unwrap(); 964 + let did = Did::new("did:plc:test").unwrap().into_static(); 957 965 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 958 966 959 967 // Get public key from signing key ··· 968 976 969 977 // Load commit and verify signature 970 978 let commit_bytes = storage.get(&commit_cid).await.unwrap().unwrap(); 971 - let commit = Commit::from_cbor(&commit_bytes).unwrap(); 979 + let commit = Commit::<SmolStr>::from_cbor(&commit_bytes).unwrap(); 972 980 973 981 // Signature verification should succeed 974 982 commit.verify(&pubkey).unwrap(); ··· 979 987 let storage = Arc::new(MemoryBlockStore::new()); 980 988 let mut repo = create_test_repo(storage.clone()).await; 981 989 982 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 983 - let did = Did::new("did:plc:test").unwrap(); 990 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 991 + let did = Did::new("did:plc:test").unwrap().into_static(); 984 992 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 985 993 986 994 // Add some records and commit 987 995 let mut records = Vec::new(); 988 996 for i in 0..10 { 989 - let rkey = RecordKey(Rkey::from_str(&format!("record{}", i)).unwrap()); 997 + let rkey: RecordKey<Rkey<SmolStr>> = RecordKey( 998 + Rkey::<SmolStr>::from_str(&format!("record{}", i)) 999 + .unwrap() 1000 + .into_static(), 1001 + ); 990 1002 let cid = make_test_cid(i as u8); 991 1003 repo.create_record(&collection, &rkey, cid).await.unwrap(); 992 1004 records.push((rkey, cid)); ··· 1021 1033 let storage = Arc::new(MemoryBlockStore::new()); 1022 1034 let mut repo = create_test_repo(storage.clone()).await; 1023 1035 1024 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 1025 - let rkey1 = RecordKey(Rkey::new("test1").unwrap()); 1026 - let rkey2 = RecordKey(Rkey::new("test2").unwrap()); 1036 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 1037 + let rkey1 = RecordKey(Rkey::new("test1").unwrap()).into_static(); 1038 + let rkey2 = RecordKey(Rkey::new("test2").unwrap()).into_static(); 1027 1039 1028 - let did = Did::new("did:plc:test").unwrap(); 1040 + let did = Did::new("did:plc:test").unwrap().into_static(); 1029 1041 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 1030 1042 1031 1043 // Create records with actual data ··· 1085 1097 let storage = Arc::new(MemoryBlockStore::new()); 1086 1098 let mut repo = create_test_repo(storage.clone()).await; 1087 1099 1088 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 1089 - let rkey1 = RecordKey(Rkey::new("post1").unwrap()); 1090 - let rkey2 = RecordKey(Rkey::new("post2").unwrap()); 1100 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 1101 + let rkey1 = RecordKey(Rkey::new("post1").unwrap()).into_static(); 1102 + let rkey2 = RecordKey(Rkey::new("post2").unwrap()).into_static(); 1091 1103 1092 1104 // Create records with actual data 1093 1105 let ops = vec![ ··· 1104 1116 ]; 1105 1117 1106 1118 // Format commit 1107 - let did = Did::new("did:plc:test").unwrap(); 1119 + let did = Did::new("did:plc:test").unwrap().into_static(); 1108 1120 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 1109 1121 let (repo_ops, commit_data) = repo 1110 1122 .create_commit( ··· 1159 1171 let storage = Arc::new(MemoryBlockStore::new()); 1160 1172 let mut repo = create_test_repo(storage.clone()).await; 1161 1173 1162 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 1174 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 1163 1175 1164 1176 // Pre-populate with some records 1165 - let rkey1 = RecordKey(Rkey::new("existing1").unwrap()); 1166 - let rkey2 = RecordKey(Rkey::new("existing2").unwrap()); 1167 - let rkey3 = RecordKey(Rkey::new("existing3").unwrap()); 1177 + let rkey1 = RecordKey(Rkey::new("existing1").unwrap()).into_static(); 1178 + let rkey2 = RecordKey(Rkey::new("existing2").unwrap()).into_static(); 1179 + let rkey3 = RecordKey(Rkey::new("existing3").unwrap()).into_static(); 1168 1180 1169 - let did = Did::new("did:plc:test").unwrap(); 1181 + let did = Did::new("did:plc:test").unwrap().into_static(); 1170 1182 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 1171 1183 1172 1184 let create_ops = vec![ ··· 1203 1215 repo.apply_commit(commit_data).await.unwrap(); 1204 1216 1205 1217 // Batch operation: create new, update existing, delete existing 1206 - let new_rkey = RecordKey(Rkey::new("new1").unwrap()); 1218 + let new_rkey = RecordKey(Rkey::new("new1").unwrap()).into_static(); 1207 1219 let ops = vec![ 1208 1220 RecordWriteOp::Create { 1209 1221 collection: collection.clone(),
+22 -19
crates/jacquard-repo/tests/large_proof_tests.rs
··· 15 15 use jacquard_repo::storage::{BlockStore, MemoryBlockStore}; 16 16 use rand::Rng; 17 17 use rand::seq::SliceRandom; 18 - use smol_str::SmolStr; 18 + use smol_str::{SmolStr, ToSmolStr}; 19 19 use std::collections::{BTreeMap, HashMap}; 20 20 use std::sync::Arc; 21 21 ··· 50 50 } 51 51 } 52 52 53 - async fn create_test_repo(storage: Arc<MemoryBlockStore>) -> Repository<MemoryBlockStore> { 53 + async fn create_test_repo(storage: Arc<MemoryBlockStore>) -> Repository<SmolStr, MemoryBlockStore> { 54 54 let did = Did::new("did:plc:stresstest").unwrap(); 55 55 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 56 56 ··· 61 61 62 62 /// Track existing records for generating realistic updates/deletes 63 63 struct RecordTracker { 64 - records: HashMap<String, u32>, 64 + records: HashMap<SmolStr, u32>, 65 65 ticker: Ticker, 66 66 } 67 67 ··· 73 73 } 74 74 } 75 75 76 - fn gen_new_rkey(&mut self) -> String { 77 - self.ticker.next(None).into_static().to_string() 76 + fn gen_new_rkey(&mut self) -> SmolStr { 77 + self.ticker.next(None).into_static().to_smolstr() 78 78 } 79 79 80 - fn pick_random_existing<R: Rng>(&self, rng: &mut R) -> Option<String> { 80 + fn pick_random_existing<R: Rng>(&self, rng: &mut R) -> Option<SmolStr> { 81 81 let keys: Vec<_> = self.records.keys().cloned().collect(); 82 82 keys.choose(rng).cloned() 83 83 } 84 84 85 - fn add(&mut self, rkey: String, counter: u32) { 85 + fn add(&mut self, rkey: SmolStr, counter: u32) { 86 86 self.records.insert(rkey, counter); 87 87 } 88 88 ··· 97 97 98 98 #[derive(Debug, Clone)] 99 99 enum TestOp { 100 - Create { rkey: String, counter: u32 }, 101 - Update { rkey: String, counter: u32 }, 102 - Delete { rkey: String }, 100 + Create { rkey: SmolStr, counter: u32 }, 101 + Update { rkey: SmolStr, counter: u32 }, 102 + Delete { rkey: SmolStr }, 103 103 } 104 104 105 105 fn generate_creates_only<R: Rng>( ··· 167 167 ops 168 168 } 169 169 170 - fn test_ops_to_record_writes(ops: Vec<TestOp>, collection: &Nsid) -> Vec<RecordWriteOp<'static>> { 170 + fn test_ops_to_record_writes( 171 + ops: Vec<TestOp>, 172 + collection: &Nsid, 173 + ) -> Vec<RecordWriteOp<'_, SmolStr>> { 171 174 let collection_static = collection.clone().into_static(); 172 175 ops.into_iter() 173 176 .map(|op| match op { 174 177 TestOp::Create { rkey, counter } => RecordWriteOp::Create { 175 178 collection: collection_static.clone(), 176 - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), 179 + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), 177 180 record: make_test_record(counter, "Random post"), 178 181 }, 179 182 TestOp::Update { rkey, counter } => RecordWriteOp::Update { 180 183 collection: collection_static.clone(), 181 - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), 184 + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), 182 185 record: make_test_record(counter, "Updated post"), 183 186 prev: None, 184 187 }, 185 188 TestOp::Delete { rkey } => RecordWriteOp::Delete { 186 189 collection: collection_static.clone(), 187 - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), 190 + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), 188 191 prev: None, 189 192 }, 190 193 }) ··· 196 199 let storage = Arc::new(MemoryBlockStore::new()); 197 200 let mut repo = create_test_repo(storage.clone()).await; 198 201 199 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 200 - let did = Did::new("did:plc:stresstest").unwrap(); 202 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 203 + let did = Did::new("did:plc:stresstest").unwrap().into_static(); 201 204 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 202 205 let pubkey = get_public_key(&signing_key); 203 206 ··· 308 311 let storage = Arc::new(MemoryBlockStore::new()); 309 312 let mut repo = create_test_repo(storage.clone()).await; 310 313 311 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 312 - let did = Did::new("did:plc:stresstest").unwrap(); 314 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 315 + let did = Did::new("did:plc:stresstest").unwrap().into_static(); 313 316 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 314 317 let pubkey = get_public_key(&signing_key); 315 318 ··· 408 411 repo.current_commit_cid() 409 412 ); 410 413 411 - let collection = Nsid::new("app.bsky.feed.post").unwrap(); 414 + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); 412 415 let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); 413 416 let pubkey = get_public_key(&signing_key); 414 417 let did = repo.did().clone().into_static();