Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

small bug fixes, code dedup

+3433 -3409
+2 -478
Cargo.lock
··· 1067 1067 version = "1.11.0" 1068 1068 source = "registry+https://github.com/rust-lang/crates.io-index" 1069 1069 checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 1070 - dependencies = [ 1071 - "serde", 1072 - ] 1073 1070 1074 1071 [[package]] 1075 1072 name = "bytes-utils" ··· 1377 1374 version = "1.2.0" 1378 1375 source = "registry+https://github.com/rust-lang/crates.io-index" 1379 1376 checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 1380 - 1381 - [[package]] 1382 - name = "crossbeam-channel" 1383 - version = "0.5.15" 1384 - source = "registry+https://github.com/rust-lang/crates.io-index" 1385 - checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 1386 - dependencies = [ 1387 - "crossbeam-utils", 1388 - ] 1389 1377 1390 1378 [[package]] 1391 1379 name = "crossbeam-epoch" ··· 2070 2058 checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 2071 2059 2072 2060 [[package]] 2073 - name = "futf" 2074 - version = "0.1.5" 2075 - source = "registry+https://github.com/rust-lang/crates.io-index" 2076 - checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 2077 - dependencies = [ 2078 - "mac", 2079 - "new_debug_unreachable", 2080 - ] 2081 - 2082 - [[package]] 2083 2061 name = "futures" 2084 2062 version = "0.3.31" 2085 2063 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2285 2263 ] 2286 2264 2287 2265 [[package]] 2288 - name = "gloo-storage" 2289 - version = "0.3.0" 2290 - source = "registry+https://github.com/rust-lang/crates.io-index" 2291 - checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 2292 - dependencies = [ 2293 - "gloo-utils", 2294 - "js-sys", 2295 - "serde", 2296 - "serde_json", 2297 - "thiserror 1.0.69", 2298 - "wasm-bindgen", 2299 - "web-sys", 2300 - ] 2301 - 2302 - [[package]] 2303 2266 name = "gloo-timers" 2304 2267 version = "0.3.0" 2305 2268 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2309 2272 "futures-core", 2310 2273 "js-sys", 2311 2274 "wasm-bindgen", 2312 - ] 2313 - 2314 - [[package]] 2315 - name = "gloo-utils" 2316 - version = "0.2.0" 2317 - source = "registry+https://github.com/rust-lang/crates.io-index" 2318 - checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 2319 - dependencies = [ 2320 - "js-sys", 2321 - "serde", 2322 - "serde_json", 2323 - "wasm-bindgen", 2324 - "web-sys", 2325 2275 ] 2326 2276 2327 2277 [[package]] ··· 2587 2537 ] 2588 2538 2589 2539 [[package]] 2590 - name = "html5ever" 2591 - version = "0.27.0" 2592 - source = "registry+https://github.com/rust-lang/crates.io-index" 2593 - checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" 2594 - dependencies = [ 2595 - "log", 2596 - "mac", 2597 - "markup5ever", 2598 - "proc-macro2", 2599 - "quote", 2600 - "syn 2.0.111", 2601 - ] 2602 - 2603 - [[package]] 2604 2540 name = "http" 2605 2541 version = "0.2.12" 2606 2542 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3128 3064 checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" 3129 3065 3130 3066 [[package]] 3131 - name = "jacquard" 3132 - version = "0.9.5" 3133 - source = "registry+https://github.com/rust-lang/crates.io-index" 3134 - checksum = "f7c1fdbcf1153e6e6b87fde20036c1ffe7473c4852f1c6369bc4ef1fe47ccb9f" 3135 - dependencies = [ 3136 - "bytes", 3137 - "getrandom 0.2.16", 3138 - "gloo-storage", 3139 - "http 1.4.0", 3140 - "jacquard-api", 3141 - "jacquard-common", 3142 - "jacquard-derive", 3143 - "jacquard-identity", 3144 - "jacquard-oauth", 3145 - "jose-jwk", 3146 - "miette", 3147 - "regex", 3148 - "regex-lite", 3149 - "reqwest", 3150 - "serde", 3151 - "serde_html_form", 3152 - "serde_json", 3153 - "smol_str", 3154 - "thiserror 2.0.17", 3155 - "tokio", 3156 - "trait-variant", 3157 - "url", 3158 - "webpage", 3159 - ] 3160 - 3161 - [[package]] 3162 - name = "jacquard-api" 3163 - version = "0.9.5" 3164 - source = "registry+https://github.com/rust-lang/crates.io-index" 3165 - checksum = "4979fb1848c1dd7ac8fd12745bc71f56f6da61374407d5f9b06005467a954e5a" 3166 - dependencies = [ 3167 - "bon", 3168 - "bytes", 3169 - "jacquard-common", 3170 - "jacquard-derive", 3171 - "jacquard-lexicon", 3172 - "miette", 3173 - "rustversion", 3174 - "serde", 3175 - "serde_bytes", 3176 - "serde_ipld_dagcbor", 3177 - "thiserror 2.0.17", 3178 - "unicode-segmentation", 3179 - ] 3180 - 3181 - [[package]] 3182 - name = "jacquard-axum" 3183 - version = "0.9.6" 3184 - source = "registry+https://github.com/rust-lang/crates.io-index" 3185 - checksum = "ed99b0dc0cd54189bebb83d5d5cc5ac2889f62ede9729a6ead9035073d111bc9" 3186 - dependencies = [ 3187 - "axum", 3188 - "bytes", 3189 - "jacquard", 3190 - "jacquard-common", 3191 - "jacquard-derive", 3192 - "jacquard-identity", 3193 - "miette", 3194 - "multibase", 3195 - "serde", 3196 - "serde_html_form", 3197 - "serde_json", 3198 - "thiserror 2.0.17", 3199 - "tokio", 3200 - "tower-http", 3201 - "tracing", 3202 - ] 3203 - 3204 - [[package]] 3205 3067 name = "jacquard-common" 3206 3068 version = "0.9.5" 3207 3069 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3257 3119 ] 3258 3120 3259 3121 [[package]] 3260 - name = "jacquard-identity" 3261 - version = "0.9.5" 3262 - source = "registry+https://github.com/rust-lang/crates.io-index" 3263 - checksum = "e7aaefa819fa4213cf59f180dba932f018a7cd0599582fd38474ee2a38c16cf2" 3264 - dependencies = [ 3265 - "bon", 3266 - "bytes", 3267 - "hickory-resolver", 3268 - "http 1.4.0", 3269 - "jacquard-api", 3270 - "jacquard-common", 3271 - "jacquard-lexicon", 3272 - "miette", 3273 - "mini-moka-wasm", 3274 - "n0-future", 3275 - "percent-encoding", 3276 - "reqwest", 3277 - "serde", 3278 - "serde_html_form", 3279 - "serde_json", 3280 - "thiserror 2.0.17", 3281 - "tokio", 3282 - "trait-variant", 3283 - "url", 3284 - "urlencoding", 3285 - ] 3286 - 3287 - [[package]] 3288 3122 name = "jacquard-lexicon" 3289 3123 version = "0.9.5" 3290 3124 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3312 3146 ] 3313 3147 3314 3148 [[package]] 3315 - name = "jacquard-oauth" 3316 - version = "0.9.6" 3317 - source = "registry+https://github.com/rust-lang/crates.io-index" 3318 - checksum = "68bf0b0e061d85b09cfa78588dc098918d5b62f539a719165c6a806a1d2c0ef2" 3319 - dependencies = [ 3320 - "base64 0.22.1", 3321 - "bytes", 3322 - "chrono", 3323 - "dashmap", 3324 - "elliptic-curve 0.13.8", 3325 - "http 1.4.0", 3326 - "jacquard-common", 3327 - "jacquard-identity", 3328 - "jose-jwa", 3329 - "jose-jwk", 3330 - "miette", 3331 - "p256 0.13.2", 3332 - "rand 0.8.5", 3333 - "serde", 3334 - "serde_html_form", 3335 - "serde_json", 3336 - "sha2", 3337 - "smol_str", 3338 - "thiserror 2.0.17", 3339 - "tokio", 3340 - "trait-variant", 3341 - "url", 3342 - ] 3343 - 3344 - [[package]] 3345 3149 name = "jacquard-repo" 3346 3150 version = "0.9.6" 3347 3151 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3379 3183 ] 3380 3184 3381 3185 [[package]] 3382 - name = "jose-b64" 3383 - version = "0.1.2" 3384 - source = "registry+https://github.com/rust-lang/crates.io-index" 3385 - checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" 3386 - dependencies = [ 3387 - "base64ct", 3388 - "serde", 3389 - "subtle", 3390 - "zeroize", 3391 - ] 3392 - 3393 - [[package]] 3394 - name = "jose-jwa" 3395 - version = "0.1.2" 3396 - source = "registry+https://github.com/rust-lang/crates.io-index" 3397 - checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" 3398 - dependencies = [ 3399 - "serde", 3400 - ] 3401 - 3402 - [[package]] 3403 - name = "jose-jwk" 3404 - version = "0.1.2" 3405 - source = "registry+https://github.com/rust-lang/crates.io-index" 3406 - checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" 3407 - dependencies = [ 3408 - "jose-b64", 3409 - "jose-jwa", 3410 - "p256 0.13.2", 3411 - "p384", 3412 - "rsa", 3413 - "serde", 3414 - "zeroize", 3415 - ] 3416 - 3417 - [[package]] 3418 3186 name = "js-sys" 3419 3187 version = "0.3.83" 3420 3188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3425 3193 ] 3426 3194 3427 3195 [[package]] 3428 - name = "jsonwebtoken" 3429 - version = "10.2.0" 3430 - source = "registry+https://github.com/rust-lang/crates.io-index" 3431 - checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" 3432 - dependencies = [ 3433 - "base64 0.22.1", 3434 - "ed25519-dalek", 3435 - "getrandom 0.2.16", 3436 - "hmac", 3437 - "js-sys", 3438 - "p256 0.13.2", 3439 - "p384", 3440 - "pem", 3441 - "rand 0.8.5", 3442 - "rsa", 3443 - "serde", 3444 - "serde_json", 3445 - "sha2", 3446 - "signature 2.2.0", 3447 - "simple_asn1", 3448 - ] 3449 - 3450 - [[package]] 3451 3196 name = "k256" 3452 3197 version = "0.13.4" 3453 3198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3594 3339 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 3595 3340 3596 3341 [[package]] 3597 - name = "mac" 3598 - version = "0.1.1" 3599 - source = "registry+https://github.com/rust-lang/crates.io-index" 3600 - checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 3601 - 3602 - [[package]] 3603 - name = "markup5ever" 3604 - version = "0.12.1" 3605 - source = "registry+https://github.com/rust-lang/crates.io-index" 3606 - checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" 3607 - dependencies = [ 3608 - "log", 3609 - "phf", 3610 - "phf_codegen", 3611 - "string_cache", 3612 - "string_cache_codegen", 3613 - "tendril", 3614 - ] 3615 - 3616 - [[package]] 3617 - name = "markup5ever_rcdom" 3618 - version = "0.3.0" 3619 - source = "registry+https://github.com/rust-lang/crates.io-index" 3620 - checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" 3621 - dependencies = [ 3622 - "html5ever", 3623 - "markup5ever", 3624 - "tendril", 3625 - "xml5ever", 3626 - ] 3627 - 3628 - [[package]] 3629 3342 name = "match-lookup" 3630 3343 version = "0.1.1" 3631 3344 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3752 3465 ] 3753 3466 3754 3467 [[package]] 3755 - name = "mini-moka-wasm" 3756 - version = "0.10.99" 3757 - source = "registry+https://github.com/rust-lang/crates.io-index" 3758 - checksum = "0102b9a2ad50fa47ca89eead2316c8222285ecfbd3f69ce99564fbe4253866e8" 3759 - dependencies = [ 3760 - "crossbeam-channel", 3761 - "crossbeam-utils", 3762 - "dashmap", 3763 - "smallvec", 3764 - "tagptr", 3765 - "triomphe", 3766 - "web-time", 3767 - ] 3768 - 3769 - [[package]] 3770 3468 name = "minimal-lexical" 3771 3469 version = "0.2.1" 3772 3470 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3863 3561 "security-framework-sys", 3864 3562 "tempfile", 3865 3563 ] 3866 - 3867 - [[package]] 3868 - name = "new_debug_unreachable" 3869 - version = "1.0.6" 3870 - source = "registry+https://github.com/rust-lang/crates.io-index" 3871 - checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 3872 3564 3873 3565 [[package]] 3874 3566 name = "nom" ··· 4192 3884 checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 4193 3885 4194 3886 [[package]] 4195 - name = "pem" 4196 - version = "3.0.6" 4197 - source = "registry+https://github.com/rust-lang/crates.io-index" 4198 - checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" 4199 - dependencies = [ 4200 - "base64 0.22.1", 4201 - "serde_core", 4202 - ] 4203 - 4204 - [[package]] 4205 3887 name = "pem-rfc7468" 4206 3888 version = "0.7.0" 4207 3889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4215 3897 version = "2.3.2" 4216 3898 source = "registry+https://github.com/rust-lang/crates.io-index" 4217 3899 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 4218 - 4219 - [[package]] 4220 - name = "phf" 4221 - version = "0.11.3" 4222 - source = "registry+https://github.com/rust-lang/crates.io-index" 4223 - checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 4224 - dependencies = [ 4225 - "phf_shared", 4226 - ] 4227 - 4228 - [[package]] 4229 - name = "phf_codegen" 4230 - version = "0.11.3" 4231 - source = "registry+https://github.com/rust-lang/crates.io-index" 4232 - checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 4233 - dependencies = [ 4234 - "phf_generator", 4235 - "phf_shared", 4236 - ] 4237 - 4238 - [[package]] 4239 - name = "phf_generator" 4240 - version = "0.11.3" 4241 - source = "registry+https://github.com/rust-lang/crates.io-index" 4242 - checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 4243 - dependencies = [ 4244 - "phf_shared", 4245 - "rand 0.8.5", 4246 - ] 4247 - 4248 - [[package]] 4249 - name = "phf_shared" 4250 - version = "0.11.3" 4251 - source = "registry+https://github.com/rust-lang/crates.io-index" 4252 - checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 4253 - dependencies = [ 4254 - "siphasher", 4255 - ] 4256 3900 4257 3901 [[package]] 4258 3902 name = "pin-project" ··· 4392 4036 ] 4393 4037 4394 4038 [[package]] 4395 - name = "precomputed-hash" 4396 - version = "0.1.1" 4397 - source = "registry+https://github.com/rust-lang/crates.io-index" 4398 - checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 4399 - 4400 - [[package]] 4401 4039 name = "prettyplease" 4402 4040 version = "0.2.37" 4403 4041 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5392 5030 checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 5393 5031 5394 5032 [[package]] 5395 - name = "simple_asn1" 5396 - version = "0.6.3" 5397 - source = "registry+https://github.com/rust-lang/crates.io-index" 5398 - checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" 5399 - dependencies = [ 5400 - "num-bigint", 5401 - "num-traits", 5402 - "thiserror 2.0.17", 5403 - "time", 5404 - ] 5405 - 5406 - [[package]] 5407 - name = "siphasher" 5408 - version = "1.0.1" 5409 - source = "registry+https://github.com/rust-lang/crates.io-index" 5410 - checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 5411 - 5412 - [[package]] 5413 5033 name = "sketches-ddsketch" 5414 5034 version = "0.3.0" 5415 5035 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5735 5355 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 5736 5356 5737 5357 [[package]] 5738 - name = "string_cache" 5739 - version = "0.8.9" 5740 - source = "registry+https://github.com/rust-lang/crates.io-index" 5741 - checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 5742 - dependencies = [ 5743 - "new_debug_unreachable", 5744 - "parking_lot", 5745 - "phf_shared", 5746 - "precomputed-hash", 5747 - "serde", 5748 - ] 5749 - 5750 - [[package]] 5751 - name = "string_cache_codegen" 5752 - version = "0.5.4" 5753 - source = "registry+https://github.com/rust-lang/crates.io-index" 5754 - checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 5755 - dependencies = [ 5756 - "phf_generator", 5757 - "phf_shared", 5758 - "proc-macro2", 5759 - "quote", 5760 - ] 5761 - 5762 - [[package]] 5763 5358 name = "stringprep" 5764 5359 version = "0.1.5" 5765 5360 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5869 5464 ] 5870 5465 5871 5466 [[package]] 5872 - name = "tagptr" 5873 - version = "0.2.0" 5874 - source = "registry+https://github.com/rust-lang/crates.io-index" 5875 - checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 5876 - 5877 - [[package]] 5878 5467 name = "tempfile" 5879 5468 version = "3.23.0" 5880 5469 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5888 5477 ] 5889 5478 5890 5479 [[package]] 5891 - name = "tendril" 5892 - version = "0.4.3" 5893 - source = "registry+https://github.com/rust-lang/crates.io-index" 5894 - checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 5895 - dependencies = [ 5896 - "futf", 5897 - "mac", 5898 - "utf-8", 5899 - ] 5900 - 5901 - [[package]] 5902 5480 name = "testcontainers" 5903 5481 version = "0.26.2" 5904 5482 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6343 5921 "serde_json", 6344 5922 "sha2", 6345 5923 "subtle", 6346 - "thiserror 2.0.17", 6347 5924 "totp-rs", 6348 5925 "tranquil-crypto", 6349 5926 "urlencoding", ··· 6357 5934 "async-trait", 6358 5935 "base64 0.22.1", 6359 5936 "redis", 6360 - "thiserror 2.0.17", 6361 5937 "tracing", 6362 5938 "tranquil-infra", 6363 5939 ] ··· 6368 5944 dependencies = [ 6369 5945 "async-trait", 6370 5946 "base64 0.22.1", 6371 - "chrono", 6372 5947 "reqwest", 6373 - "serde", 6374 5948 "serde_json", 6375 - "sqlx", 6376 5949 "thiserror 2.0.17", 6377 5950 "tokio", 6378 5951 "tranquil-db-traits", 6379 - "urlencoding", 6380 5952 "uuid", 6381 5953 ] 6382 5954 ··· 6391 5963 "p256 0.13.2", 6392 5964 "rand 0.8.5", 6393 5965 "serde", 6394 - "serde_json", 6395 5966 "sha2", 6396 5967 "subtle", 6397 5968 "thiserror 2.0.17", ··· 6407 5978 "serde", 6408 5979 "serde_json", 6409 5980 "sqlx", 6410 - "thiserror 2.0.17", 6411 5981 "tracing", 6412 5982 "tranquil-db-traits", 6413 5983 "tranquil-oauth", ··· 6439 6009 "bytes", 6440 6010 "futures", 6441 6011 "thiserror 2.0.17", 6442 - "tokio", 6443 - "tracing", 6444 6012 ] 6445 6013 6446 6014 [[package]] ··· 6472 6040 dependencies = [ 6473 6041 "aes-gcm", 6474 6042 "anyhow", 6475 - "async-trait", 6476 6043 "aws-config", 6477 6044 "aws-sdk-s3", 6478 6045 "axum", ··· 6487 6054 "cid", 6488 6055 "ctor", 6489 6056 "dotenvy", 6490 - "ed25519-dalek", 6491 6057 "futures", 6492 6058 "futures-util", 6493 6059 "governor", 6494 - "hex", 6495 6060 "hickory-resolver", 6496 6061 "hkdf", 6497 6062 "hmac", ··· 6500 6065 "infer", 6501 6066 "ipld-core", 6502 6067 "iroh-car", 6503 - "jacquard", 6504 - "jacquard-axum", 6068 + "jacquard-common", 6505 6069 "jacquard-repo", 6506 - "jsonwebtoken", 6507 6070 "k256", 6508 6071 "metrics", 6509 6072 "metrics-exporter-prometheus", 6510 6073 "multibase", 6511 6074 "multihash", 6512 6075 "p256 0.13.2", 6513 - "p384", 6514 6076 "rand 0.8.5", 6515 6077 "redis", 6516 6078 "regex", ··· 6528 6090 "thiserror 2.0.17", 6529 6091 "tokio", 6530 6092 "tokio-tungstenite", 6531 - "totp-rs", 6532 6093 "tower", 6533 6094 "tower-http", 6534 6095 "tower-layer", ··· 6540 6101 "tranquil-crypto", 6541 6102 "tranquil-db", 6542 6103 "tranquil-db-traits", 6543 - "tranquil-infra", 6544 6104 "tranquil-oauth", 6545 6105 "tranquil-repo", 6546 6106 "tranquil-scopes", ··· 6549 6109 "urlencoding", 6550 6110 "uuid", 6551 6111 "webauthn-rs", 6552 - "webauthn-rs-proto", 6553 6112 "wiremock", 6554 6113 "zip", 6555 6114 ] ··· 6564 6123 "multihash", 6565 6124 "sha2", 6566 6125 "sqlx", 6567 - "tranquil-types", 6568 6126 ] 6569 6127 6570 6128 [[package]] ··· 6590 6148 "bytes", 6591 6149 "futures", 6592 6150 "sha2", 6593 - "thiserror 2.0.17", 6594 - "tracing", 6595 6151 "tranquil-infra", 6596 6152 ] 6597 6153 ··· 6601 6157 dependencies = [ 6602 6158 "chrono", 6603 6159 "cid", 6604 - "jacquard", 6160 + "jacquard-common", 6605 6161 "serde", 6606 6162 "serde_json", 6607 6163 "sqlx", ··· 6609 6165 ] 6610 6166 6611 6167 [[package]] 6612 - name = "triomphe" 6613 - version = "0.1.15" 6614 - source = "registry+https://github.com/rust-lang/crates.io-index" 6615 - checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" 6616 - 6617 - [[package]] 6618 6168 name = "try-lock" 6619 6169 version = "0.2.5" 6620 6170 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6996 6546 ] 6997 6547 6998 6548 [[package]] 6999 - name = "webpage" 7000 - version = "2.0.1" 7001 - source = "registry+https://github.com/rust-lang/crates.io-index" 7002 - checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" 7003 - dependencies = [ 7004 - "html5ever", 7005 - "markup5ever_rcdom", 7006 - "serde_json", 7007 - "url", 7008 - ] 7009 - 7010 - [[package]] 7011 6549 name = "webpki-roots" 7012 6550 version = "0.26.11" 7013 6551 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7434 6972 ] 7435 6973 7436 6974 [[package]] 7437 - name = "xml5ever" 7438 - version = "0.18.1" 7439 - source = "registry+https://github.com/rust-lang/crates.io-index" 7440 - checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" 7441 - dependencies = [ 7442 - "log", 7443 - "mac", 7444 - "markup5ever", 7445 - ] 7446 - 7447 - [[package]] 7448 6975 name = "xmlparser" 7449 6976 version = "0.13.6" 7450 6977 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7531 7058 version = "1.8.2" 7532 7059 source = "registry+https://github.com/rust-lang/crates.io-index" 7533 7060 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 7534 - dependencies = [ 7535 - "serde", 7536 - ] 7537 7061 7538 7062 [[package]] 7539 7063 name = "zerotrie"
+1 -2
Cargo.toml
··· 63 63 infer = "0.19" 64 64 ipld-core = "0.4" 65 65 iroh-car = "0.5" 66 - jacquard = { version = "0.9", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] } 67 - jacquard-axum = "0.9" 66 + jacquard-common = { version = "0.9", features = ["crypto-k256"] } 68 67 jacquard-repo = "0.9" 69 68 jsonwebtoken = { version = "10.2", features = ["rust_crypto"] } 70 69 k256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
-1
crates/tranquil-auth/Cargo.toml
··· 19 19 serde_json = { workspace = true } 20 20 sha2 = { workspace = true } 21 21 subtle = { workspace = true } 22 - thiserror = { workspace = true } 23 22 totp-rs = { workspace = true } 24 23 urlencoding = { workspace = true } 25 24 uuid = { workspace = true }
-1
crates/tranquil-cache/Cargo.toml
··· 10 10 async-trait = { workspace = true } 11 11 base64 = { workspace = true } 12 12 redis = { workspace = true } 13 - thiserror = { workspace = true } 14 13 tracing = { workspace = true }
-4
crates/tranquil-comms/Cargo.toml
··· 7 7 [dependencies] 8 8 async-trait = { workspace = true } 9 9 base64 = { workspace = true } 10 - chrono = { workspace = true } 11 10 reqwest = { workspace = true } 12 - serde = { workspace = true } 13 11 serde_json = { workspace = true } 14 - sqlx = { workspace = true } 15 12 thiserror = { workspace = true } 16 13 tokio = { workspace = true } 17 14 tranquil-db-traits = { workspace = true } 18 - urlencoding = { workspace = true } 19 15 uuid = { workspace = true }
-1
crates/tranquil-crypto/Cargo.toml
··· 12 12 p256 = { workspace = true } 13 13 rand = { workspace = true } 14 14 serde = { workspace = true } 15 - serde_json = { workspace = true } 16 15 sha2 = { workspace = true } 17 16 subtle = { workspace = true } 18 17 thiserror = { workspace = true }
+5 -6
crates/tranquil-db-traits/src/backup.rs
··· 53 53 54 54 #[async_trait] 55 55 pub trait BackupRepository: Send + Sync { 56 - async fn get_user_backup_status( 57 - &self, 58 - did: &Did, 59 - ) -> Result<Option<(Uuid, bool)>, DbError>; 56 + async fn get_user_backup_status(&self, did: &Did) -> Result<Option<(Uuid, bool)>, DbError>; 60 57 61 58 async fn list_backups_for_user(&self, user_id: Uuid) -> Result<Vec<BackupRow>, DbError>; 62 59 ··· 92 89 did: &Did, 93 90 ) -> Result<Option<BackupForDeletion>, DbError>; 94 91 95 - async fn get_user_deactivated_status(&self, did: &Did) 96 - -> Result<Option<Option<DateTime<Utc>>>, DbError>; 92 + async fn get_user_deactivated_status( 93 + &self, 94 + did: &Did, 95 + ) -> Result<Option<Option<DateTime<Utc>>>, DbError>; 97 96 98 97 async fn update_backup_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>; 99 98
+1 -5
crates/tranquil-db-traits/src/blob.rs
··· 58 58 limit: i64, 59 59 ) -> Result<Vec<CidLink>, DbError>; 60 60 61 - async fn list_blobs_since_rev( 62 - &self, 63 - did: &Did, 64 - since: &str, 65 - ) -> Result<Vec<CidLink>, DbError>; 61 + async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result<Vec<CidLink>, DbError>; 66 62 67 63 async fn count_blobs_by_user(&self, user_id: Uuid) -> Result<i64, DbError>; 68 64
+1
crates/tranquil-db-traits/src/delegation.rs
··· 112 112 113 113 async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError>; 114 114 115 + #[allow(clippy::too_many_arguments)] 115 116 async fn log_delegation_action( 116 117 &self, 117 118 delegated_did: &Did,
+2 -4
crates/tranquil-db-traits/src/infra.rs
··· 109 109 110 110 #[async_trait] 111 111 pub trait InfraRepository: Send + Sync { 112 + #[allow(clippy::too_many_arguments)] 112 113 async fn enqueue_comms( 113 114 &self, 114 115 user_id: Option<Uuid>, ··· 287 288 limit: i64, 288 289 ) -> Result<Vec<NotificationHistoryRow>, DbError>; 289 290 290 - async fn get_server_configs( 291 - &self, 292 - keys: &[&str], 293 - ) -> Result<Vec<(String, String)>, DbError>; 291 + async fn get_server_configs(&self, keys: &[&str]) -> Result<Vec<(String, String)>, DbError>; 294 292 295 293 async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError>; 296 294
+11 -12
crates/tranquil-db-traits/src/lib.rs
··· 14 14 BackupForDeletion, BackupRepository, BackupRow, BackupStorageInfo, BlobExportInfo, 15 15 OldBackupInfo, UserBackupInfo, 16 16 }; 17 - pub use blob::{ 18 - BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, MissingBlobInfo, 19 - }; 17 + pub use blob::{BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, MissingBlobInfo}; 20 18 pub use delegation::{ 21 19 AuditLogEntry, ControllerInfo, DelegatedAccountInfo, DelegationActionType, DelegationGrant, 22 20 DelegationRepository, ··· 43 41 SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate, 44 42 }; 45 43 pub use user::{ 46 - AccountSearchResult, CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, 47 - CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, 48 - DidWebOverrides, MigrationReactivationError, MigrationReactivationInput, NotificationPrefs, 49 - OAuthTokenWithUser, PasswordResetResult, ReactivatedAccountInfo, RecoverPasskeyAccountInput, 50 - RecoverPasskeyAccountResult, ScheduledDeletionAccount, StoredBackupCode, StoredPasskey, 51 - TotpRecord, User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, 52 - UserEmailInfo, UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, 44 + AccountSearchResult, CompletePasskeySetupInput, CreateAccountError, 45 + CreateDelegatedAccountInput, CreatePasskeyAccountInput, CreatePasswordAccountInput, 46 + CreatePasswordAccountResult, DidWebOverrides, MigrationReactivationError, 47 + MigrationReactivationInput, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, 48 + ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, 49 + ScheduledDeletionAccount, StoredBackupCode, StoredPasskey, TotpRecord, User2faStatus, 50 + UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, 51 + UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, 53 52 UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle, 54 53 UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, 55 54 UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo, 56 - UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus, 57 - UserVerificationInfo, UserWithKey, 55 + UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, 56 + UserStatus, UserVerificationInfo, UserWithKey, 58 57 };
+51 -12
crates/tranquil-db-traits/src/oauth.rs
··· 2 2 use chrono::{DateTime, Utc}; 3 3 use serde::{Deserialize, Serialize}; 4 4 use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; 5 - use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId}; 5 + use tranquil_types::{ 6 + AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, 7 + TokenId, 8 + }; 6 9 use uuid::Uuid; 7 10 8 11 use crate::DbError; ··· 106 109 new_refresh_token: &RefreshToken, 107 110 new_expires_at: DateTime<Utc>, 108 111 ) -> Result<(), DbError>; 109 - async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError>; 112 + async fn check_refresh_token_used( 113 + &self, 114 + refresh_token: &RefreshToken, 115 + ) -> Result<Option<i32>, DbError>; 110 116 async fn delete_token(&self, token_id: &TokenId) -> Result<(), DbError>; 111 117 async fn delete_token_family(&self, db_id: i32) -> Result<(), DbError>; 112 118 async fn list_tokens_for_user(&self, did: &Did) -> Result<Vec<TokenData>, DbError>; ··· 116 122 did: &Did, 117 123 keep_count: i64, 118 124 ) -> Result<u64, DbError>; 119 - async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError>; 125 + async fn revoke_tokens_for_client( 126 + &self, 127 + did: &Did, 128 + client_id: &ClientId, 129 + ) -> Result<u64, DbError>; 120 130 async fn revoke_tokens_for_controller( 121 131 &self, 122 132 delegated_did: &Did, ··· 157 167 did: &Did, 158 168 device_id: Option<&DeviceId>, 159 169 ) -> Result<(), DbError>; 160 - async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError>; 161 - async fn set_controller_did(&self, request_id: &RequestId, controller_did: &Did) 162 - -> Result<(), DbError>; 170 + async fn update_request_scope( 171 + &self, 172 + request_id: &RequestId, 173 + scope: &str, 174 + ) -> Result<(), DbError>; 175 + async fn set_controller_did( 176 + &self, 177 + request_id: &RequestId, 178 + controller_did: &Did, 179 + ) -> Result<(), DbError>; 163 180 async fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), DbError>; 164 181 165 182 async fn create_device(&self, device_id: &DeviceId, data: &DeviceData) -> Result<(), DbError>; ··· 167 184 async fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), DbError>; 168 185 async fn delete_device(&self, device_id: &DeviceId) -> Result<(), DbError>; 169 186 async fn upsert_account_device(&self, did: &Did, device_id: &DeviceId) -> Result<(), DbError>; 170 - async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError>; 171 - async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>; 187 + async fn get_device_accounts( 188 + &self, 189 + device_id: &DeviceId, 190 + ) -> Result<Vec<DeviceAccountRow>, DbError>; 191 + async fn verify_account_on_device( 192 + &self, 193 + device_id: &DeviceId, 194 + did: &Did, 195 + ) -> Result<bool, DbError>; 172 196 173 197 async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result<bool, DbError>; 174 198 async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result<u64, DbError>; ··· 184 208 ) -> Result<Option<TwoFactorChallenge>, DbError>; 185 209 async fn increment_2fa_attempts(&self, id: Uuid) -> Result<i32, DbError>; 186 210 async fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), DbError>; 187 - async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError>; 211 + async fn delete_2fa_challenge_by_request_uri( 212 + &self, 213 + request_uri: &RequestId, 214 + ) -> Result<(), DbError>; 188 215 async fn cleanup_expired_2fa_challenges(&self) -> Result<u64, DbError>; 189 216 async fn check_user_2fa_enabled(&self, did: &Did) -> Result<bool, DbError>; 190 217 ··· 199 226 client_id: &ClientId, 200 227 prefs: &[ScopePreference], 201 228 ) -> Result<(), DbError>; 202 - async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError>; 229 + async fn delete_scope_preferences( 230 + &self, 231 + did: &Did, 232 + client_id: &ClientId, 233 + ) -> Result<(), DbError>; 203 234 204 235 async fn upsert_authorized_client( 205 236 &self, ··· 219 250 device_id: &DeviceId, 220 251 did: &Did, 221 252 ) -> Result<Option<DeviceTrustInfo>, DbError>; 222 - async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>; 253 + async fn device_belongs_to_user( 254 + &self, 255 + device_id: &DeviceId, 256 + did: &Did, 257 + ) -> Result<bool, DbError>; 223 258 async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>; 224 259 async fn update_device_friendly_name( 225 260 &self, ··· 241 276 async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<OAuthSessionListItem>, DbError>; 242 277 async fn delete_session_by_id(&self, session_id: i32, did: &Did) -> Result<u64, DbError>; 243 278 async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>; 244 - async fn delete_sessions_by_did_except(&self, did: &Did, except_token_id: &TokenId) -> Result<u64, DbError>; 279 + async fn delete_sessions_by_did_except( 280 + &self, 281 + did: &Did, 282 + except_token_id: &TokenId, 283 + ) -> Result<u64, DbError>; 245 284 }
+21 -8
crates/tranquil-db-traits/src/repo.rs
··· 233 233 rkey: &Rkey, 234 234 ) -> Result<Option<CidLink>, DbError>; 235 235 236 + #[allow(clippy::too_many_arguments)] 236 237 async fn list_records( 237 238 &self, 238 239 repo_id: Uuid, ··· 252 253 253 254 async fn count_all_records(&self) -> Result<i64, DbError>; 254 255 255 - async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError>; 256 + async fn get_record_by_cid(&self, cid: &CidLink) 257 + -> Result<Option<RecordWithTakedown>, DbError>; 256 258 257 - async fn set_record_takedown(&self, cid: &CidLink, takedown_ref: Option<&str>) 258 - -> Result<(), DbError>; 259 + async fn set_record_takedown( 260 + &self, 261 + cid: &CidLink, 262 + takedown_ref: Option<&str>, 263 + ) -> Result<(), DbError>; 259 264 260 265 async fn insert_user_blocks( 261 266 &self, ··· 264 269 repo_rev: &str, 265 270 ) -> Result<(), DbError>; 266 271 267 - async fn delete_user_blocks(&self, user_id: Uuid, block_cids: &[Vec<u8>]) 268 - -> Result<(), DbError>; 272 + async fn delete_user_blocks( 273 + &self, 274 + user_id: Uuid, 275 + block_cids: &[Vec<u8>], 276 + ) -> Result<(), DbError>; 269 277 270 278 async fn get_user_block_cids_since_rev( 271 279 &self, ··· 277 285 278 286 async fn insert_commit_event(&self, data: &CommitEventData) -> Result<i64, DbError>; 279 287 280 - async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError>; 288 + async fn insert_identity_event( 289 + &self, 290 + did: &Did, 291 + handle: Option<&Handle>, 292 + ) -> Result<i64, DbError>; 281 293 282 294 async fn insert_account_event( 283 295 &self, ··· 302 314 ) -> Result<i64, DbError>; 303 315 304 316 async fn update_seq_blocks_cids(&self, seq: i64, blocks_cids: &[String]) 305 - -> Result<(), DbError>; 317 + -> Result<(), DbError>; 306 318 307 319 async fn delete_sequences_except(&self, did: &Did, keep_seq: i64) -> Result<(), DbError>; 308 320 ··· 344 356 limit: i64, 345 357 ) -> Result<Vec<RepoListItem>, DbError>; 346 358 347 - async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError>; 359 + async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) 360 + -> Result<Option<CidLink>, DbError>; 348 361 349 362 async fn notify_update(&self, seq: i64) -> Result<(), DbError>; 350 363
+58 -38
crates/tranquil-db-traits/src/user.rs
··· 139 139 140 140 async fn get_user_key_by_id(&self, user_id: Uuid) -> Result<Option<UserKeyInfo>, DbError>; 141 141 142 - async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError>; 142 + async fn get_id_and_handle_by_did(&self, did: &Did) 143 + -> Result<Option<UserIdAndHandle>, DbError>; 143 144 144 145 async fn get_did_web_info_by_handle( 145 146 &self, 146 147 handle: &Handle, 147 148 ) -> Result<Option<UserDidWebInfo>, DbError>; 148 149 149 - async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError>; 150 + async fn get_did_web_overrides( 151 + &self, 152 + user_id: Uuid, 153 + ) -> Result<Option<DidWebOverrides>, DbError>; 150 154 151 155 async fn get_handle_by_did(&self, did: &Did) -> Result<Option<Handle>, DbError>; 152 156 153 157 async fn is_account_active_by_did(&self, did: &Did) -> Result<Option<bool>, DbError>; 154 158 155 - async fn get_user_for_deletion( 156 - &self, 157 - did: &Did, 158 - ) -> Result<Option<UserForDeletion>, DbError>; 159 + async fn get_user_for_deletion(&self, did: &Did) -> Result<Option<UserForDeletion>, DbError>; 159 160 160 - async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError>; 161 + async fn check_handle_exists( 162 + &self, 163 + handle: &Handle, 164 + exclude_user_id: Uuid, 165 + ) -> Result<bool, DbError>; 161 166 162 167 async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError>; 163 168 164 - async fn get_user_with_key_by_did( 165 - &self, 166 - did: &Did, 167 - ) -> Result<Option<UserKeyWithId>, DbError>; 169 + async fn get_user_with_key_by_did(&self, did: &Did) -> Result<Option<UserKeyWithId>, DbError>; 168 170 169 171 async fn is_account_migrated(&self, did: &Did) -> Result<bool, DbError>; 170 172 ··· 174 176 175 177 async fn get_email_info_by_did(&self, did: &Did) -> Result<Option<UserEmailInfo>, DbError>; 176 178 177 - async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError>; 179 + async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) 180 + -> Result<bool, DbError>; 178 181 179 182 async fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), DbError>; 180 183 ··· 191 194 192 195 async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>; 193 196 194 - async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError>; 197 + async fn get_notification_prefs(&self, did: &Did) 198 + -> Result<Option<NotificationPrefs>, DbError>; 195 199 196 200 async fn get_id_handle_email_by_did( 197 201 &self, 198 202 did: &Did, 199 203 ) -> Result<Option<UserIdHandleEmail>, DbError>; 200 204 201 - async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError>; 205 + async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) 206 + -> Result<(), DbError>; 202 207 203 208 async fn clear_discord(&self, user_id: Uuid) -> Result<(), DbError>; 204 209 ··· 221 226 telegram_username: &str, 222 227 ) -> Result<(), DbError>; 223 228 224 - async fn verify_signal_channel(&self, user_id: Uuid, signal_number: &str) 225 - -> Result<(), DbError>; 229 + async fn verify_signal_channel( 230 + &self, 231 + user_id: Uuid, 232 + signal_number: &str, 233 + ) -> Result<(), DbError>; 226 234 227 235 async fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>; 228 236 ··· 276 284 challenge_type: &str, 277 285 ) -> Result<Option<String>, DbError>; 278 286 279 - async fn delete_webauthn_challenge(&self, did: &Did, challenge_type: &str) 280 - -> Result<(), DbError>; 287 + async fn delete_webauthn_challenge( 288 + &self, 289 + did: &Did, 290 + challenge_type: &str, 291 + ) -> Result<(), DbError>; 281 292 282 293 async fn get_totp_record(&self, did: &Did) -> Result<Option<TotpRecord>, DbError>; 283 294 ··· 316 327 317 328 async fn get_session_info_by_did(&self, did: &Did) -> Result<Option<UserSessionInfo>, DbError>; 318 329 319 - async fn get_legacy_login_pref(&self, did: &Did) -> Result<Option<UserLegacyLoginPref>, DbError>; 330 + async fn get_legacy_login_pref( 331 + &self, 332 + did: &Did, 333 + ) -> Result<Option<UserLegacyLoginPref>, DbError>; 320 334 321 335 async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result<bool, DbError>; 322 336 ··· 364 378 did: &Did, 365 379 ) -> Result<Option<UserIdAndPasswordHash>, DbError>; 366 380 367 - async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>; 381 + async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) 382 + -> Result<(), DbError>; 368 383 369 384 async fn reset_password_with_sessions( 370 385 &self, ··· 389 404 390 405 async fn remove_user_password(&self, user_id: Uuid) -> Result<(), DbError>; 391 406 392 - async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>; 407 + async fn set_new_user_password( 408 + &self, 409 + user_id: Uuid, 410 + password_hash: &str, 411 + ) -> Result<(), DbError>; 393 412 394 413 async fn get_user_key_by_did(&self, did: &Did) -> Result<Option<UserKeyInfo>, DbError>; 395 414 396 - async fn delete_account_complete( 415 + async fn delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError>; 416 + 417 + async fn set_user_takedown( 397 418 &self, 398 - user_id: Uuid, 399 419 did: &Did, 400 - ) -> Result<(), DbError>; 401 - 402 - async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError>; 420 + takedown_ref: Option<&str>, 421 + ) -> Result<bool, DbError>; 403 422 404 423 async fn admin_delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError>; 405 424 406 425 async fn get_user_for_did_doc(&self, did: &Did) -> Result<Option<UserForDidDoc>, DbError>; 407 426 408 - async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError>; 427 + async fn get_user_for_did_doc_build( 428 + &self, 429 + did: &Did, 430 + ) -> Result<Option<UserForDidDocBuild>, DbError>; 409 431 410 432 async fn upsert_did_web_overrides( 411 433 &self, ··· 414 436 also_known_as: Option<Vec<String>>, 415 437 ) -> Result<(), DbError>; 416 438 417 - async fn update_migrated_to_pds( 439 + async fn update_migrated_to_pds(&self, did: &Did, endpoint: &str) -> Result<(), DbError>; 440 + 441 + async fn get_user_for_passkey_setup( 418 442 &self, 419 443 did: &Did, 420 - endpoint: &str, 421 - ) -> Result<(), DbError>; 422 - 423 - async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError>; 444 + ) -> Result<Option<UserForPasskeySetup>, DbError>; 424 445 425 446 async fn get_user_for_passkey_recovery( 426 447 &self, ··· 442 463 limit: i64, 443 464 ) -> Result<Vec<ScheduledDeletionAccount>, DbError>; 444 465 445 - async fn delete_account_with_firehose( 446 - &self, 447 - user_id: Uuid, 448 - did: &Did, 449 - ) -> Result<i64, DbError>; 466 + async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result<i64, DbError>; 450 467 451 468 async fn create_password_account( 452 469 &self, ··· 468 485 input: &MigrationReactivationInput, 469 486 ) -> Result<ReactivatedAccountInfo, MigrationReactivationError>; 470 487 471 - async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError>; 488 + async fn check_handle_available_for_new_account( 489 + &self, 490 + handle: &Handle, 491 + ) -> Result<bool, DbError>; 472 492 473 493 async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError>; 474 494
-1
crates/tranquil-db/Cargo.toml
··· 18 18 rand = { workspace = true } 19 19 serde = { workspace = true } 20 20 serde_json = { workspace = true } 21 - thiserror = { workspace = true } 22 21 tracing = { workspace = true } 23 22 uuid = { workspace = true } 24 23
+6 -12
crates/tranquil-db/src/postgres/blob.rs
··· 80 80 } 81 81 82 82 async fn get_blob_storage_key(&self, cid: &CidLink) -> Result<Option<String>, DbError> { 83 - let result = sqlx::query_scalar!( 84 - "SELECT storage_key FROM blobs WHERE cid = $1", 85 - cid.as_str() 86 - ) 87 - .fetch_optional(&self.pool) 88 - .await 89 - .map_err(map_sqlx_error)?; 83 + let result = 84 + sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str()) 85 + .fetch_optional(&self.pool) 86 + .await 87 + .map_err(map_sqlx_error)?; 90 88 91 89 Ok(result) 92 90 } ··· 114 112 Ok(results.into_iter().map(CidLink::from).collect()) 115 113 } 116 114 117 - async fn list_blobs_since_rev( 118 - &self, 119 - did: &Did, 120 - since: &str, 121 - ) -> Result<Vec<CidLink>, DbError> { 115 + async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result<Vec<CidLink>, DbError> { 122 116 let results = sqlx::query_scalar!( 123 117 r#"SELECT DISTINCT unnest(blobs) as "cid!" 124 118 FROM repo_seq
+7 -4
crates/tranquil-db/src/postgres/delegation.rs
··· 62 62 #[async_trait] 63 63 impl DelegationRepository for PostgresDelegationRepository { 64 64 async fn is_delegated_account(&self, did: &Did) -> Result<bool, DbError> { 65 - let result = sqlx::query_scalar!( 66 - r#"SELECT account_type::text = 'delegated' as "is_delegated!" FROM users WHERE did = $1"#, 65 + let exists = sqlx::query_scalar!( 66 + r#"SELECT EXISTS( 67 + SELECT 1 FROM account_delegations 68 + WHERE delegated_did = $1 AND revoked_at IS NULL 69 + ) as "exists!""#, 67 70 did.as_str() 68 71 ) 69 - .fetch_optional(&self.pool) 72 + .fetch_one(&self.pool) 70 73 .await 71 74 .map_err(map_sqlx_error)?; 72 75 73 - Ok(result.unwrap_or(false)) 76 + Ok(exists) 74 77 } 75 78 76 79 async fn create_delegation(
+5 -2
crates/tranquil-db/src/postgres/event_notifier.rs
··· 1 1 use async_trait::async_trait; 2 - use sqlx::postgres::PgListener; 3 2 use sqlx::PgPool; 3 + use sqlx::postgres::PgListener; 4 4 use tranquil_db_traits::{DbError, RepoEventNotifier, RepoEventReceiver}; 5 5 6 6 use super::user::map_sqlx_error; ··· 21 21 let mut listener = PgListener::connect_with(&self.pool) 22 22 .await 23 23 .map_err(map_sqlx_error)?; 24 - listener.listen("repo_updates").await.map_err(map_sqlx_error)?; 24 + listener 25 + .listen("repo_updates") 26 + .await 27 + .map_err(map_sqlx_error)?; 25 28 Ok(Box::new(PostgresRepoEventReceiver { listener })) 26 29 } 27 30 }
+57 -66
crates/tranquil-db/src/postgres/infra.rs
··· 310 310 sort: InviteCodeSortOrder, 311 311 ) -> Result<Vec<InviteCodeRow>, DbError> { 312 312 let results = match (cursor, sort) { 313 - (Some(cursor_code), InviteCodeSortOrder::Recent) => { 314 - sqlx::query_as!( 315 - InviteCodeRow, 316 - r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 313 + (Some(cursor_code), InviteCodeSortOrder::Recent) => sqlx::query_as!( 314 + InviteCodeRow, 315 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 317 316 FROM invite_codes ic 318 317 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 319 318 ORDER BY created_at DESC 320 319 LIMIT $2"#, 321 - cursor_code, 322 - limit 323 - ) 324 - .fetch_all(&self.pool) 325 - .await 326 - .map_err(map_sqlx_error)? 327 - } 328 - (None, InviteCodeSortOrder::Recent) => { 329 - sqlx::query_as!( 330 - InviteCodeRow, 331 - r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 320 + cursor_code, 321 + limit 322 + ) 323 + .fetch_all(&self.pool) 324 + .await 325 + .map_err(map_sqlx_error)?, 326 + (None, InviteCodeSortOrder::Recent) => sqlx::query_as!( 327 + InviteCodeRow, 328 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 332 329 FROM invite_codes ic 333 330 ORDER BY created_at DESC 334 331 LIMIT $1"#, 335 - limit 336 - ) 337 - .fetch_all(&self.pool) 338 - .await 339 - .map_err(map_sqlx_error)? 340 - } 341 - (Some(cursor_code), InviteCodeSortOrder::Usage) => { 342 - sqlx::query_as!( 343 - InviteCodeRow, 344 - r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 332 + limit 333 + ) 334 + .fetch_all(&self.pool) 335 + .await 336 + .map_err(map_sqlx_error)?, 337 + (Some(cursor_code), InviteCodeSortOrder::Usage) => sqlx::query_as!( 338 + InviteCodeRow, 339 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 345 340 FROM invite_codes ic 346 341 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 347 342 ORDER BY available_uses DESC 348 343 LIMIT $2"#, 349 - cursor_code, 350 - limit 351 - ) 352 - .fetch_all(&self.pool) 353 - .await 354 - .map_err(map_sqlx_error)? 355 - } 356 - (None, InviteCodeSortOrder::Usage) => { 357 - sqlx::query_as!( 358 - InviteCodeRow, 359 - r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 344 + cursor_code, 345 + limit 346 + ) 347 + .fetch_all(&self.pool) 348 + .await 349 + .map_err(map_sqlx_error)?, 350 + (None, InviteCodeSortOrder::Usage) => sqlx::query_as!( 351 + InviteCodeRow, 352 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 360 353 FROM invite_codes ic 361 354 ORDER BY available_uses DESC 362 355 LIMIT $1"#, 363 - limit 364 - ) 365 - .fetch_all(&self.pool) 366 - .await 367 - .map_err(map_sqlx_error)? 368 - } 356 + limit 357 + ) 358 + .fetch_all(&self.pool) 359 + .await 360 + .map_err(map_sqlx_error)?, 369 361 }; 370 362 371 363 Ok(results) 372 364 } 373 365 374 366 async fn get_user_dids_by_ids(&self, user_ids: &[Uuid]) -> Result<Vec<(Uuid, Did)>, DbError> { 375 - let results = sqlx::query!( 376 - "SELECT id, did FROM users WHERE id = ANY($1)", 377 - user_ids 378 - ) 379 - .fetch_all(&self.pool) 380 - .await 381 - .map_err(map_sqlx_error)?; 367 + let results = sqlx::query!("SELECT id, did FROM users WHERE id = ANY($1)", user_ids) 368 + .fetch_all(&self.pool) 369 + .await 370 + .map_err(map_sqlx_error)?; 382 371 383 - Ok(results.into_iter().map(|r| (r.id, Did::from(r.did))).collect()) 372 + Ok(results 373 + .into_iter() 374 + .map(|r| (r.id, Did::from(r.did))) 375 + .collect()) 384 376 } 385 377 386 378 async fn get_invite_code_uses_batch( ··· 688 680 } 689 681 690 682 async fn get_server_config(&self, key: &str) -> Result<Option<String>, DbError> { 691 - let row = 692 - sqlx::query_scalar!("SELECT value FROM server_config WHERE key = $1", key) 693 - .fetch_optional(&self.pool) 694 - .await 695 - .map_err(map_sqlx_error)?; 683 + let row = sqlx::query_scalar!("SELECT value FROM server_config WHERE key = $1", key) 684 + .fetch_optional(&self.pool) 685 + .await 686 + .map_err(map_sqlx_error)?; 696 687 Ok(row) 697 688 } 698 689 ··· 881 872 882 873 async fn get_server_configs(&self, keys: &[&str]) -> Result<Vec<(String, String)>, DbError> { 883 874 let keys_vec: Vec<String> = keys.iter().map(|s| s.to_string()).collect(); 884 - let rows: Vec<(String, String)> = sqlx::query_as( 885 - "SELECT key, value FROM server_config WHERE key = ANY($1)", 886 - ) 887 - .bind(&keys_vec) 888 - .fetch_all(&self.pool) 889 - .await 890 - .map_err(map_sqlx_error)?; 875 + let rows: Vec<(String, String)> = 876 + sqlx::query_as("SELECT key, value FROM server_config WHERE key = ANY($1)") 877 + .bind(&keys_vec) 878 + .fetch_all(&self.pool) 879 + .await 880 + .map_err(map_sqlx_error)?; 891 881 892 882 Ok(rows) 893 883 } ··· 917 907 } 918 908 919 909 async fn get_blob_storage_key_by_cid(&self, cid: &CidLink) -> Result<Option<String>, DbError> { 920 - let result = sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str()) 921 - .fetch_optional(&self.pool) 922 - .await 923 - .map_err(map_sqlx_error)?; 910 + let result = 911 + sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str()) 912 + .fetch_optional(&self.pool) 913 + .await 914 + .map_err(map_sqlx_error)?; 924 915 925 916 Ok(result) 926 917 }
+1 -1
crates/tranquil-db/src/postgres/mod.rs
··· 21 21 pub use oauth::PostgresOAuthRepository; 22 22 pub use repo::PostgresRepoRepository; 23 23 pub use session::PostgresSessionRepository; 24 - pub use user::PostgresUserRepository; 25 24 use tranquil_db_traits::{ 26 25 BacklinkRepository, BackupRepository, BlobRepository, DelegationRepository, InfraRepository, 27 26 OAuthRepository, RepoEventNotifier, RepoRepository, SessionRepository, UserRepository, 28 27 }; 28 + pub use user::PostgresUserRepository; 29 29 30 30 pub struct PostgresRepositories { 31 31 pub pool: PgPool,
+45 -10
crates/tranquil-db/src/postgres/oauth.rs
··· 6 6 DbError, DeviceAccountRow, DeviceTrustInfo, OAuthRepository, OAuthSessionListItem, 7 7 ScopePreference, TrustedDeviceRow, TwoFactorChallenge, 8 8 }; 9 - use tranquil_oauth::{AuthorizedClientData, ClientAuth, AuthorizationRequestParameters, DeviceData, RequestData, TokenData}; 10 - use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId}; 9 + use tranquil_oauth::{ 10 + AuthorizationRequestParameters, AuthorizedClientData, ClientAuth, DeviceData, RequestData, 11 + TokenData, 12 + }; 13 + use tranquil_types::{ 14 + AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, 15 + TokenId, 16 + }; 11 17 use uuid::Uuid; 12 18 13 19 use super::user::map_sqlx_error; ··· 236 242 Ok(()) 237 243 } 238 244 239 - async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError> { 245 + async fn check_refresh_token_used( 246 + &self, 247 + refresh_token: &RefreshToken, 248 + ) -> Result<Option<i32>, DbError> { 240 249 let row = sqlx::query_scalar!( 241 250 r#" 242 251 SELECT token_id FROM oauth_used_refresh_token WHERE refresh_token = $1 ··· 347 356 Ok(result.rows_affected()) 348 357 } 349 358 350 - async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError> { 359 + async fn revoke_tokens_for_client( 360 + &self, 361 + did: &Did, 362 + client_id: &ClientId, 363 + ) -> Result<u64, DbError> { 351 364 let result = sqlx::query!( 352 365 "DELETE FROM oauth_token WHERE did = $1 AND client_id = $2", 353 366 did.as_str(), ··· 574 587 Ok(()) 575 588 } 576 589 577 - async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError> { 590 + async fn update_request_scope( 591 + &self, 592 + request_id: &RequestId, 593 + scope: &str, 594 + ) -> Result<(), DbError> { 578 595 sqlx::query!( 579 596 r#" 580 597 UPDATE oauth_authorization_request ··· 708 725 Ok(()) 709 726 } 710 727 711 - async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError> { 728 + async fn get_device_accounts( 729 + &self, 730 + device_id: &DeviceId, 731 + ) -> Result<Vec<DeviceAccountRow>, DbError> { 712 732 let rows = sqlx::query!( 713 733 r#" 714 734 SELECT u.did, u.handle, u.email, ad.updated_at as last_used_at ··· 735 755 .collect()) 736 756 } 737 757 738 - async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> { 758 + async fn verify_account_on_device( 759 + &self, 760 + device_id: &DeviceId, 761 + did: &Did, 762 + ) -> Result<bool, DbError> { 739 763 let row = sqlx::query!( 740 764 r#" 741 765 SELECT 1 as "exists!" ··· 875 899 Ok(()) 876 900 } 877 901 878 - async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError> { 902 + async fn delete_2fa_challenge_by_request_uri( 903 + &self, 904 + request_uri: &RequestId, 905 + ) -> Result<(), DbError> { 879 906 sqlx::query!( 880 907 r#" 881 908 DELETE FROM oauth_2fa_challenge WHERE request_uri = $1 ··· 966 993 Ok(()) 967 994 } 968 995 969 - async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError> { 996 + async fn delete_scope_preferences( 997 + &self, 998 + did: &Did, 999 + client_id: &ClientId, 1000 + ) -> Result<(), DbError> { 970 1001 sqlx::query!( 971 1002 r#" 972 1003 DELETE FROM oauth_scope_preference ··· 1074 1105 })) 1075 1106 } 1076 1107 1077 - async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> { 1108 + async fn device_belongs_to_user( 1109 + &self, 1110 + device_id: &DeviceId, 1111 + did: &Did, 1112 + ) -> Result<bool, DbError> { 1078 1113 let exists = sqlx::query_scalar!( 1079 1114 r#"SELECT 1 as "one!" FROM oauth_device od 1080 1115 JOIN oauth_account_device oad ON od.id = oad.device_id
+52 -38
crates/tranquil-db/src/postgres/repo.rs
··· 147 147 } 148 148 149 149 async fn count_repos(&self) -> Result<i64, DbError> { 150 - let count = 151 - sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM repos"#) 152 - .fetch_one(&self.pool) 153 - .await 154 - .map_err(map_sqlx_error)?; 150 + let count = sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM repos"#) 151 + .fetch_one(&self.pool) 152 + .await 153 + .map_err(map_sqlx_error)?; 155 154 156 155 Ok(count) 157 156 } 158 157 159 158 async fn get_repos_without_rev(&self) -> Result<Vec<RepoWithoutRev>, DbError> { 160 - let rows = sqlx::query!( 161 - "SELECT user_id, repo_root_cid FROM repos WHERE repo_rev IS NULL" 162 - ) 163 - .fetch_all(&self.pool) 164 - .await 165 - .map_err(map_sqlx_error)?; 159 + let rows = sqlx::query!("SELECT user_id, repo_root_cid FROM repos WHERE repo_rev IS NULL") 160 + .fetch_all(&self.pool) 161 + .await 162 + .map_err(map_sqlx_error)?; 166 163 167 164 Ok(rows 168 165 .into_iter() ··· 522 519 Ok(count) 523 520 } 524 521 525 - async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError> { 522 + async fn get_record_by_cid( 523 + &self, 524 + cid: &CidLink, 525 + ) -> Result<Option<RecordWithTakedown>, DbError> { 526 526 let row = sqlx::query!( 527 527 "SELECT id, takedown_ref FROM records WHERE record_cid = $1", 528 528 cid.as_str() ··· 651 651 Ok(seq) 652 652 } 653 653 654 - async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError> { 654 + async fn insert_identity_event( 655 + &self, 656 + did: &Did, 657 + handle: Option<&Handle>, 658 + ) -> Result<i64, DbError> { 655 659 let handle_str = handle.map(|h| h.as_str()); 656 660 let seq = sqlx::query_scalar!( 657 661 r#" ··· 1061 1065 .collect()) 1062 1066 } 1063 1067 1064 - async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError> { 1068 + async fn get_repo_root_cid_by_user_id( 1069 + &self, 1070 + user_id: Uuid, 1071 + ) -> Result<Option<CidLink>, DbError> { 1065 1072 let cid = sqlx::query_scalar!( 1066 1073 "SELECT repo_root_cid FROM repos WHERE user_id = $1", 1067 1074 user_id ··· 1203 1210 } 1204 1211 } 1205 1212 1206 - let is_account_active: bool = sqlx::query_scalar( 1207 - "SELECT deactivated_at IS NULL FROM users WHERE id = $1", 1208 - ) 1209 - .bind(input.user_id) 1210 - .fetch_optional(&mut *tx) 1211 - .await 1212 - .map_err(|e| ApplyCommitError::Database(e.to_string()))? 1213 - .flatten() 1214 - .unwrap_or(false); 1213 + let is_account_active: bool = 1214 + sqlx::query_scalar("SELECT deactivated_at IS NULL FROM users WHERE id = $1") 1215 + .bind(input.user_id) 1216 + .fetch_optional(&mut *tx) 1217 + .await 1218 + .map_err(|e| ApplyCommitError::Database(e.to_string()))? 1219 + .flatten() 1220 + .unwrap_or(false); 1215 1221 1216 - sqlx::query( 1217 - "UPDATE repos SET repo_root_cid = $1, repo_rev = $2 WHERE user_id = $3", 1218 - ) 1219 - .bind(&input.new_root_cid) 1220 - .bind(&input.new_rev) 1221 - .bind(input.user_id) 1222 - .execute(&mut *tx) 1223 - .await 1224 - .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1222 + sqlx::query("UPDATE repos SET repo_root_cid = $1, repo_rev = $2 WHERE user_id = $3") 1223 + .bind(&input.new_root_cid) 1224 + .bind(&input.new_rev) 1225 + .bind(input.user_id) 1226 + .execute(&mut *tx) 1227 + .await 1228 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1225 1229 1226 1230 if !input.new_block_cids.is_empty() { 1227 1231 sqlx::query( ··· 1260 1264 .iter() 1261 1265 .map(|r| r.collection.as_str()) 1262 1266 .collect(); 1263 - let rkeys: Vec<&str> = input.record_upserts.iter().map(|r| r.rkey.as_str()).collect(); 1264 - let cids: Vec<&str> = input.record_upserts.iter().map(|r| r.cid.as_str()).collect(); 1267 + let rkeys: Vec<&str> = input 1268 + .record_upserts 1269 + .iter() 1270 + .map(|r| r.rkey.as_str()) 1271 + .collect(); 1272 + let cids: Vec<&str> = input 1273 + .record_upserts 1274 + .iter() 1275 + .map(|r| r.cid.as_str()) 1276 + .collect(); 1265 1277 1266 1278 sqlx::query( 1267 1279 r#" ··· 1287 1299 .iter() 1288 1300 .map(|r| r.collection.as_str()) 1289 1301 .collect(); 1290 - let rkeys: Vec<&str> = input.record_deletes.iter().map(|r| r.rkey.as_str()).collect(); 1302 + let rkeys: Vec<&str> = input 1303 + .record_deletes 1304 + .iter() 1305 + .map(|r| r.rkey.as_str()) 1306 + .collect(); 1291 1307 1292 1308 sqlx::query( 1293 1309 r#" ··· 1366 1382 .collect()) 1367 1383 } 1368 1384 1369 - async fn get_users_without_blocks( 1370 - &self, 1371 - ) -> Result<Vec<UserWithoutBlocks>, DbError> { 1385 + async fn get_users_without_blocks(&self) -> Result<Vec<UserWithoutBlocks>, DbError> { 1372 1386 let rows: Vec<(Uuid, String, Option<String>)> = sqlx::query_as( 1373 1387 r#" 1374 1388 SELECT u.id as user_id, r.repo_root_cid, r.repo_rev
+155 -99
crates/tranquil-db/src/postgres/user.rs
··· 8 8 AccountSearchResult, CommsChannel, DbError, DidWebOverrides, NotificationPrefs, 9 9 OAuthTokenWithUser, PasswordResetResult, StoredBackupCode, StoredPasskey, TotpRecord, 10 10 User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, 11 - UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup, 12 - UserForRecovery, UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, UserIdHandleEmail, 13 - UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, UserLoginCheck, UserLoginFull, 14 - UserLoginInfo, UserPasswordInfo, UserRepository, UserResendVerification, UserResetCodeInfo, 15 - UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey, 11 + UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, 12 + UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle, 13 + UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, 14 + UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo, 15 + UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, 16 + UserStatus, UserVerificationInfo, UserWithKey, 16 17 }; 17 18 18 19 pub struct PostgresUserRepository { ··· 344 345 })) 345 346 } 346 347 347 - async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError> { 348 - let row = sqlx::query!( 349 - "SELECT id, handle FROM users WHERE did = $1", 350 - did.as_str() 351 - ) 352 - .fetch_optional(&self.pool) 353 - .await 354 - .map_err(map_sqlx_error)?; 348 + async fn get_id_and_handle_by_did( 349 + &self, 350 + did: &Did, 351 + ) -> Result<Option<UserIdAndHandle>, DbError> { 352 + let row = sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str()) 353 + .fetch_optional(&self.pool) 354 + .await 355 + .map_err(map_sqlx_error)?; 355 356 Ok(row.map(|r| UserIdAndHandle { 356 357 id: r.id, 357 358 handle: Handle::from(r.handle), ··· 376 377 })) 377 378 } 378 379 379 - async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError> { 380 + async fn get_did_web_overrides( 381 + &self, 382 + user_id: Uuid, 383 + ) -> Result<Option<DidWebOverrides>, DbError> { 380 384 let row = sqlx::query!( 381 385 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 382 386 user_id ··· 398 402 Ok(handle.map(Handle::from)) 399 403 } 400 404 401 - async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError> { 405 + async fn check_handle_exists( 406 + &self, 407 + handle: &Handle, 408 + exclude_user_id: Uuid, 409 + ) -> Result<bool, DbError> { 402 410 let exists = sqlx::query_scalar!( 403 411 "SELECT EXISTS(SELECT 1 FROM users WHERE handle = $1 AND id != $2) as \"exists!\"", 404 412 handle.as_str(), ··· 422 430 Ok(()) 423 431 } 424 432 425 - async fn get_user_with_key_by_did( 426 - &self, 427 - did: &Did, 428 - ) -> Result<Option<UserKeyWithId>, DbError> { 433 + async fn get_user_with_key_by_did(&self, did: &Did) -> Result<Option<UserKeyWithId>, DbError> { 429 434 let row = sqlx::query!( 430 435 r#"SELECT u.id, uk.key_bytes, uk.encryption_version 431 436 FROM users u ··· 469 474 .await 470 475 .map_err(map_sqlx_error)?; 471 476 Ok(row 472 - .map(|r| r.email_verified || r.discord_verified || r.telegram_verified || r.signal_verified) 477 + .map(|r| { 478 + r.email_verified || r.discord_verified || r.telegram_verified || r.signal_verified 479 + }) 473 480 .unwrap_or(false)) 474 481 } 475 482 ··· 497 504 })) 498 505 } 499 506 500 - async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError> { 507 + async fn check_email_exists( 508 + &self, 509 + email: &str, 510 + exclude_user_id: Uuid, 511 + ) -> Result<bool, DbError> { 501 512 let row = sqlx::query!( 502 513 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 503 514 email.to_lowercase(), ··· 583 594 Ok(result.rows_affected()) 584 595 } 585 596 586 - async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError> { 597 + async fn get_notification_prefs( 598 + &self, 599 + did: &Did, 600 + ) -> Result<Option<NotificationPrefs>, DbError> { 587 601 let row = sqlx::query!( 588 602 r#"SELECT 589 603 email, ··· 630 644 })) 631 645 } 632 646 633 - async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError> { 647 + async fn update_preferred_comms_channel( 648 + &self, 649 + did: &Did, 650 + channel: &str, 651 + ) -> Result<(), DbError> { 634 652 sqlx::query( 635 653 "UPDATE users SET preferred_comms_channel = $1::comms_channel, updated_at = NOW() WHERE did = $2", 636 654 ) ··· 1349 1367 }) 1350 1368 } 1351 1369 1352 - async fn get_session_info_by_did( 1353 - &self, 1354 - did: &Did, 1355 - ) -> Result<Option<UserSessionInfo>, DbError> { 1370 + async fn get_session_info_by_did(&self, did: &Did) -> Result<Option<UserSessionInfo>, DbError> { 1356 1371 sqlx::query!( 1357 1372 r#" 1358 1373 SELECT handle, email, email_verified, is_admin, deactivated_at, takedown_ref, ··· 1647 1662 }) 1648 1663 } 1649 1664 1650 - async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> { 1665 + async fn update_password_hash( 1666 + &self, 1667 + user_id: Uuid, 1668 + password_hash: &str, 1669 + ) -> Result<(), DbError> { 1651 1670 sqlx::query!( 1652 1671 "UPDATE users SET password_hash = $1 WHERE id = $2", 1653 1672 password_hash, ··· 1769 1788 Ok(()) 1770 1789 } 1771 1790 1772 - async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> { 1791 + async fn set_new_user_password( 1792 + &self, 1793 + user_id: Uuid, 1794 + password_hash: &str, 1795 + ) -> Result<(), DbError> { 1773 1796 sqlx::query!( 1774 1797 "UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2", 1775 1798 password_hash, ··· 1854 1877 .execute(&mut *tx) 1855 1878 .await 1856 1879 .map_err(map_sqlx_error)?; 1857 - sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str()) 1858 - .execute(&mut *tx) 1859 - .await 1860 - .map_err(map_sqlx_error)?; 1880 + sqlx::query!( 1881 + "DELETE FROM account_deletion_requests WHERE did = $1", 1882 + did.as_str() 1883 + ) 1884 + .execute(&mut *tx) 1885 + .await 1886 + .map_err(map_sqlx_error)?; 1861 1887 sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 1862 1888 .execute(&mut *tx) 1863 1889 .await ··· 1866 1892 Ok(()) 1867 1893 } 1868 1894 1869 - async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError> { 1895 + async fn set_user_takedown( 1896 + &self, 1897 + did: &Did, 1898 + takedown_ref: Option<&str>, 1899 + ) -> Result<bool, DbError> { 1870 1900 let result = sqlx::query!( 1871 1901 "UPDATE users SET takedown_ref = $1 WHERE did = $2", 1872 1902 takedown_ref, ··· 1949 1979 })) 1950 1980 } 1951 1981 1952 - async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError> { 1982 + async fn get_user_for_did_doc_build( 1983 + &self, 1984 + did: &Did, 1985 + ) -> Result<Option<UserForDidDocBuild>, DbError> { 1953 1986 let row = sqlx::query!( 1954 1987 "SELECT id, handle, migrated_to_pds FROM users WHERE did = $1", 1955 1988 did.as_str() ··· 2006 2039 Ok(()) 2007 2040 } 2008 2041 2009 - async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError> { 2042 + async fn get_user_for_passkey_setup( 2043 + &self, 2044 + did: &Did, 2045 + ) -> Result<Option<UserForPasskeySetup>, DbError> { 2010 2046 let row = sqlx::query!( 2011 2047 r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required 2012 2048 FROM users WHERE did = $1"#, ··· 2111 2147 .collect()) 2112 2148 } 2113 2149 2114 - async fn delete_account_with_firehose( 2115 - &self, 2116 - user_id: Uuid, 2117 - did: &Did, 2118 - ) -> Result<i64, DbError> { 2150 + async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result<i64, DbError> { 2119 2151 let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 2120 2152 2121 2153 sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) ··· 2173 2205 .await 2174 2206 .map_err(map_sqlx_error)?; 2175 2207 2176 - sqlx::query!("DELETE FROM webauthn_challenges WHERE did = $1", did.as_str()) 2177 - .execute(&mut *tx) 2178 - .await 2179 - .map_err(map_sqlx_error)?; 2208 + sqlx::query!( 2209 + "DELETE FROM webauthn_challenges WHERE did = $1", 2210 + did.as_str() 2211 + ) 2212 + .execute(&mut *tx) 2213 + .await 2214 + .map_err(map_sqlx_error)?; 2180 2215 2181 2216 sqlx::query!("DELETE FROM account_backups WHERE user_id = $1", user_id) 2182 2217 .execute(&mut *tx) 2183 2218 .await 2184 2219 .map_err(map_sqlx_error)?; 2185 2220 2186 - sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str()) 2187 - .execute(&mut *tx) 2188 - .await 2189 - .map_err(map_sqlx_error)?; 2221 + sqlx::query!( 2222 + "DELETE FROM account_deletion_requests WHERE did = $1", 2223 + did.as_str() 2224 + ) 2225 + .execute(&mut *tx) 2226 + .await 2227 + .map_err(map_sqlx_error)?; 2190 2228 2191 2229 sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 2192 2230 .execute(&mut *tx) ··· 2231 2269 tranquil_db_traits::CreatePasswordAccountResult, 2232 2270 tranquil_db_traits::CreateAccountError, 2233 2271 > { 2234 - let mut tx = self 2235 - .pool 2236 - .begin() 2237 - .await 2238 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2272 + let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| { 2273 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2274 + })?; 2239 2275 2240 2276 let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") 2241 2277 .fetch_one(&mut *tx) ··· 2279 2315 return Err(tranquil_db_traits::CreateAccountError::DidExists); 2280 2316 } 2281 2317 } 2282 - return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2318 + return Err(tranquil_db_traits::CreateAccountError::Database( 2319 + e.to_string(), 2320 + )); 2283 2321 } 2284 2322 }; 2285 2323 ··· 2300 2338 ) 2301 2339 .execute(&mut *tx) 2302 2340 .await 2303 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2341 + .map_err(|e: sqlx::Error| { 2342 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2343 + })?; 2304 2344 } 2305 2345 2306 2346 sqlx::query!( ··· 2311 2351 ) 2312 2352 .execute(&mut *tx) 2313 2353 .await 2314 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2354 + .map_err(|e: sqlx::Error| { 2355 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2356 + })?; 2315 2357 2316 2358 sqlx::query( 2317 2359 r#" ··· 2325 2367 .bind(&input.repo_rev) 2326 2368 .execute(&mut *tx) 2327 2369 .await 2328 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2370 + .map_err(|e: sqlx::Error| { 2371 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2372 + })?; 2329 2373 2330 2374 if let Some(code) = &input.invite_code { 2331 2375 let _ = sqlx::query!( ··· 2356 2400 .await; 2357 2401 } 2358 2402 2359 - tx.commit() 2360 - .await 2361 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2403 + tx.commit().await.map_err(|e: sqlx::Error| { 2404 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2405 + })?; 2362 2406 2363 2407 Ok(tranquil_db_traits::CreatePasswordAccountResult { 2364 2408 user_id, ··· 2370 2414 &self, 2371 2415 input: &tranquil_db_traits::CreateDelegatedAccountInput, 2372 2416 ) -> Result<uuid::Uuid, tranquil_db_traits::CreateAccountError> { 2373 - let mut tx = self 2374 - .pool 2375 - .begin() 2376 - .await 2377 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2417 + let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| { 2418 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2419 + })?; 2378 2420 2379 2421 let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 2380 2422 r#"INSERT INTO users ( ··· 2401 2443 return Err(tranquil_db_traits::CreateAccountError::EmailTaken); 2402 2444 } 2403 2445 } 2404 - return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2446 + return Err(tranquil_db_traits::CreateAccountError::Database( 2447 + e.to_string(), 2448 + )); 2405 2449 } 2406 2450 }; 2407 2451 ··· 2435 2479 ) 2436 2480 .execute(&mut *tx) 2437 2481 .await 2438 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2482 + .map_err(|e: sqlx::Error| { 2483 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2484 + })?; 2439 2485 2440 2486 sqlx::query( 2441 2487 r#" ··· 2449 2495 .bind(&input.repo_rev) 2450 2496 .execute(&mut *tx) 2451 2497 .await 2452 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2498 + .map_err(|e: sqlx::Error| { 2499 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2500 + })?; 2453 2501 2454 2502 if let Some(code) = &input.invite_code { 2455 2503 let _ = sqlx::query!( ··· 2468 2516 .await; 2469 2517 } 2470 2518 2471 - tx.commit() 2472 - .await 2473 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2519 + tx.commit().await.map_err(|e: sqlx::Error| { 2520 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2521 + })?; 2474 2522 2475 2523 Ok(user_id) 2476 2524 } ··· 2482 2530 tranquil_db_traits::CreatePasswordAccountResult, 2483 2531 tranquil_db_traits::CreateAccountError, 2484 2532 > { 2485 - let mut tx = self 2486 - .pool 2487 - .begin() 2488 - .await 2489 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2533 + let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| { 2534 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2535 + })?; 2490 2536 2491 2537 let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") 2492 2538 .fetch_one(&mut *tx) ··· 2530 2576 return Err(tranquil_db_traits::CreateAccountError::EmailTaken); 2531 2577 } 2532 2578 } 2533 - return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2579 + return Err(tranquil_db_traits::CreateAccountError::Database( 2580 + e.to_string(), 2581 + )); 2534 2582 } 2535 2583 }; 2536 2584 ··· 2551 2599 ) 2552 2600 .execute(&mut *tx) 2553 2601 .await 2554 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2602 + .map_err(|e: sqlx::Error| { 2603 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2604 + })?; 2555 2605 } 2556 2606 2557 2607 sqlx::query!( ··· 2562 2612 ) 2563 2613 .execute(&mut *tx) 2564 2614 .await 2565 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2615 + .map_err(|e: sqlx::Error| { 2616 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2617 + })?; 2566 2618 2567 2619 sqlx::query( 2568 2620 r#" ··· 2576 2628 .bind(&input.repo_rev) 2577 2629 .execute(&mut *tx) 2578 2630 .await 2579 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2631 + .map_err(|e: sqlx::Error| { 2632 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2633 + })?; 2580 2634 2581 2635 if let Some(code) = &input.invite_code { 2582 2636 let _ = sqlx::query!( ··· 2607 2661 .await; 2608 2662 } 2609 2663 2610 - tx.commit() 2611 - .await 2612 - .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2664 + tx.commit().await.map_err(|e: sqlx::Error| { 2665 + tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2666 + })?; 2613 2667 2614 2668 Ok(tranquil_db_traits::CreatePasswordAccountResult { 2615 2669 user_id, ··· 2624 2678 tranquil_db_traits::ReactivatedAccountInfo, 2625 2679 tranquil_db_traits::MigrationReactivationError, 2626 2680 > { 2627 - let mut tx = self 2628 - .pool 2629 - .begin() 2630 - .await 2631 - .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; 2681 + let mut tx = 2682 + self.pool.begin().await.map_err(|e| { 2683 + tranquil_db_traits::MigrationReactivationError::Database(e.to_string()) 2684 + })?; 2632 2685 2633 2686 let existing: Option<(uuid::Uuid, String, Option<chrono::DateTime<chrono::Utc>>)> = 2634 - sqlx::query_as("SELECT id, handle, deactivated_at FROM users WHERE did = $1 FOR UPDATE") 2635 - .bind(input.did.as_str()) 2636 - .fetch_optional(&mut *tx) 2637 - .await 2638 - .map_err(|e| { 2639 - tranquil_db_traits::MigrationReactivationError::Database(e.to_string()) 2640 - })?; 2687 + sqlx::query_as( 2688 + "SELECT id, handle, deactivated_at FROM users WHERE did = $1 FOR UPDATE", 2689 + ) 2690 + .bind(input.did.as_str()) 2691 + .fetch_optional(&mut *tx) 2692 + .await 2693 + .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; 2641 2694 2642 - let (account_id, old_handle, deactivated_at) = existing 2643 - .ok_or(tranquil_db_traits::MigrationReactivationError::NotFound)?; 2695 + let (account_id, old_handle, deactivated_at) = 2696 + existing.ok_or(tranquil_db_traits::MigrationReactivationError::NotFound)?; 2644 2697 2645 2698 if deactivated_at.is_none() { 2646 2699 return Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated); ··· 2677 2730 }) 2678 2731 } 2679 2732 2680 - async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError> { 2733 + async fn check_handle_available_for_new_account( 2734 + &self, 2735 + handle: &Handle, 2736 + ) -> Result<bool, DbError> { 2681 2737 let exists: Option<(i32,)> = 2682 2738 sqlx::query_as("SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL") 2683 2739 .bind(handle.as_str())
-2
crates/tranquil-infra/Cargo.toml
··· 9 9 bytes = { workspace = true } 10 10 futures = { workspace = true } 11 11 thiserror = { workspace = true } 12 - tokio = { workspace = true } 13 - tracing = { workspace = true }
+4 -1
crates/tranquil-oauth/src/dpop.rs
··· 422 422 y: Some(y_b64), 423 423 }; 424 424 let result = verify_es256(&jwk, b"test", &[0u8; 64]); 425 - assert!(result.is_err(), "Invalid coordinates should return error, not panic"); 425 + assert!( 426 + result.is_err(), 427 + "Invalid coordinates should return error, not panic" 428 + ); 426 429 } 427 430 428 431 #[test]
+1 -10
crates/tranquil-pds/Cargo.toml
··· 6 6 7 7 [dependencies] 8 8 tranquil-types = { workspace = true } 9 - tranquil-infra = { workspace = true } 10 9 tranquil-crypto = { workspace = true } 11 10 tranquil-storage = { workspace = true } 12 11 tranquil-cache = { workspace = true } ··· 21 20 aes-gcm = { workspace = true } 22 21 backon = { workspace = true } 23 22 anyhow = { workspace = true } 24 - async-trait = { workspace = true } 25 23 aws-config = { workspace = true } 26 24 aws-sdk-s3 = { workspace = true } 27 25 axum = { workspace = true } ··· 33 31 chrono = { workspace = true } 34 32 cid = { workspace = true } 35 33 dotenvy = { workspace = true } 36 - ed25519-dalek = { workspace = true } 37 34 futures = { workspace = true } 38 35 futures-util = { workspace = true } 39 36 governor = { workspace = true } 40 - hex = { workspace = true } 41 37 hickory-resolver = { workspace = true } 42 38 hkdf = { workspace = true } 43 39 hmac = { workspace = true } ··· 46 42 infer = { workspace = true } 47 43 ipld-core = { workspace = true } 48 44 iroh-car = { workspace = true } 49 - jacquard = { workspace = true } 50 - jacquard-axum = { workspace = true } 45 + jacquard-common = { workspace = true } 51 46 jacquard-repo = { workspace = true } 52 - jsonwebtoken = { workspace = true } 53 47 k256 = { workspace = true } 54 48 metrics = { workspace = true } 55 49 metrics-exporter-prometheus = { workspace = true } 56 50 multibase = { workspace = true } 57 51 multihash = { workspace = true } 58 52 p256 = { workspace = true } 59 - p384 = { workspace = true } 60 53 rand = { workspace = true } 61 54 redis = { workspace = true } 62 55 regex = { workspace = true } ··· 72 65 thiserror = { workspace = true } 73 66 tokio = { workspace = true } 74 67 tokio-tungstenite = { workspace = true } 75 - totp-rs = { workspace = true } 76 68 tower = { workspace = true } 77 69 tower-http = { workspace = true } 78 70 tower-layer = { workspace = true } ··· 81 73 urlencoding = { workspace = true } 82 74 uuid = { workspace = true } 83 75 webauthn-rs = { workspace = true } 84 - webauthn-rs-proto = { workspace = true } 85 76 zip = { workspace = true } 86 77 87 78 [features]
+5 -4
crates/tranquil-pds/src/api/actor/preferences.rs
··· 150 150 )) 151 151 .into_response(), 152 152 ), 153 - PrefValidation::MissingType => Some( 154 - ApiError::InvalidRequest("Preference is missing a $type".into()).into_response(), 155 - ), 153 + PrefValidation::MissingType => { 154 + Some(ApiError::InvalidRequest("Preference is missing a $type".into()).into_response()) 155 + } 156 156 PrefValidation::WrongNamespace => Some( 157 157 ApiError::InvalidRequest(format!( 158 158 "Some preferences are not in the {} namespace", ··· 192 192 }) 193 193 .collect(); 194 194 195 - if let Err(_) = state 195 + if state 196 196 .infra_repo 197 197 .replace_namespace_preferences(user_id, APP_BSKY_NAMESPACE, prefs_to_save) 198 198 .await 199 + .is_err() 199 200 { 200 201 return ApiError::InternalError(Some("Failed to save preferences".into())).into_response(); 201 202 }
+31 -16
crates/tranquil-pds/src/api/admin/account/info.rs
··· 70 70 _auth: BearerAuthAdmin, 71 71 Query(params): Query<GetAccountInfoParams>, 72 72 ) -> Response { 73 - let account = match state.infra_repo.get_admin_account_info_by_did(&params.did).await { 73 + let account = match state 74 + .infra_repo 75 + .get_admin_account_info_by_did(&params.did) 76 + .await 77 + { 74 78 Ok(Some(a)) => a, 75 79 Ok(None) => return ApiError::AccountNotFound.into_response(), 76 80 Err(e) => { ··· 114 118 get_invite_code_info(state, &code).await 115 119 } 116 120 117 - async fn get_invites_for_user(state: &AppState, user_id: uuid::Uuid) -> Option<Vec<InviteCodeInfo>> { 121 + async fn get_invites_for_user( 122 + state: &AppState, 123 + user_id: uuid::Uuid, 124 + ) -> Option<Vec<InviteCodeInfo>> { 118 125 let invite_codes = state 119 126 .infra_repo 120 127 .get_invites_created_by_user(user_id) ··· 135 142 136 143 let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> = 137 144 uses.into_iter().fold(HashMap::new(), |mut acc, u| { 138 - acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo { 139 - used_by: u.used_by_did, 140 - used_at: u.used_at.to_rfc3339(), 141 - }); 145 + acc.entry(u.code.clone()) 146 + .or_default() 147 + .push(InviteCodeUseInfo { 148 + used_by: u.used_by_did, 149 + used_at: u.used_at.to_rfc3339(), 150 + }); 142 151 acc 143 152 }); 144 153 ··· 203 212 return ApiError::InvalidRequest("dids is required".into()).into_response(); 204 213 } 205 214 206 - let dids_typed: Vec<Did> = dids 207 - .iter() 208 - .filter_map(|d| d.parse().ok()) 209 - .collect(); 210 - let accounts = match state.infra_repo.get_admin_account_infos_by_dids(&dids_typed).await { 215 + let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 216 + let accounts = match state 217 + .infra_repo 218 + .get_admin_account_infos_by_dids(&dids_typed) 219 + .await 220 + { 211 221 Ok(accounts) => accounts, 212 222 Err(e) => { 213 223 error!("Failed to fetch account infos: {:?}", e); ··· 223 233 .await 224 234 .unwrap_or_default(); 225 235 226 - let all_codes: Vec<String> = all_invite_codes.iter().map(|(_, c)| c.code.clone()).collect(); 236 + let all_codes: Vec<String> = all_invite_codes 237 + .iter() 238 + .map(|(_, c)| c.code.clone()) 239 + .collect(); 227 240 228 241 let all_invite_uses = if !all_codes.is_empty() { 229 242 state ··· 247 260 all_invite_uses 248 261 .into_iter() 249 262 .fold(HashMap::new(), |mut acc, u| { 250 - acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo { 251 - used_by: u.used_by_did, 252 - used_at: u.used_at.to_rfc3339(), 253 - }); 263 + acc.entry(u.code.clone()) 264 + .or_default() 265 + .push(InviteCodeUseInfo { 266 + used_by: u.used_by_did, 267 + used_at: u.used_at.to_rfc3339(), 268 + }); 254 269 acc 255 270 }); 256 271
+27 -13
crates/tranquil-pds/src/api/admin/account/update.rs
··· 31 31 Ok(d) => d, 32 32 Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 33 33 }; 34 - match state.user_repo.admin_update_email(&account_did, email).await { 34 + match state 35 + .user_repo 36 + .admin_update_email(&account_did, email) 37 + .await 38 + { 35 39 Ok(0) => ApiError::AccountNotFound.into_response(), 36 40 Ok(_) => EmptyResponse::ok().into_response(), 37 41 Err(e) => { ··· 70 74 } else { 71 75 input_handle.to_string() 72 76 }; 73 - let old_handle = state 74 - .user_repo 75 - .get_handle_by_did(did) 76 - .await 77 - .ok() 78 - .flatten(); 77 + let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); 79 78 let user_id = match state.user_repo.get_id_by_did(did).await { 80 79 Ok(Some(id)) => id, 81 80 _ => return ApiError::AccountNotFound.into_response(), 82 81 }; 83 82 let handle_for_check = Handle::new_unchecked(&handle); 84 - if let Ok(true) = state.user_repo.check_handle_exists(&handle_for_check, user_id).await { 83 + if let Ok(true) = state 84 + .user_repo 85 + .check_handle_exists(&handle_for_check, user_id) 86 + .await 87 + { 85 88 return ApiError::HandleTaken.into_response(); 86 89 } 87 - match state.user_repo.admin_update_handle(did, &handle_for_check).await { 90 + match state 91 + .user_repo 92 + .admin_update_handle(did, &handle_for_check) 93 + .await 94 + { 88 95 Ok(0) => ApiError::AccountNotFound.into_response(), 89 96 Ok(_) => { 90 97 if let Some(old) = old_handle { 91 98 let _ = state.cache.delete(&format!("handle:{}", old)).await; 92 99 } 93 100 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 94 - if let Err(e) = 95 - crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle_for_check)) 96 - .await 101 + if let Err(e) = crate::api::repo::record::sequence_identity_event( 102 + &state, 103 + did, 104 + Some(&handle_for_check), 105 + ) 106 + .await 97 107 { 98 108 warn!( 99 109 "Failed to sequence identity event for admin handle update: {}", ··· 137 147 return ApiError::InternalError(None).into_response(); 138 148 } 139 149 }; 140 - match state.user_repo.admin_update_password(did, &password_hash).await { 150 + match state 151 + .user_repo 152 + .admin_update_password(did, &password_hash) 153 + .await 154 + { 141 155 Ok(0) => ApiError::AccountNotFound.into_response(), 142 156 Ok(_) => EmptyResponse::ok().into_response(), 143 157 Err(e) => {
+36 -29
crates/tranquil-pds/src/api/admin/invite.rs
··· 24 24 _auth: BearerAuthAdmin, 25 25 Json(input): Json<DisableInviteCodesInput>, 26 26 ) -> Response { 27 - if let Some(codes) = &input.codes { 28 - if let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await { 29 - error!("DB error disabling invite codes: {:?}", e); 30 - } 27 + if let Some(codes) = &input.codes 28 + && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 + { 30 + error!("DB error disabling invite codes: {:?}", e); 31 31 } 32 32 if let Some(accounts) = &input.accounts { 33 - let accounts_typed: Vec<tranquil_types::Did> = accounts 34 - .iter() 35 - .filter_map(|a| a.parse().ok()) 36 - .collect(); 33 + let accounts_typed: Vec<tranquil_types::Did> = 34 + accounts.iter().filter_map(|a| a.parse().ok()).collect(); 37 35 if let Err(e) = state 38 36 .infra_repo 39 37 .disable_invite_codes_by_account(&accounts_typed) ··· 112 110 .into_iter() 113 111 .collect(); 114 112 115 - let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> = if code_strings 116 - .is_empty() 117 - { 118 - std::collections::HashMap::new() 119 - } else { 120 - state 121 - .infra_repo 122 - .get_invite_code_uses_batch(&code_strings) 123 - .await 124 - .unwrap_or_default() 125 - .into_iter() 126 - .fold(std::collections::HashMap::new(), |mut acc, u| { 127 - acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo { 128 - used_by: u.used_by_did.to_string(), 129 - used_at: u.used_at.to_rfc3339(), 130 - }); 131 - acc 132 - }) 133 - }; 113 + let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> = 114 + if code_strings.is_empty() { 115 + std::collections::HashMap::new() 116 + } else { 117 + state 118 + .infra_repo 119 + .get_invite_code_uses_batch(&code_strings) 120 + .await 121 + .unwrap_or_default() 122 + .into_iter() 123 + .fold(std::collections::HashMap::new(), |mut acc, u| { 124 + acc.entry(u.code.clone()) 125 + .or_default() 126 + .push(InviteCodeUseInfo { 127 + used_by: u.used_by_did.to_string(), 128 + used_at: u.used_at.to_rfc3339(), 129 + }); 130 + acc 131 + }) 132 + }; 134 133 135 134 let codes: Vec<InviteCodeInfo> = codes_rows 136 135 .iter() ··· 184 183 Ok(d) => d, 185 184 Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 186 185 }; 187 - match state.user_repo.set_invites_disabled(&account_did, true).await { 186 + match state 187 + .user_repo 188 + .set_invites_disabled(&account_did, true) 189 + .await 190 + { 188 191 Ok(true) => EmptyResponse::ok().into_response(), 189 192 Ok(false) => ApiError::AccountNotFound.into_response(), 190 193 Err(e) => { ··· 212 215 Ok(d) => d, 213 216 Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 214 217 }; 215 - match state.user_repo.set_invites_disabled(&account_did, false).await { 218 + match state 219 + .user_repo 220 + .set_invites_disabled(&account_did, false) 221 + .await 222 + { 216 223 Ok(true) => EmptyResponse::ok().into_response(), 217 224 Ok(false) => ApiError::AccountNotFound.into_response(), 218 225 Err(e) => {
+13 -8
crates/tranquil-pds/src/api/admin/status.rs
··· 187 187 } else { 188 188 None 189 189 }; 190 - if let Err(e) = state 191 - .user_repo 192 - .set_user_takedown(&did, takedown_ref) 193 - .await 194 - { 190 + if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await { 195 191 error!("Failed to update user takedown status for {}: {:?}", did, e); 196 192 return ApiError::InternalError(Some( 197 193 "Failed to update takedown status".into(), ··· 274 270 if let Some(uri_str) = uri_str { 275 271 let cid: CidLink = match uri_str.parse() { 276 272 Ok(c) => c, 277 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 273 + Err(_) => { 274 + return ApiError::InvalidRequest("Invalid CID format".into()) 275 + .into_response(); 276 + } 278 277 }; 279 278 if let Some(takedown) = &input.takedown { 280 279 let takedown_ref = if takedown.applied { ··· 315 314 if let Some(cid_str) = cid_str { 316 315 let cid: CidLink = match cid_str.parse() { 317 316 Ok(c) => c, 318 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 317 + Err(_) => { 318 + return ApiError::InvalidRequest("Invalid CID format".into()) 319 + .into_response(); 320 + } 319 321 }; 320 322 if let Some(takedown) = &input.takedown { 321 323 let takedown_ref = if takedown.applied { ··· 328 330 .update_blob_takedown(&cid, takedown_ref) 329 331 .await 330 332 { 331 - error!("Failed to update blob takedown status for {}: {:?}", cid_str, e); 333 + error!( 334 + "Failed to update blob takedown status for {}: {:?}", 335 + cid_str, e 336 + ); 332 337 return ApiError::InternalError(Some( 333 338 "Failed to update takedown status".into(), 334 339 ))
+67 -33
crates/tranquil-pds/src/api/backup.rs
··· 14 14 use serde::{Deserialize, Serialize}; 15 15 use serde_json::json; 16 16 use std::str::FromStr; 17 - use tranquil_db::{BackupRepository, OldBackupInfo}; 18 17 use tracing::{error, info, warn}; 18 + use tranquil_db::{BackupRepository, OldBackupInfo}; 19 19 20 20 #[derive(Serialize)] 21 21 #[serde(rename_all = "camelCase")] ··· 36 36 } 37 37 38 38 pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response { 39 - let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.0.did).await { 40 - Ok(Some(status)) => status, 41 - Ok(None) => { 42 - return ApiError::AccountNotFound.into_response(); 43 - } 44 - Err(e) => { 45 - error!("DB error fetching user: {:?}", e); 46 - return ApiError::InternalError(None).into_response(); 47 - } 48 - }; 39 + let (user_id, backup_enabled) = 40 + match state.backup_repo.get_user_backup_status(&auth.0.did).await { 41 + Ok(Some(status)) => status, 42 + Ok(None) => { 43 + return ApiError::AccountNotFound.into_response(); 44 + } 45 + Err(e) => { 46 + error!("DB error fetching user: {:?}", e); 47 + return ApiError::InternalError(None).into_response(); 48 + } 49 + }; 49 50 50 51 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 51 52 Ok(rows) => rows, ··· 94 95 } 95 96 }; 96 97 97 - let backup_info = match state.backup_repo.get_backup_storage_info(backup_id, &auth.0.did).await { 98 + let backup_info = match state 99 + .backup_repo 100 + .get_backup_storage_info(backup_id, &auth.0.did) 101 + .await 102 + { 98 103 Ok(Some(b)) => b, 99 104 Ok(None) => { 100 105 return ApiError::BackupNotFound.into_response(); ··· 181 186 } 182 187 }; 183 188 184 - let car_bytes = 185 - match generate_full_backup(state.repo_repo.as_ref(), &state.block_store, user.id, &head_cid).await { 186 - Ok(bytes) => bytes, 187 - Err(e) => { 188 - error!("Failed to generate CAR: {:?}", e); 189 - return ApiError::InternalError(Some("Failed to generate backup".into())) 190 - .into_response(); 191 - } 192 - }; 189 + let car_bytes = match generate_full_backup( 190 + state.repo_repo.as_ref(), 191 + &state.block_store, 192 + user.id, 193 + &head_cid, 194 + ) 195 + .await 196 + { 197 + Ok(bytes) => bytes, 198 + Err(e) => { 199 + error!("Failed to generate CAR: {:?}", e); 200 + return ApiError::InternalError(Some("Failed to generate backup".into())) 201 + .into_response(); 202 + } 203 + }; 193 204 194 205 let block_count = crate::scheduled::count_car_blocks(&car_bytes); 195 206 let size_bytes = car_bytes.len() as i64; ··· 205 216 } 206 217 }; 207 218 208 - let backup_id = match state.backup_repo.insert_backup( 209 - user.id, 210 - &storage_key, 211 - &user.repo_root_cid, 212 - &repo_rev, 213 - block_count, 214 - size_bytes, 215 - ).await { 219 + let backup_id = match state 220 + .backup_repo 221 + .insert_backup( 222 + user.id, 223 + &storage_key, 224 + &user.repo_root_cid, 225 + &repo_rev, 226 + block_count, 227 + size_bytes, 228 + ) 229 + .await 230 + { 216 231 Ok(id) => id, 217 232 Err(e) => { 218 233 error!("DB error inserting backup: {:?}", e); ··· 235 250 ); 236 251 237 252 let retention = BackupStorage::retention_count(); 238 - if let Err(e) = cleanup_old_backups(state.backup_repo.as_ref(), backup_storage, user.id, retention).await { 253 + if let Err(e) = cleanup_old_backups( 254 + state.backup_repo.as_ref(), 255 + backup_storage, 256 + user.id, 257 + retention, 258 + ) 259 + .await 260 + { 239 261 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 240 262 } 241 263 ··· 298 320 } 299 321 }; 300 322 301 - let backup = match state.backup_repo.get_backup_for_deletion(backup_id, &auth.0.did).await { 323 + let backup = match state 324 + .backup_repo 325 + .get_backup_for_deletion(backup_id, &auth.0.did) 326 + .await 327 + { 302 328 Ok(Some(b)) => b, 303 329 Ok(None) => { 304 330 return ApiError::BackupNotFound.into_response(); ··· 344 370 auth: BearerAuth, 345 371 Json(input): Json<SetBackupEnabledInput>, 346 372 ) -> Response { 347 - let deactivated_at = match state.backup_repo.get_user_deactivated_status(&auth.0.did).await { 373 + let deactivated_at = match state 374 + .backup_repo 375 + .get_user_deactivated_status(&auth.0.did) 376 + .await 377 + { 348 378 Ok(Some(status)) => status, 349 379 Ok(None) => { 350 380 return ApiError::AccountNotFound.into_response(); ··· 359 389 return ApiError::AccountDeactivated.into_response(); 360 390 } 361 391 362 - if let Err(e) = state.backup_repo.update_backup_enabled(&auth.0.did, input.enabled).await { 392 + if let Err(e) = state 393 + .backup_repo 394 + .update_backup_enabled(&auth.0.did, input.enabled) 395 + .await 396 + { 363 397 error!("DB error updating backup_enabled: {:?}", e); 364 398 return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 365 399 }
+23 -11
crates/tranquil-pds/src/api/delegation.rs
··· 11 11 http::{HeaderMap, StatusCode}, 12 12 response::{IntoResponse, Response}, 13 13 }; 14 - use jacquard::types::{integer::LimitedU32, string::Tid}; 14 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 15 15 use jacquard_repo::{mst::Mst, storage::BlockStore}; 16 16 use serde::{Deserialize, Serialize}; 17 17 use serde_json::json; ··· 51 51 controllers: controllers 52 52 .into_iter() 53 53 .map(|c| ControllerInfo { 54 - did: c.did.into(), 55 - handle: c.handle.into(), 54 + did: c.did, 55 + handle: c.handle, 56 56 granted_scopes: c.granted_scopes, 57 57 granted_at: c.granted_at, 58 58 is_active: c.is_active, ··· 89 89 return ApiError::ControllerNotFound.into_response(); 90 90 } 91 91 92 - match state.delegation_repo.controls_any_accounts(&auth.0.did).await { 92 + match state 93 + .delegation_repo 94 + .controls_any_accounts(&auth.0.did) 95 + .await 96 + { 93 97 Ok(true) => { 94 98 return ApiError::InvalidDelegation( 95 99 "Cannot add controllers to an account that controls other accounts".into(), ··· 309 313 accounts: accounts 310 314 .into_iter() 311 315 .map(|a| DelegatedAccountInfo { 312 - did: a.did.into(), 313 - handle: a.handle.into(), 316 + did: a.did, 317 + handle: a.handle, 314 318 granted_scopes: a.granted_scopes, 315 319 granted_at: a.granted_at, 316 320 }) ··· 380 384 .into_iter() 381 385 .map(|e| AuditLogEntry { 382 386 id: e.id.to_string(), 383 - delegated_did: e.delegated_did.into(), 384 - actor_did: e.actor_did.into(), 385 - controller_did: e.controller_did.map(Into::into), 387 + delegated_did: e.delegated_did, 388 + actor_did: e.actor_did, 389 + controller_did: e.controller_did, 386 390 action_type: format!("{:?}", e.action_type), 387 391 action_details: e.action_details, 388 392 created_at: e.created_at, ··· 509 513 } 510 514 511 515 if let Some(ref code) = input.invite_code { 512 - let valid = state.infra_repo.is_invite_code_valid(code).await.unwrap_or(false); 516 + let valid = state 517 + .infra_repo 518 + .is_invite_code_valid(code) 519 + .await 520 + .unwrap_or(false); 513 521 514 522 if !valid { 515 523 return ApiError::InvalidInviteCode.into_response(); ··· 620 628 invite_code: input.invite_code.clone(), 621 629 }; 622 630 623 - let _user_id = match state.user_repo.create_delegated_account(&create_input).await { 631 + let _user_id = match state 632 + .user_repo 633 + .create_delegated_account(&create_input) 634 + .await 635 + { 624 636 Ok(id) => id, 625 637 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 626 638 return ApiError::HandleNotAvailable(None).into_response();
+9 -3
crates/tranquil-pds/src/api/error.rs
··· 398 398 Self::UpstreamError { message, .. } => message.clone(), 399 399 Self::UpstreamTimeout => Some("Upstream service timed out".to_string()), 400 400 Self::AdminRequired => Some("This action requires admin privileges".to_string()), 401 + Self::EmailTaken => Some("This email address is already registered".to_string()), 402 + Self::HandleTaken => Some("This handle is already taken".to_string()), 403 + Self::InvalidEmail => Some("Please provide a valid email address".to_string()), 404 + Self::InvalidInviteCode => Some("The invite code provided is invalid".to_string()), 405 + Self::DuplicateCreate => Some("Account creation failed: duplicate request".to_string()), 401 406 _ => None, 402 407 } 403 408 } ··· 481 486 } 482 487 } 483 488 } 484 - 485 489 486 490 impl From<crate::auth::extractor::AuthError> for ApiError { 487 491 fn from(e: crate::auth::extractor::AuthError) -> Self { ··· 566 570 } 567 571 } 568 572 569 - impl From<jacquard::types::string::AtStrError> for ApiError { 570 - fn from(e: jacquard::types::string::AtStrError) -> Self { 573 + impl From<jacquard_common::types::string::AtStrError> for ApiError { 574 + fn from(e: jacquard_common::types::string::AtStrError) -> Self { 571 575 Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind)) 572 576 } 573 577 } ··· 637 641 } 638 642 } 639 643 644 + #[allow(clippy::result_large_err)] 640 645 pub fn parse_did(s: &str) -> Result<tranquil_types::Did, Response> { 641 646 s.parse() 642 647 .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()).into_response()) 643 648 } 644 649 650 + #[allow(clippy::result_large_err)] 645 651 pub fn parse_did_option(s: Option<&str>) -> Result<Option<tranquil_types::Did>, Response> { 646 652 s.map(parse_did).transpose() 647 653 }
+44 -11
crates/tranquil-pds/src/api/identity/account.rs
··· 13 13 response::{IntoResponse, Response}, 14 14 }; 15 15 use bcrypt::{DEFAULT_COST, hash}; 16 - use jacquard::types::{integer::LimitedU32, string::Tid}; 16 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 17 17 use jacquard_repo::{mst::Mst, storage::BlockStore}; 18 18 use k256::{SecretKey, ecdsa::SigningKey}; 19 19 use rand::rngs::OsRng; ··· 255 255 }; 256 256 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 257 257 if let Some(signing_key_did) = &input.signing_key { 258 - match state.infra_repo.get_reserved_signing_key(signing_key_did).await { 258 + match state 259 + .infra_repo 260 + .get_reserved_signing_key(signing_key_did) 261 + .await 262 + { 259 263 Ok(Some(key)) => (key.private_key_bytes, Some(key.id)), 260 264 Ok(None) => { 261 265 return ApiError::InvalidSigningKey.into_response(); ··· 407 411 did: Did::new_unchecked(&did), 408 412 new_handle: Handle::new_unchecked(&handle), 409 413 }; 410 - match state.user_repo.reactivate_migration_account(&reactivate_input).await { 414 + match state 415 + .user_repo 416 + .reactivate_migration_account(&reactivate_input) 417 + .await 418 + { 411 419 Ok(reactivated) => { 412 420 info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); 413 - let secret_key_bytes = match state.user_repo.get_user_key_by_id(reactivated.user_id).await { 421 + let secret_key_bytes = match state 422 + .user_repo 423 + .get_user_key_by_id(reactivated.user_id) 424 + .await 425 + { 414 426 Ok(Some(key_info)) => { 415 - match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 427 + match crate::config::decrypt_key( 428 + &key_info.key_bytes, 429 + key_info.encryption_version, 430 + ) { 416 431 Ok(k) => k, 417 432 Err(e) => { 418 433 error!("Error decrypting key for reactivated account: {:?}", e); ··· 476 491 ) 477 492 .into_response(); 478 493 } 479 - Err(tranquil_db_traits::MigrationReactivationError::NotFound) => { 480 - } 494 + Err(tranquil_db_traits::MigrationReactivationError::NotFound) => {} 481 495 Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated) => { 482 496 return ApiError::AccountAlreadyExists.into_response(); 483 497 } ··· 492 506 } 493 507 494 508 let handle_typed = Handle::new_unchecked(&handle); 495 - let handle_available = match state.user_repo.check_handle_available_for_new_account(&handle_typed).await { 509 + let handle_available = match state 510 + .user_repo 511 + .check_handle_available_for_new_account(&handle_typed) 512 + .await 513 + { 496 514 Ok(available) => available, 497 515 Err(e) => { 498 516 error!("Error checking handle availability: {:?}", e); ··· 612 630 did: Did::new_unchecked(&did), 613 631 password_hash, 614 632 preferred_comms_channel, 615 - discord_id: input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 616 - telegram_username: input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 617 - signal_number: input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 633 + discord_id: input 634 + .discord_id 635 + .as_deref() 636 + .map(|s| s.trim()) 637 + .filter(|s| !s.is_empty()) 638 + .map(String::from), 639 + telegram_username: input 640 + .telegram_username 641 + .as_deref() 642 + .map(|s| s.trim()) 643 + .filter(|s| !s.is_empty()) 644 + .map(String::from), 645 + signal_number: input 646 + .signal_number 647 + .as_deref() 648 + .map(|s| s.trim()) 649 + .filter(|s| !s.is_empty()) 650 + .map(String::from), 618 651 deactivated_at, 619 652 encrypted_key_bytes, 620 653 encryption_version: crate::config::ENCRYPTION_VERSION,
+58 -44
crates/tranquil-pds/src/api/identity/did.rs
··· 44 44 } 45 45 let handle: Handle = match handle_str.parse() { 46 46 Ok(h) => h, 47 - Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(), 47 + Err(_) => { 48 + return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); 49 + } 48 50 }; 49 51 let user = state.user_repo.get_by_handle(&handle).await; 50 52 match user { ··· 131 133 .into_response() 132 134 } 133 135 134 - async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response { 136 + async fn serve_subdomain_did_doc(state: &AppState, subdomain: &str, hostname: &str) -> Response { 135 137 let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname); 136 - let full_handle = format!("{}.{}", handle, hostname_for_handles); 137 - let full_handle_typed: Handle = match full_handle.parse() { 138 - Ok(h) => h, 139 - Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(), 138 + let subdomain_host = format!("{}.{}", subdomain, hostname_for_handles); 139 + let encoded_subdomain = subdomain_host.replace(':', "%3A"); 140 + let expected_did = format!("did:web:{}", encoded_subdomain); 141 + let expected_did_typed: crate::types::Did = match expected_did.parse() { 142 + Ok(d) => d, 143 + Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), 140 144 }; 141 - let user = match state.user_repo.get_did_web_info_by_handle(&full_handle_typed).await { 145 + let user = match state 146 + .user_repo 147 + .get_user_for_did_doc_build(&expected_did_typed) 148 + .await 149 + { 142 150 Ok(Some(u)) => u, 143 151 Ok(None) => { 144 152 return ApiError::NotFoundMsg("User not found".into()).into_response(); ··· 148 156 return ApiError::InternalError(None).into_response(); 149 157 } 150 158 }; 151 - let (user_id, did, migrated_to_pds) = (user.id, user.did, user.migrated_to_pds); 152 - if !did.starts_with("did:web:") { 153 - return ApiError::NotFoundMsg("User is not did:web".into()).into_response(); 154 - } 155 - let subdomain_host = format!("{}.{}", handle, hostname_for_handles); 156 - let encoded_subdomain = subdomain_host.replace(':', "%3A"); 157 - let expected_self_hosted = format!("did:web:{}", encoded_subdomain); 158 - if did != expected_self_hosted { 159 - return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into()) 160 - .into_response(); 161 - } 159 + let (user_id, current_handle, migrated_to_pds) = (user.id, user.handle, user.migrated_to_pds); 160 + let did = expected_did; 162 161 163 162 let overrides = state 164 163 .user_repo ··· 178 177 let also_known_as = if !ovr.also_known_as.is_empty() { 179 178 ovr.also_known_as.clone() 180 179 } else { 181 - vec![format!("at://{}", full_handle)] 180 + vec![format!("at://{}", current_handle)] 182 181 }; 183 182 184 183 return Json(json!({ ··· 209 208 Ok(None) => return ApiError::InternalError(None).into_response(), 210 209 Err(_) => return ApiError::InternalError(None).into_response(), 211 210 }; 212 - let key_bytes: Vec<u8> = match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 213 - Ok(k) => k, 214 - Err(_) => { 215 - return ApiError::InternalError(None).into_response(); 216 - } 217 - }; 211 + let key_bytes: Vec<u8> = 212 + match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 213 + Ok(k) => k, 214 + Err(_) => { 215 + return ApiError::InternalError(None).into_response(); 216 + } 217 + }; 218 218 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 219 219 Ok(pk) => pk, 220 220 Err(e) => { ··· 227 227 if !ovr.also_known_as.is_empty() { 228 228 ovr.also_known_as.clone() 229 229 } else { 230 - vec![format!("at://{}", full_handle)] 230 + vec![format!("at://{}", current_handle)] 231 231 } 232 232 } else { 233 - vec![format!("at://{}", full_handle)] 233 + vec![format!("at://{}", current_handle)] 234 234 }; 235 235 236 236 Json(json!({ ··· 259 259 pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 260 260 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 261 261 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 262 - let full_handle = format!("{}.{}", handle, hostname_for_handles); 263 - let full_handle_typed: Handle = match full_handle.parse() { 262 + let current_handle = format!("{}.{}", handle, hostname_for_handles); 263 + let current_handle_typed: Handle = match current_handle.parse() { 264 264 Ok(h) => h, 265 - Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(), 265 + Err(_) => { 266 + return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); 267 + } 266 268 }; 267 - let user = match state.user_repo.get_did_web_info_by_handle(&full_handle_typed).await { 269 + let user = match state 270 + .user_repo 271 + .get_did_web_info_by_handle(&current_handle_typed) 272 + .await 273 + { 268 274 Ok(Some(u)) => u, 269 275 Ok(None) => { 270 276 return ApiError::NotFoundMsg("User not found".into()).into_response(); ··· 306 312 let also_known_as = if !ovr.also_known_as.is_empty() { 307 313 ovr.also_known_as.clone() 308 314 } else { 309 - vec![format!("at://{}", full_handle)] 315 + vec![format!("at://{}", current_handle)] 310 316 }; 311 317 312 318 return Json(json!({ ··· 337 343 Ok(None) => return ApiError::InternalError(None).into_response(), 338 344 Err(_) => return ApiError::InternalError(None).into_response(), 339 345 }; 340 - let key_bytes: Vec<u8> = match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 341 - Ok(k) => k, 342 - Err(_) => { 343 - return ApiError::InternalError(None).into_response(); 344 - } 345 - }; 346 + let key_bytes: Vec<u8> = 347 + match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 348 + Ok(k) => k, 349 + Err(_) => { 350 + return ApiError::InternalError(None).into_response(); 351 + } 352 + }; 346 353 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 347 354 Ok(pk) => pk, 348 355 Err(e) => { ··· 355 362 if !ovr.also_known_as.is_empty() { 356 363 ovr.also_known_as.clone() 357 364 } else { 358 - vec![format!("at://{}", full_handle)] 365 + vec![format!("at://{}", current_handle)] 359 366 } 360 367 } else { 361 - vec![format!("at://{}", full_handle)] 368 + vec![format!("at://{}", current_handle)] 362 369 }; 363 370 364 371 Json(json!({ ··· 639 646 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 640 647 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 641 648 let suffix = format!(".{}", hostname_for_handles); 642 - let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles); 643 - let handle = if is_service_domain { 649 + let is_service_domain = 650 + crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles); 651 + let handle = if is_service_domain && new_handle != hostname_for_handles { 644 652 let short_part = if new_handle.ends_with(&suffix) { 645 653 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 646 654 } else { ··· 710 718 }; 711 719 let handle_typed: Handle = match handle.parse() { 712 720 Ok(h) => h, 713 - Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(), 721 + Err(_) => { 722 + return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); 723 + } 714 724 }; 715 - let handle_exists = match state.user_repo.check_handle_exists(&handle_typed, user_id).await { 725 + let handle_exists = match state 726 + .user_repo 727 + .check_handle_exists(&handle_typed, user_id) 728 + .await 729 + { 716 730 Ok(exists) => exists, 717 731 Err(_) => return ApiError::InternalError(None).into_response(), 718 732 };
+34 -31
crates/tranquil-pds/src/api/moderation/mod.rs
··· 71 71 72 72 let key_bytes = match &auth_user.key_bytes { 73 73 Some(kb) => kb.clone(), 74 - None => { 75 - match state.user_repo.get_with_key_by_did(&auth_user.did).await { 76 - Ok(Some(user_with_key)) => { 77 - match crate::config::decrypt_key(&user_with_key.key_bytes, user_with_key.encryption_version) { 78 - Ok(key) => key, 79 - Err(e) => { 80 - error!(error = ?e, "Failed to decrypt user key for report service auth"); 81 - return ApiError::AuthenticationFailed(Some( 82 - "Failed to get signing key".into(), 83 - )) 84 - .into_response(); 85 - } 74 + None => match state.user_repo.get_with_key_by_did(&auth_user.did).await { 75 + Ok(Some(user_with_key)) => { 76 + match crate::config::decrypt_key( 77 + &user_with_key.key_bytes, 78 + user_with_key.encryption_version, 79 + ) { 80 + Ok(key) => key, 81 + Err(e) => { 82 + error!(error = ?e, "Failed to decrypt user key for report service auth"); 83 + return ApiError::AuthenticationFailed(Some( 84 + "Failed to get signing key".into(), 85 + )) 86 + .into_response(); 86 87 } 87 88 } 88 - Ok(None) => { 89 - return ApiError::AuthenticationFailed(Some("User has no signing key".into())) 90 - .into_response(); 91 - } 92 - Err(e) => { 93 - error!(error = ?e, "DB error fetching user key for report"); 94 - return ApiError::AuthenticationFailed(Some( 95 - "Failed to get signing key".into(), 96 - )) 89 + } 90 + Ok(None) => { 91 + return ApiError::AuthenticationFailed(Some("User has no signing key".into())) 97 92 .into_response(); 98 - } 93 + } 94 + Err(e) => { 95 + error!(error = ?e, "DB error fetching user key for report"); 96 + return ApiError::AuthenticationFailed(Some("Failed to get signing key".into())) 97 + .into_response(); 99 98 } 100 - } 99 + }, 101 100 }; 102 101 103 102 let service_token = match crate::auth::create_service_token( ··· 205 204 let report_id = (uuid::Uuid::now_v7().as_u128() & 0x7FFF_FFFF_FFFF_FFFF) as i64; 206 205 let subject_json = json!(input.subject); 207 206 208 - if let Err(e) = state.infra_repo.insert_report( 209 - report_id, 210 - &input.reason_type, 211 - input.reason.as_deref(), 212 - subject_json, 213 - did, 214 - created_at, 215 - ).await { 207 + if let Err(e) = state 208 + .infra_repo 209 + .insert_report( 210 + report_id, 211 + &input.reason_type, 212 + input.reason.as_deref(), 213 + subject_json, 214 + did, 215 + created_at, 216 + ) 217 + .await 218 + { 216 219 error!("Failed to insert report: {:?}", e); 217 220 return ApiError::InternalError(None).into_response(); 218 221 }
+10 -9
crates/tranquil-pds/src/api/notification_prefs.rs
··· 189 189 ) -> Response { 190 190 let user = auth.0; 191 191 192 - let user_row = match state 193 - .user_repo 194 - .get_id_handle_email_by_did(&user.did) 195 - .await 196 - { 192 + let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await { 197 193 Ok(Some(row)) => row, 198 194 Ok(None) => return ApiError::AccountNotFound.into_response(), 199 195 Err(e) => { ··· 238 234 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) { 239 235 info!(did = %user.did, "Email unchanged, skipping"); 240 236 } else { 241 - match state.user_repo.check_email_exists(&email_clean, user_id).await { 237 + match state 238 + .user_repo 239 + .check_email_exists(&email_clean, user_id) 240 + .await 241 + { 242 242 Ok(true) => return ApiError::EmailTaken.into_response(), 243 243 Err(e) => { 244 244 return ApiError::InternalError(Some(format!("Database error: {}", e))) ··· 272 272 } 273 273 info!(did = %user.did, "Cleared Discord ID"); 274 274 } else { 275 - if let Err(e) = 276 - request_channel_verification(&state, user_id, &user.did, "discord", discord_id, None) 277 - .await 275 + if let Err(e) = request_channel_verification( 276 + &state, user_id, &user.did, "discord", discord_id, None, 277 + ) 278 + .await 278 279 { 279 280 return ApiError::InternalError(Some(e)).into_response(); 280 281 }
+3 -4
crates/tranquil-pds/src/api/proxy.rs
··· 150 150 } 151 151 152 152 fn call(&mut self, req: Request) -> Self::Future { 153 - if req 154 - .headers() 155 - .contains_key(http::HeaderName::from(jacquard::xrpc::Header::AtprotoProxy)) 156 - { 153 + if req.headers().contains_key(http::HeaderName::from( 154 + jacquard_common::xrpc::Header::AtprotoProxy, 155 + )) { 157 156 let path = req.uri().path(); 158 157 let method = path.trim_start_matches("/"); 159 158
+7 -7
crates/tranquil-pds/src/api/repo/blob.rs
··· 67 67 debug!("Service token verified for DID: {}", claims.iss); 68 68 let did: Did = match claims.iss.parse() { 69 69 Ok(d) => d, 70 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 70 + Err(_) => { 71 + return ApiError::InvalidDid("Invalid DID format".into()).into_response(); 72 + } 71 73 }; 72 74 (did, false, None) 73 75 } ··· 217 219 } 218 220 }; 219 221 220 - if was_inserted { 221 - if let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 222 - let _ = state.blob_store.delete(&temp_key).await; 223 - error!("Failed to copy blob to final location: {:?}", e); 224 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 225 - } 222 + if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 223 + let _ = state.blob_store.delete(&temp_key).await; 224 + error!("Failed to copy blob to final location: {:?}", e); 225 + return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 226 226 } 227 227 228 228 let _ = state.blob_store.delete(&temp_key).await;
+11 -8
crates/tranquil-pds/src/api/repo/import.rs
··· 6 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 7 use crate::sync::verify::CarVerifier; 8 8 use crate::types::Did; 9 - use tranquil_types::{AtUri, CidLink}; 10 9 use axum::{ 11 10 body::Bytes, 12 11 extract::State, 13 12 response::{IntoResponse, Response}, 14 13 }; 15 - use jacquard::types::{integer::LimitedU32, string::Tid}; 14 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 16 15 use jacquard_repo::storage::BlockStore; 17 16 use k256::ecdsa::SigningKey; 18 17 use serde_json::json; 19 18 use tracing::{debug, error, info, warn}; 19 + use tranquil_types::{AtUri, CidLink}; 20 20 21 21 const DEFAULT_MAX_IMPORT_SIZE: usize = 1024 * 1024 * 1024; 22 22 const DEFAULT_MAX_BLOCKS: usize = 500000; ··· 196 196 .records 197 197 .iter() 198 198 .flat_map(|record| { 199 - let record_uri = AtUri::from_parts(did.as_str(), &record.collection, &record.rkey); 200 - record 201 - .blob_refs 202 - .iter() 203 - .map(move |blob_ref| (record_uri.clone(), CidLink::new_unchecked(blob_ref.cid.clone()))) 199 + let record_uri = 200 + AtUri::from_parts(did.as_str(), &record.collection, &record.rkey); 201 + record.blob_refs.iter().map(move |blob_ref| { 202 + ( 203 + record_uri.clone(), 204 + CidLink::new_unchecked(blob_ref.cid.clone()), 205 + ) 206 + }) 204 207 }) 205 208 .collect(); 206 209 ··· 273 276 return ApiError::InternalError(None).into_response(); 274 277 } 275 278 }; 276 - let new_root_cid_link = CidLink::new_unchecked(&new_root_cid.to_string()); 279 + let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 277 280 if let Err(e) = state 278 281 .repo_repo 279 282 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str)
+3 -1
crates/tranquil-pds/src/api/repo/meta.rs
··· 39 39 }; 40 40 let handle: crate::types::Handle = match handle_str.parse() { 41 41 Ok(h) => h, 42 - Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(), 42 + Err(_) => { 43 + return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); 44 + } 43 45 }; 44 46 state 45 47 .user_repo
+2 -10
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 378 378 } 379 379 } 380 380 381 - let user_id: uuid::Uuid = match state 382 - .user_repo 383 - .get_id_by_did(&did) 384 - .await 385 - { 381 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await { 386 382 Ok(Some(id)) => id, 387 383 _ => return ApiError::InternalError(Some("User not found".into())).into_response(), 388 384 }; 389 - let root_cid_str = match state 390 - .repo_repo 391 - .get_repo_root_cid_by_user_id(user_id) 392 - .await 393 - { 385 + let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await { 394 386 Ok(Some(cid_str)) => cid_str, 395 387 _ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(), 396 388 };
+7 -3
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 202 202 } 203 203 204 204 let deleted_uri = AtUri::from_parts(&did, &input.collection, &input.rkey); 205 - if let Err(e) = state.backlink_repo.remove_backlinks_by_uri(&deleted_uri).await { 205 + if let Err(e) = state 206 + .backlink_repo 207 + .remove_backlinks_by_uri(&deleted_uri) 208 + .await 209 + { 206 210 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 207 211 } 208 212 ··· 245 249 .map_err(|e| format!("Failed to fetch commit: {:?}", e))? 246 250 .ok_or_else(|| "Commit block not found".to_string())?; 247 251 248 - let commit = Commit::from_cbor(&commit_bytes) 249 - .map_err(|e| format!("Failed to parse commit: {:?}", e))?; 252 + let commit = 253 + Commit::from_cbor(&commit_bytes).map_err(|e| format!("Failed to parse commit: {:?}", e))?; 250 254 251 255 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 252 256 let key = format!("{}/{}", collection, rkey);
+12 -13
crates/tranquil-pds/src/api/repo/record/read.rs
··· 65 65 Ok(d) => d, 66 66 Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), 67 67 }; 68 - state 69 - .user_repo 70 - .get_id_by_did(&did) 71 - .await 72 - .map_err(|_| ()) 68 + state.user_repo.get_id_by_did(&did).await.map_err(|_| ()) 73 69 } else { 74 70 let repo_str = input.repo.as_str(); 75 71 let handle_str = if !repo_str.contains('.') { ··· 79 75 }; 80 76 let handle: crate::types::Handle = match handle_str.parse() { 81 77 Ok(h) => h, 82 - Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(), 78 + Err(_) => { 79 + return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); 80 + } 83 81 }; 84 82 state 85 83 .user_repo ··· 166 164 Ok(d) => d, 167 165 Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), 168 166 }; 169 - state 170 - .user_repo 171 - .get_id_by_did(&did) 172 - .await 173 - .map_err(|_| ()) 167 + state.user_repo.get_id_by_did(&did).await.map_err(|_| ()) 174 168 } else { 175 169 let repo_str = input.repo.as_str(); 176 170 let handle_str = if !repo_str.contains('.') { ··· 180 174 }; 181 175 let handle: crate::types::Handle = match handle_str.parse() { 182 176 Ok(h) => h, 183 - Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(), 177 + Err(_) => { 178 + return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); 179 + } 184 180 }; 185 181 state 186 182 .user_repo ··· 200 196 let limit = input.limit.unwrap_or(50).clamp(1, 100); 201 197 let reverse = input.reverse.unwrap_or(false); 202 198 let limit_i64 = limit as i64; 203 - let cursor_rkey = input.cursor.as_ref().and_then(|c| c.parse::<crate::types::Rkey>().ok()); 199 + let cursor_rkey = input 200 + .cursor 201 + .as_ref() 202 + .and_then(|c| c.parse::<crate::types::Rkey>().ok()); 204 203 let rows = match state 205 204 .repo_repo 206 205 .list_records(
+63 -30
crates/tranquil-pds/src/api/repo/record/utils.rs
··· 2 2 use crate::types::{Did, Handle, Nsid, Rkey}; 3 3 use bytes::Bytes; 4 4 use cid::Cid; 5 - use jacquard::types::{integer::LimitedU32, string::Tid}; 5 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 6 6 use jacquard_repo::commit::Commit; 7 7 use jacquard_repo::storage::BlockStore; 8 8 use k256::ecdsa::SigningKey; ··· 36 36 } 37 37 } 38 38 39 - use tranquil_db_traits::Backlink; 40 39 use crate::types::AtUri; 40 + use tranquil_db_traits::Backlink; 41 41 42 42 pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec<Backlink> { 43 43 let record_type = record ··· 50 50 .get("subject") 51 51 .and_then(|v| v.as_str()) 52 52 .filter(|s| s.starts_with("did:")) 53 - .map(|subject| vec![Backlink { 54 - uri: uri.clone(), 55 - path: "subject".to_string(), 56 - link_to: subject.to_string(), 57 - }]) 53 + .map(|subject| { 54 + vec![Backlink { 55 + uri: uri.clone(), 56 + path: "subject".to_string(), 57 + link_to: subject.to_string(), 58 + }] 59 + }) 58 60 .unwrap_or_default(), 59 61 "app.bsky.feed.like" | "app.bsky.feed.repost" => record 60 62 .get("subject") 61 63 .and_then(|v| v.get("uri")) 62 64 .and_then(|v| v.as_str()) 63 65 .filter(|s| s.starts_with("at://")) 64 - .map(|subject_uri| vec![Backlink { 65 - uri: uri.clone(), 66 - path: "subject.uri".to_string(), 67 - link_to: subject_uri.to_string(), 68 - }]) 66 + .map(|subject_uri| { 67 + vec![Backlink { 68 + uri: uri.clone(), 69 + path: "subject.uri".to_string(), 70 + link_to: subject_uri.to_string(), 71 + }] 72 + }) 69 73 .unwrap_or_default(), 70 74 _ => Vec::new(), 71 75 } ··· 78 82 prev: Option<Cid>, 79 83 signing_key: &SigningKey, 80 84 ) -> Result<(Vec<u8>, Bytes), String> { 81 - let did = jacquard::types::string::Did::new(did.as_str()) 85 + let did = jacquard_common::types::string::Did::new(did.as_str()) 82 86 .map_err(|e| format!("Invalid DID: {:?}", e))?; 83 - let rev = 84 - jacquard::types::string::Tid::from_str(rev).map_err(|e| format!("Invalid TID: {:?}", e))?; 87 + let rev = jacquard_common::types::string::Tid::from_str(rev) 88 + .map_err(|e| format!("Invalid TID: {:?}", e))?; 85 89 let unsigned = Commit::new_unsigned(did, data, rev, prev); 86 90 let signed = unsigned 87 91 .sign(signing_key) ··· 133 137 state: &AppState, 134 138 params: CommitParams<'_>, 135 139 ) -> Result<CommitResult, String> { 136 - use tranquil_db_traits::{ApplyCommitError, ApplyCommitInput, CommitEventData, RecordDelete, RecordUpsert}; 140 + use tranquil_db_traits::{ 141 + ApplyCommitError, ApplyCommitInput, CommitEventData, RecordDelete, RecordUpsert, 142 + }; 137 143 138 144 let CommitParams { 139 145 did, ··· 179 185 (Vec::new(), Vec::new()), 180 186 |(mut upserts, mut deletes), op| { 181 187 match op { 182 - RecordOp::Create { collection, rkey, cid } 183 - | RecordOp::Update { collection, rkey, cid, .. } => { 188 + RecordOp::Create { 189 + collection, 190 + rkey, 191 + cid, 192 + } 193 + | RecordOp::Update { 194 + collection, 195 + rkey, 196 + cid, 197 + .. 198 + } => { 184 199 upserts.push(RecordUpsert { 185 200 collection: collection.clone(), 186 201 rkey: rkey.clone(), 187 - cid: crate::types::CidLink::new_unchecked(&cid.to_string()), 202 + cid: crate::types::CidLink::new_unchecked(cid.to_string()), 188 203 }); 189 204 } 190 - RecordOp::Delete { collection, rkey, .. } => { 205 + RecordOp::Delete { 206 + collection, rkey, .. 207 + } => { 191 208 deletes.push(RecordDelete { 192 209 collection: collection.clone(), 193 210 rkey: rkey.clone(), ··· 201 218 let ops_json: Vec<serde_json::Value> = ops 202 219 .iter() 203 220 .map(|op| match op { 204 - RecordOp::Create { collection, rkey, cid } => json!({ 221 + RecordOp::Create { 222 + collection, 223 + rkey, 224 + cid, 225 + } => json!({ 205 226 "action": "create", 206 227 "path": format!("{}/{}", collection, rkey), 207 228 "cid": cid.to_string() 208 229 }), 209 - RecordOp::Update { collection, rkey, cid, prev } => { 230 + RecordOp::Update { 231 + collection, 232 + rkey, 233 + cid, 234 + prev, 235 + } => { 210 236 let mut obj = json!({ 211 237 "action": "update", 212 238 "path": format!("{}/{}", collection, rkey), ··· 217 243 } 218 244 obj 219 245 } 220 - RecordOp::Delete { collection, rkey, prev } => { 246 + RecordOp::Delete { 247 + collection, 248 + rkey, 249 + prev, 250 + } => { 221 251 let mut obj = json!({ 222 252 "action": "delete", 223 253 "path": format!("{}/{}", collection, rkey), ··· 234 264 let commit_event = CommitEventData { 235 265 did: did.clone(), 236 266 event_type: "commit".to_string(), 237 - commit_cid: Some(crate::types::CidLink::new_unchecked(&new_root_cid.to_string())), 238 - prev_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())), 267 + commit_cid: Some(crate::types::CidLink::new_unchecked( 268 + new_root_cid.to_string(), 269 + )), 270 + prev_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(c.to_string())), 239 271 ops: Some(json!(ops_json)), 240 272 blobs: Some(blobs.to_vec()), 241 273 blocks_cids: Some(blocks_cids.to_vec()), 242 - prev_data_cid: prev_data_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())), 274 + prev_data_cid: prev_data_cid.map(|c| crate::types::CidLink::new_unchecked(c.to_string())), 243 275 rev: Some(rev_str.clone()), 244 276 }; 245 277 246 278 let input = ApplyCommitInput { 247 279 user_id, 248 280 did: did.clone(), 249 - expected_root_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())), 250 - new_root_cid: crate::types::CidLink::new_unchecked(&new_root_cid.to_string()), 281 + expected_root_cid: current_root_cid 282 + .map(|c| crate::types::CidLink::new_unchecked(c.to_string())), 283 + new_root_cid: crate::types::CidLink::new_unchecked(new_root_cid.to_string()), 251 284 new_rev: rev_str.clone(), 252 285 new_block_cids: all_block_cids, 253 286 obsolete_block_cids: obsolete_bytes, ··· 424 457 mst_root_cid: &Cid, 425 458 rev: &str, 426 459 ) -> Result<i64, String> { 427 - let commit_cid_link = crate::types::CidLink::new_unchecked(&commit_cid.to_string()); 428 - let mst_root_cid_link = crate::types::CidLink::new_unchecked(&mst_root_cid.to_string()); 460 + let commit_cid_link = crate::types::CidLink::new_unchecked(commit_cid.to_string()); 461 + let mst_root_cid_link = crate::types::CidLink::new_unchecked(mst_root_cid.to_string()); 429 462 state 430 463 .repo_repo 431 464 .insert_genesis_commit_event(did, &commit_cid_link, &mst_root_cid_link, rev)
+29 -14
crates/tranquil-pds/src/api/repo/record/write.rs
··· 1 1 use super::validation::validate_record_with_status; 2 2 use crate::api::error::ApiError; 3 - use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids}; 3 + use crate::api::repo::record::utils::{ 4 + CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 + }; 4 6 use crate::delegation::DelegationActionType; 5 7 use crate::repo::tracking::TrackingBlockStore; 6 8 use crate::state::AppState; ··· 100 102 error!("DB error fetching repo root: {}", e); 101 103 ApiError::InternalError(None).into_response() 102 104 })? 103 - .ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())).into_response())?; 105 + .ok_or_else(|| { 106 + ApiError::InternalError(Some("Repo root not found".into())).into_response() 107 + })?; 104 108 let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| { 105 109 ApiError::InternalError(Some("Invalid repo root CID".into())).into_response() 106 110 })?; ··· 245 249 Err(_) => continue, 246 250 }; 247 251 248 - if mst.blocks_for_path(&conflict_key, &mut all_old_mst_blocks).await.is_err() { 252 + if mst 253 + .blocks_for_path(&conflict_key, &mut all_old_mst_blocks) 254 + .await 255 + .is_err() 256 + { 249 257 error!("Failed to get old MST blocks for conflict {}", conflict_uri); 250 258 } 251 259 252 260 mst = match mst.delete(&conflict_key).await { 253 261 Ok(m) => m, 254 262 Err(e) => { 255 - error!("Failed to delete conflict from MST {}: {:?}", conflict_uri, e); 263 + error!( 264 + "Failed to delete conflict from MST {}: {:?}", 265 + conflict_uri, e 266 + ); 256 267 continue; 257 268 } 258 269 }; ··· 281 292 }; 282 293 let key = format!("{}/{}", input.collection, rkey); 283 294 284 - if mst.blocks_for_path(&key, &mut all_old_mst_blocks).await.is_err() { 295 + if mst 296 + .blocks_for_path(&key, &mut all_old_mst_blocks) 297 + .await 298 + .is_err() 299 + { 285 300 error!("Failed to get old MST blocks for new record path"); 286 301 } 287 302 ··· 355 370 }; 356 371 357 372 for conflict_uri in conflict_uris_to_cleanup { 358 - if let Err(e) = state.backlink_repo.remove_backlinks_by_uri(&conflict_uri).await { 373 + if let Err(e) = state 374 + .backlink_repo 375 + .remove_backlinks_by_uri(&conflict_uri) 376 + .await 377 + { 359 378 error!("Failed to remove backlinks for {}: {}", conflict_uri, e); 360 379 } 361 380 } ··· 381 400 382 401 let created_uri = AtUri::from_parts(&did, &input.collection, &rkey); 383 402 let backlinks = extract_backlinks(&created_uri, &input.record); 384 - if !backlinks.is_empty() { 385 - if let Err(e) = state 386 - .backlink_repo 387 - .add_backlinks(user_id, &backlinks) 388 - .await 389 - { 390 - error!("Failed to add backlinks for {}: {}", created_uri, e); 391 - } 403 + if !backlinks.is_empty() 404 + && let Err(e) = state.backlink_repo.add_backlinks(user_id, &backlinks).await 405 + { 406 + error!("Failed to add backlinks for {}: {}", created_uri, e); 392 407 } 393 408 394 409 (
+43 -56
crates/tranquil-pds/src/api/server/account_status.rs
··· 110 110 String::new() 111 111 }; 112 112 let record_count: i64 = state.repo_repo.count_records(user_id).await.unwrap_or(0); 113 - let imported_blobs: i64 = state.blob_repo.count_blobs_by_user(user_id).await.unwrap_or(0); 113 + let imported_blobs: i64 = state 114 + .blob_repo 115 + .count_blobs_by_user(user_id) 116 + .await 117 + .unwrap_or(0); 114 118 let expected_blobs: i64 = state 115 119 .blob_repo 116 120 .count_distinct_record_blobs(user_id) 117 121 .await 118 122 .unwrap_or(0); 119 - let valid_did = is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; 123 + let valid_did = 124 + is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; 120 125 ( 121 126 StatusCode::OK, 122 127 Json(CheckAccountStatusOutput { ··· 134 139 .into_response() 135 140 } 136 141 137 - async fn is_valid_did_for_service(user_repo: &dyn tranquil_db_traits::UserRepository, cache: Arc<dyn Cache>, did: &crate::types::Did) -> bool { 142 + async fn is_valid_did_for_service( 143 + user_repo: &dyn tranquil_db_traits::UserRepository, 144 + cache: Arc<dyn Cache>, 145 + did: &crate::types::Did, 146 + ) -> bool { 138 147 assert_valid_did_document_for_service(user_repo, cache, did, false) 139 148 .await 140 149 .is_ok() ··· 235 244 .and_then(|v| v.get("atproto")) 236 245 .and_then(|k| k.as_str()); 237 246 238 - let user_key = user_repo 239 - .get_user_key_by_did(&did) 240 - .await 241 - .map_err(|e| { 242 - error!("Failed to fetch user key: {:?}", e); 243 - ApiError::InternalError(None) 244 - })?; 247 + let user_key = user_repo.get_user_key_by_did(did).await.map_err(|e| { 248 + error!("Failed to fetch user key: {:?}", e); 249 + ApiError::InternalError(None) 250 + })?; 245 251 246 252 if let Some(key_info) = user_key { 247 - let key_bytes = crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) 248 - .map_err(|e| { 249 - error!("Failed to decrypt user key: {}", e); 250 - ApiError::InternalError(None) 251 - })?; 253 + let key_bytes = 254 + crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) 255 + .map_err(|e| { 256 + error!("Failed to decrypt user key: {}", e); 257 + ApiError::InternalError(None) 258 + })?; 252 259 let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 253 260 error!("Failed to create signing key: {:?}", e); 254 261 ApiError::InternalError(None) ··· 382 389 did 383 390 ); 384 391 let did_validation_start = std::time::Instant::now(); 385 - if let Err(e) = 386 - assert_valid_did_document_for_service(state.user_repo.as_ref(), state.cache.clone(), &did, true) 387 - .await 392 + if let Err(e) = assert_valid_did_document_for_service( 393 + state.user_repo.as_ref(), 394 + state.cache.clone(), 395 + &did, 396 + true, 397 + ) 398 + .await 388 399 { 389 400 info!( 390 401 "[MIGRATION] activateAccount: DID document validation FAILED for {} (took {:?})", ··· 399 410 did_validation_start.elapsed() 400 411 ); 401 412 402 - let handle = state 403 - .user_repo 404 - .get_handle_by_did(&did) 405 - .await 406 - .ok() 407 - .flatten(); 413 + let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 408 414 info!( 409 415 "[MIGRATION] activateAccount: Activating account did={} handle={:?}", 410 416 did, handle ··· 562 568 563 569 let did = auth_user.did; 564 570 565 - let handle = state 566 - .user_repo 567 - .get_handle_by_did(&did) 568 - .await 569 - .ok() 570 - .flatten(); 571 + let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 571 572 572 - let result = state 573 - .user_repo 574 - .deactivate_account(&did, delete_after) 575 - .await; 573 + let result = state.user_repo.deactivate_account(&did, delete_after).await; 576 574 577 575 match result { 578 576 Ok(true) => { ··· 591 589 } 592 590 EmptyResponse::ok().into_response() 593 591 } 594 - Ok(false) => { 595 - EmptyResponse::ok().into_response() 596 - } 592 + Ok(false) => EmptyResponse::ok().into_response(), 597 593 Err(e) => { 598 594 error!("DB error deactivating account: {:?}", e); 599 595 ApiError::InternalError(None).into_response() ··· 635 631 let did = validated.did.clone(); 636 632 637 633 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &did).await { 638 - return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &did) 639 - .await; 634 + return crate::api::server::reauth::legacy_mfa_required_response( 635 + &*state.user_repo, 636 + &*state.session_repo, 637 + &did, 638 + ) 639 + .await; 640 640 } 641 641 642 642 let user_id = match state.user_repo.get_id_by_did(&did).await { ··· 742 742 let _ = state.infra_repo.delete_deletion_request(token).await; 743 743 return ApiError::ExpiredToken(None).into_response(); 744 744 } 745 - if let Err(e) = state 746 - .user_repo 747 - .delete_account_complete(user_id, did) 748 - .await 749 - { 745 + if let Err(e) = state.user_repo.delete_account_complete(user_id, did).await { 750 746 error!("DB error deleting account: {:?}", e); 751 747 return ApiError::InternalError(None).into_response(); 752 748 } 753 - let account_seq = crate::api::repo::record::sequence_account_event( 754 - &state, 755 - did, 756 - false, 757 - Some("deleted"), 758 - ) 759 - .await; 749 + let account_seq = 750 + crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await; 760 751 match account_seq { 761 752 Ok(seq) => { 762 - if let Err(e) = state 763 - .repo_repo 764 - .delete_sequences_except(did, seq) 765 - .await 766 - { 753 + if let Err(e) = state.repo_repo.delete_sequences_except(did, seq).await { 767 754 warn!( 768 755 "Failed to cleanup sequences for deleted account {}: {}", 769 756 did, e
+10 -3
crates/tranquil-pds/src/api/server/app_password.rs
··· 11 11 }; 12 12 use serde::{Deserialize, Serialize}; 13 13 use serde_json::json; 14 - use tranquil_db_traits::AppPasswordCreate; 15 14 use tracing::{error, warn}; 15 + use tranquil_db_traits::AppPasswordCreate; 16 16 17 17 #[derive(Serialize)] 18 18 #[serde(rename_all = "camelCase")] ··· 53 53 created_at: row.created_at.to_rfc3339(), 54 54 privileged: row.privileged, 55 55 scopes: row.scopes.clone(), 56 - created_by_controller: row.created_by_controller_did.as_ref().map(|d| d.to_string()), 56 + created_by_controller: row 57 + .created_by_controller_did 58 + .as_ref() 59 + .map(|d| d.to_string()), 57 60 }) 58 61 .collect(); 59 62 Json(ListAppPasswordsOutput { passwords }).into_response() ··· 112 115 return ApiError::InvalidRequest("name is required".into()).into_response(); 113 116 } 114 117 115 - match state.session_repo.get_app_password_by_name(user.id, name).await { 118 + match state 119 + .session_repo 120 + .get_app_password_by_name(user.id, name) 121 + .await 122 + { 116 123 Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), 117 124 Err(e) => { 118 125 error!("DB error checking app password: {:?}", e);
+6 -2
crates/tranquil-pds/src/api/server/email.rs
··· 254 254 } 255 255 } 256 256 257 - if let Ok(true) = state.user_repo.check_email_exists(&new_email, user_id).await { 257 + if let Ok(true) = state 258 + .user_repo 259 + .check_email_exists(&new_email, user_id) 260 + .await 261 + { 258 262 return ApiError::InvalidRequest("Email is already in use".into()).into_response(); 259 263 } 260 264 ··· 264 268 } 265 269 266 270 let verification_token = 267 - crate::auth::verification_token::generate_signup_token(&did, "email", &new_email); 271 + crate::auth::verification_token::generate_signup_token(did, "email", &new_email); 268 272 let formatted_token = 269 273 crate::auth::verification_token::format_token_for_display(&verification_token); 270 274 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
+8 -2
crates/tranquil-pds/src/api/server/invite.rs
··· 137 137 infra_repo 138 138 .create_invite_codes_batch(&codes, use_count, admin_user_id, Some(&account)) 139 139 .await 140 - .map(|_| AccountCodes { account: account.to_string(), codes }) 140 + .map(|_| AccountCodes { 141 + account: account.to_string(), 142 + codes, 143 + }) 141 144 } 142 145 })) 143 146 .await; ··· 239 242 available: info.available_uses, 240 243 disabled: false, 241 244 for_account: info.for_account.map(|d| d.to_string()).unwrap_or_default(), 242 - created_by: info.created_by.map(|d| d.to_string()).unwrap_or_else(|| "admin".to_string()), 245 + created_by: info 246 + .created_by 247 + .map(|d| d.to_string()) 248 + .unwrap_or_else(|| "admin".to_string()), 243 249 created_at: info.created_at.to_rfc3339(), 244 250 uses, 245 251 })
+6 -1
crates/tranquil-pds/src/api/server/migration.rs
··· 266 266 }); 267 267 } 268 268 269 - let key_info = state.user_repo.get_user_key_by_id(user.id).await.ok().flatten(); 269 + let key_info = state 270 + .user_repo 271 + .get_user_key_by_id(user.id) 272 + .await 273 + .ok() 274 + .flatten(); 270 275 271 276 let public_key_multibase = match key_info { 272 277 Some(info) => match crate::config::decrypt_key(&info.key_bytes, info.encryption_version) {
+45 -10
crates/tranquil-pds/src/api/server/passkey_account.rs
··· 8 8 }; 9 9 use bcrypt::{DEFAULT_COST, hash}; 10 10 use chrono::{Duration, Utc}; 11 - use jacquard::types::{integer::LimitedU32, string::Tid}; 11 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 12 12 use jacquard_repo::{mst::Mst, storage::BlockStore}; 13 13 use rand::Rng; 14 14 use serde::{Deserialize, Serialize}; ··· 183 183 } 184 184 185 185 if let Some(ref code) = input.invite_code { 186 - let valid = state.infra_repo.is_invite_code_valid(code).await.unwrap_or(false); 186 + let valid = state 187 + .infra_repo 188 + .is_invite_code_valid(code) 189 + .await 190 + .unwrap_or(false); 187 191 188 192 if !valid { 189 193 return ApiError::InvalidInviteCode.into_response(); ··· 226 230 227 231 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<Uuid>) = 228 232 if let Some(signing_key_did) = &input.signing_key { 229 - match state.infra_repo.get_reserved_signing_key(signing_key_did).await { 233 + match state 234 + .infra_repo 235 + .get_reserved_signing_key(signing_key_did) 236 + .await 237 + { 230 238 Ok(Some(reserved)) => (reserved.private_key_bytes, Some(reserved.id)), 231 239 Ok(None) => { 232 240 return ApiError::InvalidSigningKey.into_response(); ··· 433 441 email: email.clone().unwrap_or_default(), 434 442 did: did_typed.clone(), 435 443 preferred_comms_channel, 436 - discord_id: input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 437 - telegram_username: input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 438 - signal_number: input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from), 444 + discord_id: input 445 + .discord_id 446 + .as_deref() 447 + .map(|s| s.trim()) 448 + .filter(|s| !s.is_empty()) 449 + .map(String::from), 450 + telegram_username: input 451 + .telegram_username 452 + .as_deref() 453 + .map(|s| s.trim()) 454 + .filter(|s| !s.is_empty()) 455 + .map(String::from), 456 + signal_number: input 457 + .signal_number 458 + .as_deref() 459 + .map(|s| s.trim()) 460 + .filter(|s| !s.is_empty()) 461 + .map(String::from), 439 462 setup_token_hash, 440 463 setup_expires_at, 441 464 deactivated_at, ··· 715 738 716 739 Json(CompletePasskeySetupResponse { 717 740 did: input.did.clone(), 718 - handle: user.handle.into(), 741 + handle: user.handle, 719 742 app_password, 720 743 app_password_name, 721 744 }) ··· 851 874 format!("{}.{}", identifier, hostname_for_handles) 852 875 }; 853 876 854 - let user = match state.user_repo.get_user_for_passkey_recovery(identifier, &normalized_handle).await { 877 + let user = match state 878 + .user_repo 879 + .get_user_for_passkey_recovery(identifier, &normalized_handle) 880 + .await 881 + { 855 882 Ok(Some(u)) if !u.password_required => u, 856 883 _ => { 857 884 return SuccessResponse::ok().into_response(); ··· 867 894 }; 868 895 let expires_at = Utc::now() + Duration::hours(1); 869 896 870 - if let Err(e) = state.user_repo.set_recovery_token(&user.did, &recovery_token_hash, expires_at).await { 897 + if let Err(e) = state 898 + .user_repo 899 + .set_recovery_token(&user.did, &recovery_token_hash, expires_at) 900 + .await 901 + { 871 902 error!("Error updating recovery token: {:?}", e); 872 903 return ApiError::InternalError(None).into_response(); 873 904 } ··· 944 975 did: input.did.clone(), 945 976 password_hash, 946 977 }; 947 - let result = match state.user_repo.recover_passkey_account(&recover_input).await { 978 + let result = match state 979 + .user_repo 980 + .recover_passkey_account(&recover_input) 981 + .await 982 + { 948 983 Ok(r) => r, 949 984 Err(e) => { 950 985 error!("Error recovering passkey account: {:?}", e);
+15 -4
crates/tranquil-pds/src/api/server/passkeys.rs
··· 269 269 auth: BearerAuth, 270 270 Json(input): Json<DeletePasskeyInput>, 271 271 ) -> Response { 272 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await { 273 - return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did) 274 - .await; 272 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 273 + .await 274 + { 275 + return crate::api::server::reauth::legacy_mfa_required_response( 276 + &*state.user_repo, 277 + &*state.session_repo, 278 + &auth.0.did, 279 + ) 280 + .await; 275 281 } 276 282 277 283 if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 278 - return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 284 + return crate::api::server::reauth::reauth_required_response( 285 + &*state.user_repo, 286 + &*state.session_repo, 287 + &auth.0.did, 288 + ) 289 + .await; 279 290 } 280 291 281 292 let id: uuid::Uuid = match input.id.parse() {
+51 -13
crates/tranquil-pds/src/api/server/password.rs
··· 67 67 }; 68 68 let user_id = match state 69 69 .user_repo 70 - .get_id_by_email_or_handle(&normalized, &normalized_handle) 70 + .get_id_by_email_or_handle(normalized, &normalized_handle) 71 71 .await 72 72 { 73 73 Ok(Some(id)) => id, ··· 209 209 auth: BearerAuth, 210 210 Json(input): Json<ChangePasswordInput>, 211 211 ) -> Response { 212 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await { 213 - return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did) 214 - .await; 212 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 213 + .await 214 + { 215 + return crate::api::server::reauth::legacy_mfa_required_response( 216 + &*state.user_repo, 217 + &*state.session_repo, 218 + &auth.0.did, 219 + ) 220 + .await; 215 221 } 216 222 217 223 let current_password = &input.current_password; ··· 225 231 if let Err(e) = validate_password(new_password) { 226 232 return ApiError::InvalidRequest(e.to_string()).into_response(); 227 233 } 228 - let user = match state.user_repo.get_id_and_password_hash_by_did(&auth.0.did).await { 234 + let user = match state 235 + .user_repo 236 + .get_id_and_password_hash_by_did(&auth.0.did) 237 + .await 238 + { 229 239 Ok(Some(u)) => u, 230 240 Ok(None) => { 231 241 return ApiError::AccountNotFound.into_response(); ··· 259 269 return ApiError::InternalError(None).into_response(); 260 270 } 261 271 }; 262 - if let Err(e) = state.user_repo.update_password_hash(user_id, &new_hash).await { 272 + if let Err(e) = state 273 + .user_repo 274 + .update_password_hash(user_id, &new_hash) 275 + .await 276 + { 263 277 error!("DB error updating password: {:?}", e); 264 278 return ApiError::InternalError(None).into_response(); 265 279 } ··· 279 293 } 280 294 281 295 pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 282 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await { 283 - return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did) 284 - .await; 296 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 297 + .await 298 + { 299 + return crate::api::server::reauth::legacy_mfa_required_response( 300 + &*state.user_repo, 301 + &*state.session_repo, 302 + &auth.0.did, 303 + ) 304 + .await; 285 305 } 286 306 287 307 if crate::api::server::reauth::check_reauth_required_cached( ··· 291 311 ) 292 312 .await 293 313 { 294 - return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 314 + return crate::api::server::reauth::reauth_required_response( 315 + &*state.user_repo, 316 + &*state.session_repo, 317 + &auth.0.did, 318 + ) 319 + .await; 295 320 } 296 321 297 - let has_passkeys = state.user_repo.has_passkeys(&auth.0.did).await.unwrap_or(false); 322 + let has_passkeys = state 323 + .user_repo 324 + .has_passkeys(&auth.0.did) 325 + .await 326 + .unwrap_or(false); 298 327 if !has_passkeys { 299 328 return ApiError::InvalidRequest( 300 329 "You must have at least one passkey registered before removing your password".into(), ··· 344 373 ) 345 374 .await 346 375 { 347 - return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 376 + return crate::api::server::reauth::reauth_required_response( 377 + &*state.user_repo, 378 + &*state.session_repo, 379 + &auth.0.did, 380 + ) 381 + .await; 348 382 } 349 383 350 384 let new_password = &input.new_password; ··· 387 421 } 388 422 }; 389 423 390 - if let Err(e) = state.user_repo.set_new_user_password(user.id, &new_hash).await { 424 + if let Err(e) = state 425 + .user_repo 426 + .set_new_user_password(user.id, &new_hash) 427 + .await 428 + { 391 429 error!("DB error setting password: {:?}", e); 392 430 return ApiError::InternalError(None).into_response(); 393 431 }
+8 -2
crates/tranquil-pds/src/api/server/reauth.rs
··· 372 372 methods 373 373 } 374 374 375 - pub async fn check_reauth_required(session_repo: &dyn SessionRepository, did: &crate::types::Did) -> bool { 375 + pub async fn check_reauth_required( 376 + session_repo: &dyn SessionRepository, 377 + did: &crate::types::Did, 378 + ) -> bool { 376 379 match session_repo.get_last_reauth_at(did).await { 377 380 Ok(last_reauth_at) => is_reauth_required(last_reauth_at), 378 381 _ => true, ··· 427 430 .into_response() 428 431 } 429 432 430 - pub async fn check_legacy_session_mfa(session_repo: &dyn SessionRepository, did: &crate::types::Did) -> bool { 433 + pub async fn check_legacy_session_mfa( 434 + session_repo: &dyn SessionRepository, 435 + did: &crate::types::Did, 436 + ) -> bool { 431 437 match session_repo.get_session_mfa_status(did).await { 432 438 Ok(Some(status)) => { 433 439 if !status.legacy_login {
+3 -1
crates/tranquil-pds/src/api/server/service_auth.rs
··· 119 119 } 120 120 } 121 121 } else { 122 - match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token).await { 122 + match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token) 123 + .await 124 + { 123 125 Ok(user) => user, 124 126 Err(e) => { 125 127 warn!(error = ?e, "getServiceAuth auth validation failed");
+37 -24
crates/tranquil-pds/src/api/server/session.rs
··· 267 267 access_jwt: access_meta.token, 268 268 refresh_jwt: refresh_meta.token, 269 269 handle: handle.into(), 270 - did: row.did.into(), 270 + did: row.did, 271 271 did_doc, 272 272 email: row.email, 273 273 email_confirmed: Some(row.email_verified), ··· 292 292 ); 293 293 match db_result { 294 294 Ok(Some(row)) => { 295 - let (preferred_channel, preferred_channel_verified) = match row.preferred_comms_channel { 295 + let (preferred_channel, preferred_channel_verified) = match row.preferred_comms_channel 296 + { 296 297 tranquil_db_traits::CommsChannel::Email => ("email", row.email_verified), 297 298 tranquil_db_traits::CommsChannel::Discord => ("discord", row.discord_verified), 298 299 tranquil_db_traits::CommsChannel::Telegram => ("telegram", row.telegram_verified), ··· 427 428 return ApiError::InternalError(None).into_response(); 428 429 } 429 430 }; 430 - let key_bytes = 431 - match crate::config::decrypt_key(&session_row.key_bytes, Some(session_row.encryption_version)) { 432 - Ok(k) => k, 433 - Err(e) => { 434 - error!("Failed to decrypt user key: {:?}", e); 435 - return ApiError::InternalError(None).into_response(); 436 - } 437 - }; 431 + let key_bytes = match crate::config::decrypt_key( 432 + &session_row.key_bytes, 433 + Some(session_row.encryption_version), 434 + ) { 435 + Ok(k) => k, 436 + Err(e) => { 437 + error!("Failed to decrypt user key: {:?}", e); 438 + return ApiError::InternalError(None).into_response(); 439 + } 440 + }; 438 441 if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 439 442 return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())) 440 443 .into_response(); ··· 482 485 .into_response(); 483 486 } 484 487 Ok(tranquil_db_traits::RefreshSessionResult::ConcurrentRefresh) => { 485 - warn!("Concurrent refresh detected for session_id: {}", session_row.id); 488 + warn!( 489 + "Concurrent refresh detected for session_id: {}", 490 + session_row.id 491 + ); 486 492 return ApiError::AuthenticationFailed(Some( 487 493 "Refresh token has been revoked due to suspected compromise".into(), 488 494 )) ··· 569 575 Json(input): Json<ConfirmSignupInput>, 570 576 ) -> Response { 571 577 info!("confirm_signup called for DID: {}", input.did); 572 - let row = match state 573 - .user_repo 574 - .get_confirm_signup_by_did(&input.did) 575 - .await 576 - { 578 + let row = match state.user_repo.get_confirm_signup_by_did(&input.did).await { 577 579 Ok(Some(row)) => row, 578 580 Ok(None) => { 579 581 warn!("User not found for confirm_signup: {}", input.did); ··· 653 655 654 656 if let Err(e) = state 655 657 .user_repo 656 - .set_channel_verified(&input.did, row.channel.clone()) 658 + .set_channel_verified(&input.did, row.channel) 657 659 .await 658 660 { 659 661 error!("Failed to update verification status: {:?}", e); ··· 698 700 Json(ConfirmSignupOutput { 699 701 access_jwt: access_meta.token, 700 702 refresh_jwt: refresh_meta.token, 701 - handle: row.handle.into(), 702 - did: row.did.into(), 703 + handle: row.handle, 704 + did: row.did, 703 705 email: row.email, 704 706 email_verified, 705 707 preferred_channel: preferred_channel.to_string(), ··· 830 832 let is_oauth = auth.0.is_oauth; 831 833 let oauth_sessions = oauth_rows.into_iter().map(|row| { 832 834 let client_name = extract_client_name(&row.client_id); 833 - let is_current_oauth = is_oauth && current_jti.as_ref().map(|s| s.as_str()) == Some(row.token_id.as_str()); 835 + let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); 834 836 SessionInfo { 835 837 id: format!("oauth:{}", row.id), 836 838 session_type: "oauth".to_string(), ··· 1003 1005 auth: BearerAuth, 1004 1006 Json(input): Json<UpdateLegacyLoginInput>, 1005 1007 ) -> Response { 1006 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await { 1007 - return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did) 1008 - .await; 1008 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 1009 + .await 1010 + { 1011 + return crate::api::server::reauth::legacy_mfa_required_response( 1012 + &*state.user_repo, 1013 + &*state.session_repo, 1014 + &auth.0.did, 1015 + ) 1016 + .await; 1009 1017 } 1010 1018 1011 1019 if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 1012 - return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 1020 + return crate::api::server::reauth::reauth_required_response( 1021 + &*state.user_repo, 1022 + &*state.session_repo, 1023 + &auth.0.did, 1024 + ) 1025 + .await; 1013 1026 } 1014 1027 1015 1028 match state
+1 -6
crates/tranquil-pds/src/api/server/signing_key.rs
··· 52 52 let private_bytes: &[u8] = &private_key_bytes; 53 53 match state 54 54 .infra_repo 55 - .reserve_signing_key( 56 - did.as_ref(), 57 - &public_key_did_key, 58 - private_bytes, 59 - expires_at, 60 - ) 55 + .reserve_signing_key(did.as_ref(), &public_key_did_key, private_bytes, expires_at) 61 56 .await 62 57 { 63 58 Ok(key_id) => {
+40 -29
crates/tranquil-pds/src/api/server/totp.rs
··· 125 125 return ApiError::TotpAlreadyEnabled.into_response(); 126 126 } 127 127 128 - let secret = 129 - match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) { 130 - Ok(s) => s, 131 - Err(e) => { 132 - error!("Failed to decrypt TOTP secret: {:?}", e); 133 - return ApiError::InternalError(None).into_response(); 134 - } 135 - }; 128 + let secret = match decrypt_totp_secret( 129 + &totp_record.secret_encrypted, 130 + totp_record.encryption_version, 131 + ) { 132 + Ok(s) => s, 133 + Err(e) => { 134 + error!("Failed to decrypt TOTP secret: {:?}", e); 135 + return ApiError::InternalError(None).into_response(); 136 + } 137 + }; 136 138 137 139 let code = input.code.trim(); 138 140 if !verify_totp_code(&secret, code) { ··· 175 177 auth: BearerAuth, 176 178 Json(input): Json<DisableTotpInput>, 177 179 ) -> Response { 178 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await 180 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 181 + .await 179 182 { 180 183 return crate::api::server::reauth::legacy_mfa_required_response( 181 184 &*state.user_repo, ··· 333 336 } 334 337 }; 335 338 336 - let secret = 337 - match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) { 338 - Ok(s) => s, 339 - Err(e) => { 340 - error!("Failed to decrypt TOTP secret: {:?}", e); 341 - return ApiError::InternalError(None).into_response(); 342 - } 343 - }; 339 + let secret = match decrypt_totp_secret( 340 + &totp_record.secret_encrypted, 341 + totp_record.encryption_version, 342 + ) { 343 + Ok(s) => s, 344 + Err(e) => { 345 + error!("Failed to decrypt TOTP secret: {:?}", e); 346 + return ApiError::InternalError(None).into_response(); 347 + } 348 + }; 344 349 345 350 let code = input.code.trim(); 346 351 if !verify_totp_code(&secret, code) { ··· 372 377 Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 373 378 } 374 379 375 - async fn verify_backup_code_for_user(state: &AppState, did: &crate::types::Did, code: &str) -> bool { 380 + async fn verify_backup_code_for_user( 381 + state: &AppState, 382 + did: &crate::types::Did, 383 + code: &str, 384 + ) -> bool { 376 385 let code = code.trim().to_uppercase(); 377 386 378 387 let backup_codes = match state.user_repo.get_unused_backup_codes(did).await { ··· 396 405 } 397 406 } 398 407 399 - pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &crate::types::Did, code: &str) -> bool { 408 + pub async fn verify_totp_or_backup_for_user( 409 + state: &AppState, 410 + did: &crate::types::Did, 411 + code: &str, 412 + ) -> bool { 400 413 let code = code.trim(); 401 414 402 415 if is_backup_code_format(code) { ··· 408 421 _ => return false, 409 422 }; 410 423 411 - let secret = 412 - match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) { 413 - Ok(s) => s, 414 - Err(_) => return false, 415 - }; 424 + let secret = match decrypt_totp_secret( 425 + &totp_record.secret_encrypted, 426 + totp_record.encryption_version, 427 + ) { 428 + Ok(s) => s, 429 + Err(_) => return false, 430 + }; 416 431 417 432 if verify_totp_code(&secret, code) { 418 433 let _ = state.user_repo.update_totp_last_used(did).await; ··· 423 438 } 424 439 425 440 pub async fn has_totp_enabled(state: &AppState, did: &crate::types::Did) -> bool { 426 - state 427 - .user_repo 428 - .has_totp_enabled(did) 429 - .await 430 - .unwrap_or(false) 441 + state.user_repo.has_totp_enabled(did).await.unwrap_or(false) 431 442 }
+10 -3
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 188 188 did: &tranquil_types::Did, 189 189 ) -> DeviceTrustState { 190 190 let device_id_typed = DeviceId::from(device_id.to_string()); 191 - match oauth_repo.get_device_trust_info(&device_id_typed, did).await { 191 + match oauth_repo 192 + .get_device_trust_info(&device_id_typed, did) 193 + .await 194 + { 192 195 Ok(Some(info)) => DeviceTrustState::from_timestamps(info.trusted_at, info.trusted_until), 193 196 _ => DeviceTrustState::Untrusted, 194 197 } ··· 211 214 let now = Utc::now(); 212 215 let trusted_until = now + Duration::days(TRUST_DURATION_DAYS); 213 216 let device_id_typed = DeviceId::from(device_id.to_string()); 214 - oauth_repo.trust_device(&device_id_typed, now, trusted_until).await 217 + oauth_repo 218 + .trust_device(&device_id_typed, now, trusted_until) 219 + .await 215 220 } 216 221 217 222 pub async fn extend_device_trust( ··· 220 225 ) -> Result<(), tranquil_db_traits::DbError> { 221 226 let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS); 222 227 let device_id_typed = DeviceId::from(device_id.to_string()); 223 - oauth_repo.extend_device_trust(&device_id_typed, trusted_until).await 228 + oauth_repo 229 + .extend_device_trust(&device_id_typed, trusted_until) 230 + .await 224 231 }
+9 -3
crates/tranquil-pds/src/api/server/verify_token.rs
··· 74 74 return Err(ApiError::InvalidChannel); 75 75 } 76 76 77 - let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 77 + let did_typed: Did = did 78 + .parse() 79 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 78 80 let user = state 79 81 .user_repo 80 82 .get_verification_info(&did_typed) ··· 116 118 channel: &str, 117 119 identifier: &str, 118 120 ) -> Result<Json<VerifyTokenOutput>, ApiError> { 119 - let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 121 + let did_typed: Did = did 122 + .parse() 123 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 120 124 let user_id = state 121 125 .user_repo 122 126 .get_id_by_did(&did_typed) ··· 189 193 channel: &str, 190 194 _identifier: &str, 191 195 ) -> Result<Json<VerifyTokenOutput>, ApiError> { 192 - let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 196 + let did_typed: Did = did 197 + .parse() 198 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 193 199 let user = state 194 200 .user_repo 195 201 .get_verification_info(&did_typed)
+4 -1
crates/tranquil-pds/src/auth/mod.rs
··· 434 434 .await 435 435 { 436 436 Ok(result) => { 437 - let result_did: Did = result.did.parse().map_err(|_| TokenValidationError::InvalidToken)?; 437 + let result_did: Did = result 438 + .did 439 + .parse() 440 + .map_err(|_| TokenValidationError::InvalidToken)?; 438 441 let user_info = user_repo 439 442 .get_user_info_by_did(&result_did) 440 443 .await
+169 -113
crates/tranquil-pds/src/comms/service.rs
··· 71 71 CommsType::TwoFactorCode => tranquil_db_traits::CommsType::TwoFactorCode, 72 72 CommsType::PasskeyRecovery => tranquil_db_traits::CommsType::PasskeyRecovery, 73 73 CommsType::LegacyLoginAlert => tranquil_db_traits::CommsType::LegacyLoginAlert, 74 - CommsType::MigrationVerification => tranquil_db_traits::CommsType::MigrationVerification, 74 + CommsType::MigrationVerification => { 75 + tranquil_db_traits::CommsType::MigrationVerification 76 + } 75 77 CommsType::ChannelVerification => tranquil_db_traits::CommsType::ChannelVerification, 76 78 }; 77 79 let id = self ··· 136 138 137 139 async fn fetch_pending(&self) -> Result<Vec<QueuedComms>, tranquil_db_traits::DbError> { 138 140 let now = Utc::now(); 139 - self.infra_repo.fetch_pending_comms(now, self.batch_size).await 141 + self.infra_repo 142 + .fetch_pending_comms(now, self.batch_size) 143 + .await 140 144 } 141 145 142 146 async fn process_item(&self, item: QueuedComms) { ··· 162 166 tranquil_db_traits::CommsType::TwoFactorCode => CommsType::TwoFactorCode, 163 167 tranquil_db_traits::CommsType::PasskeyRecovery => CommsType::PasskeyRecovery, 164 168 tranquil_db_traits::CommsType::LegacyLoginAlert => CommsType::LegacyLoginAlert, 165 - tranquil_db_traits::CommsType::MigrationVerification => CommsType::MigrationVerification, 166 - tranquil_db_traits::CommsType::ChannelVerification => CommsType::ChannelVerification, 169 + tranquil_db_traits::CommsType::MigrationVerification => { 170 + CommsType::MigrationVerification 171 + } 172 + tranquil_db_traits::CommsType::ChannelVerification => { 173 + CommsType::ChannelVerification 174 + } 167 175 }, 168 176 status: match item.status { 169 177 tranquil_db_traits::CommsStatus::Pending => CommsStatus::Pending, ··· 250 258 } 251 259 } 252 260 253 - 254 261 pub mod repo { 255 262 use super::*; 256 263 use tranquil_db_traits::DbError; ··· 261 268 user_id: Uuid, 262 269 hostname: &str, 263 270 ) -> Result<Uuid, DbError> { 264 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 271 + let prefs = user_repo 272 + .get_comms_prefs(user_id) 273 + .await? 274 + .ok_or(DbError::NotFound)?; 265 275 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 266 276 let body = format_message( 267 277 strings.welcome_body, ··· 269 279 ); 270 280 let subject = format_message(strings.welcome_subject, &[("hostname", hostname)]); 271 281 let channel = channel_from_str(&prefs.preferred_channel); 272 - infra_repo.enqueue_comms( 273 - Some(user_id), 274 - channel, 275 - CommsType::Welcome, 276 - &prefs.email.unwrap_or_default(), 277 - Some(&subject), 278 - &body, 279 - None, 280 - ).await 282 + infra_repo 283 + .enqueue_comms( 284 + Some(user_id), 285 + channel, 286 + CommsType::Welcome, 287 + &prefs.email.unwrap_or_default(), 288 + Some(&subject), 289 + &body, 290 + None, 291 + ) 292 + .await 281 293 } 282 294 283 295 pub async fn enqueue_password_reset( ··· 287 299 code: &str, 288 300 hostname: &str, 289 301 ) -> Result<Uuid, DbError> { 290 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 302 + let prefs = user_repo 303 + .get_comms_prefs(user_id) 304 + .await? 305 + .ok_or(DbError::NotFound)?; 291 306 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 292 307 let body = format_message( 293 308 strings.password_reset_body, ··· 295 310 ); 296 311 let subject = format_message(strings.password_reset_subject, &[("hostname", hostname)]); 297 312 let channel = channel_from_str(&prefs.preferred_channel); 298 - infra_repo.enqueue_comms( 299 - Some(user_id), 300 - channel, 301 - CommsType::PasswordReset, 302 - &prefs.email.unwrap_or_default(), 303 - Some(&subject), 304 - &body, 305 - None, 306 - ).await 313 + infra_repo 314 + .enqueue_comms( 315 + Some(user_id), 316 + channel, 317 + CommsType::PasswordReset, 318 + &prefs.email.unwrap_or_default(), 319 + Some(&subject), 320 + &body, 321 + None, 322 + ) 323 + .await 307 324 } 308 325 309 326 pub async fn enqueue_email_update( ··· 332 349 ], 333 350 ); 334 351 let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]); 335 - infra_repo.enqueue_comms( 336 - Some(user_id), 337 - tranquil_db_traits::CommsChannel::Email, 338 - CommsType::EmailUpdate, 339 - new_email, 340 - Some(&subject), 341 - &body, 342 - None, 343 - ).await 352 + infra_repo 353 + .enqueue_comms( 354 + Some(user_id), 355 + tranquil_db_traits::CommsChannel::Email, 356 + CommsType::EmailUpdate, 357 + new_email, 358 + Some(&subject), 359 + &body, 360 + None, 361 + ) 362 + .await 344 363 } 345 364 346 365 pub async fn enqueue_email_update_token( ··· 350 369 code: &str, 351 370 hostname: &str, 352 371 ) -> Result<Uuid, DbError> { 353 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 372 + let prefs = user_repo 373 + .get_comms_prefs(user_id) 374 + .await? 375 + .ok_or(DbError::NotFound)?; 354 376 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 355 377 let current_email = prefs.email.unwrap_or_default(); 356 378 let verify_page = format!("https://{}/app/verify?type=email-update", hostname); ··· 369 391 ], 370 392 ); 371 393 let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]); 372 - infra_repo.enqueue_comms( 373 - Some(user_id), 374 - tranquil_db_traits::CommsChannel::Email, 375 - CommsType::EmailUpdate, 376 - &current_email, 377 - Some(&subject), 378 - &body, 379 - None, 380 - ).await 394 + infra_repo 395 + .enqueue_comms( 396 + Some(user_id), 397 + tranquil_db_traits::CommsChannel::Email, 398 + CommsType::EmailUpdate, 399 + &current_email, 400 + Some(&subject), 401 + &body, 402 + None, 403 + ) 404 + .await 381 405 } 382 406 383 407 pub async fn enqueue_account_deletion( ··· 387 411 code: &str, 388 412 hostname: &str, 389 413 ) -> Result<Uuid, DbError> { 390 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 414 + let prefs = user_repo 415 + .get_comms_prefs(user_id) 416 + .await? 417 + .ok_or(DbError::NotFound)?; 391 418 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 392 419 let body = format_message( 393 420 strings.account_deletion_body, ··· 395 422 ); 396 423 let subject = format_message(strings.account_deletion_subject, &[("hostname", hostname)]); 397 424 let channel = channel_from_str(&prefs.preferred_channel); 398 - infra_repo.enqueue_comms( 399 - Some(user_id), 400 - channel, 401 - CommsType::AccountDeletion, 402 - &prefs.email.unwrap_or_default(), 403 - Some(&subject), 404 - &body, 405 - None, 406 - ).await 425 + infra_repo 426 + .enqueue_comms( 427 + Some(user_id), 428 + channel, 429 + CommsType::AccountDeletion, 430 + &prefs.email.unwrap_or_default(), 431 + Some(&subject), 432 + &body, 433 + None, 434 + ) 435 + .await 407 436 } 408 437 409 438 pub async fn enqueue_plc_operation( ··· 413 442 token: &str, 414 443 hostname: &str, 415 444 ) -> Result<Uuid, DbError> { 416 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 445 + let prefs = user_repo 446 + .get_comms_prefs(user_id) 447 + .await? 448 + .ok_or(DbError::NotFound)?; 417 449 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 418 450 let body = format_message( 419 451 strings.plc_operation_body, ··· 421 453 ); 422 454 let subject = format_message(strings.plc_operation_subject, &[("hostname", hostname)]); 423 455 let channel = channel_from_str(&prefs.preferred_channel); 424 - infra_repo.enqueue_comms( 425 - Some(user_id), 426 - channel, 427 - CommsType::PlcOperation, 428 - &prefs.email.unwrap_or_default(), 429 - Some(&subject), 430 - &body, 431 - None, 432 - ).await 456 + infra_repo 457 + .enqueue_comms( 458 + Some(user_id), 459 + channel, 460 + CommsType::PlcOperation, 461 + &prefs.email.unwrap_or_default(), 462 + Some(&subject), 463 + &body, 464 + None, 465 + ) 466 + .await 433 467 } 434 468 435 469 pub async fn enqueue_passkey_recovery( ··· 439 473 recovery_url: &str, 440 474 hostname: &str, 441 475 ) -> Result<Uuid, DbError> { 442 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 476 + let prefs = user_repo 477 + .get_comms_prefs(user_id) 478 + .await? 479 + .ok_or(DbError::NotFound)?; 443 480 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 444 481 let body = format_message( 445 482 strings.passkey_recovery_body, ··· 447 484 ); 448 485 let subject = format_message(strings.passkey_recovery_subject, &[("hostname", hostname)]); 449 486 let channel = channel_from_str(&prefs.preferred_channel); 450 - infra_repo.enqueue_comms( 451 - Some(user_id), 452 - channel, 453 - CommsType::PasskeyRecovery, 454 - &prefs.email.unwrap_or_default(), 455 - Some(&subject), 456 - &body, 457 - None, 458 - ).await 487 + infra_repo 488 + .enqueue_comms( 489 + Some(user_id), 490 + channel, 491 + CommsType::PasskeyRecovery, 492 + &prefs.email.unwrap_or_default(), 493 + Some(&subject), 494 + &body, 495 + None, 496 + ) 497 + .await 459 498 } 460 499 461 500 pub async fn enqueue_migration_verification( ··· 466 505 token: &str, 467 506 hostname: &str, 468 507 ) -> Result<Uuid, DbError> { 469 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 508 + let prefs = user_repo 509 + .get_comms_prefs(user_id) 510 + .await? 511 + .ok_or(DbError::NotFound)?; 470 512 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 471 513 let encoded_email = urlencoding::encode(email); 472 514 let encoded_token = urlencoding::encode(token); ··· 488 530 strings.migration_verification_subject, 489 531 &[("hostname", hostname)], 490 532 ); 491 - infra_repo.enqueue_comms( 492 - Some(user_id), 493 - tranquil_db_traits::CommsChannel::Email, 494 - CommsType::MigrationVerification, 495 - email, 496 - Some(&subject), 497 - &body, 498 - None, 499 - ).await 533 + infra_repo 534 + .enqueue_comms( 535 + Some(user_id), 536 + tranquil_db_traits::CommsChannel::Email, 537 + CommsType::MigrationVerification, 538 + email, 539 + Some(&subject), 540 + &body, 541 + None, 542 + ) 543 + .await 500 544 } 501 545 502 546 pub async fn enqueue_signup_verification( ··· 539 583 )), 540 584 _ => None, 541 585 }; 542 - infra_repo.enqueue_comms( 543 - Some(user_id), 544 - comms_channel, 545 - CommsType::EmailVerification, 546 - recipient, 547 - subject.as_deref(), 548 - &body, 549 - None, 550 - ).await 586 + infra_repo 587 + .enqueue_comms( 588 + Some(user_id), 589 + comms_channel, 590 + CommsType::EmailVerification, 591 + recipient, 592 + subject.as_deref(), 593 + &body, 594 + None, 595 + ) 596 + .await 551 597 } 552 598 553 599 pub async fn enqueue_2fa_code( ··· 557 603 code: &str, 558 604 hostname: &str, 559 605 ) -> Result<Uuid, DbError> { 560 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 606 + let prefs = user_repo 607 + .get_comms_prefs(user_id) 608 + .await? 609 + .ok_or(DbError::NotFound)?; 561 610 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 562 611 let body = format_message( 563 612 strings.two_factor_code_body, ··· 565 614 ); 566 615 let subject = format_message(strings.two_factor_code_subject, &[("hostname", hostname)]); 567 616 let channel = channel_from_str(&prefs.preferred_channel); 568 - infra_repo.enqueue_comms( 569 - Some(user_id), 570 - channel, 571 - CommsType::TwoFactorCode, 572 - &prefs.email.unwrap_or_default(), 573 - Some(&subject), 574 - &body, 575 - None, 576 - ).await 617 + infra_repo 618 + .enqueue_comms( 619 + Some(user_id), 620 + channel, 621 + CommsType::TwoFactorCode, 622 + &prefs.email.unwrap_or_default(), 623 + Some(&subject), 624 + &body, 625 + None, 626 + ) 627 + .await 577 628 } 578 629 579 630 pub async fn enqueue_legacy_login( ··· 584 635 client_ip: &str, 585 636 channel: tranquil_db_traits::CommsChannel, 586 637 ) -> Result<Uuid, DbError> { 587 - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; 638 + let prefs = user_repo 639 + .get_comms_prefs(user_id) 640 + .await? 641 + .ok_or(DbError::NotFound)?; 588 642 let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); 589 643 let timestamp = chrono::Utc::now() 590 644 .format("%Y-%m-%d %H:%M:%S UTC") ··· 599 653 ], 600 654 ); 601 655 let subject = format_message(strings.legacy_login_subject, &[("hostname", hostname)]); 602 - infra_repo.enqueue_comms( 603 - Some(user_id), 604 - channel, 605 - CommsType::LegacyLoginAlert, 606 - &prefs.email.unwrap_or_default(), 607 - Some(&subject), 608 - &body, 609 - None, 610 - ).await 656 + infra_repo 657 + .enqueue_comms( 658 + Some(user_id), 659 + channel, 660 + CommsType::LegacyLoginAlert, 661 + &prefs.email.unwrap_or_default(), 662 + Some(&subject), 663 + &body, 664 + None, 665 + ) 666 + .await 611 667 } 612 668 }
+8 -14
crates/tranquil-pds/src/delegation/scopes.rs
··· 45 45 let granted_has_atproto = granted_set.contains("atproto"); 46 46 let requested_has_atproto = requested_set.contains("atproto"); 47 47 48 - if granted_has_atproto && requested_has_atproto { 49 - return "atproto".to_string(); 50 - } 51 - 52 48 if granted_has_atproto { 53 49 return requested_set.into_iter().collect::<Vec<_>>().join(" "); 54 50 } ··· 116 112 return Ok(()); 117 113 } 118 114 119 - scopes 120 - .split_whitespace() 121 - .try_for_each(|scope| { 122 - let (base, _) = split_scope(scope); 123 - if is_valid_scope_prefix(base) { 124 - Ok(()) 125 - } else { 126 - Err(format!("Invalid scope: {}", scope)) 127 - } 128 - }) 115 + scopes.split_whitespace().try_for_each(|scope| { 116 + let (base, _) = split_scope(scope); 117 + if is_valid_scope_prefix(base) { 118 + Ok(()) 119 + } else { 120 + Err(format!("Invalid scope: {}", scope)) 121 + } 122 + }) 129 123 } 130 124 131 125 fn is_valid_scope_prefix(base: &str) -> bool {
+4
crates/tranquil-pds/src/lib.rs
··· 560 560 ) 561 561 .route("/delegation/auth", post(oauth::endpoints::delegation_auth)) 562 562 .route( 563 + "/delegation/auth-token", 564 + post(oauth::endpoints::delegation_auth_token), 565 + ) 566 + .route( 563 567 "/delegation/totp", 564 568 post(oauth::endpoints::delegation_totp_verify), 565 569 )
+4 -1
crates/tranquil-pds/src/main.rs
··· 36 36 let backfill_block_store = state.block_store.clone(); 37 37 tokio::spawn(async move { 38 38 tokio::join!( 39 - backfill_genesis_commit_blocks(backfill_repo_repo.clone(), backfill_block_store.clone()), 39 + backfill_genesis_commit_blocks( 40 + backfill_repo_repo.clone(), 41 + backfill_block_store.clone() 42 + ), 40 43 backfill_repo_rev(backfill_repo_repo.clone(), backfill_block_store.clone()), 41 44 backfill_user_blocks(backfill_repo_repo.clone(), backfill_block_store.clone()), 42 45 backfill_record_blobs(backfill_repo_repo, backfill_block_store),
+1 -1
crates/tranquil-pds/src/oauth/db/token.rs
··· 1 1 use super::super::OAuthError; 2 2 use tranquil_db_traits::OAuthRepository; 3 - use tranquil_types::{Did, RefreshToken}; 4 3 pub use tranquil_db_traits::RefreshTokenLookup; 4 + use tranquil_types::{Did, RefreshToken}; 5 5 6 6 pub async fn lookup_refresh_token( 7 7 oauth_repo: &dyn OAuthRepository,
+437 -193
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 4 4 db::should_show_consent, 5 5 }; 6 6 use crate::state::{AppState, RateLimitKind}; 7 - use tranquil_db_traits::ScopePreference; 8 7 use crate::types::{Did, Handle, PlainPassword}; 9 - use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; 10 8 use axum::{ 11 9 Json, 12 10 extract::{Query, State}, ··· 19 17 use chrono::Utc; 20 18 use serde::{Deserialize, Serialize}; 21 19 use subtle::ConstantTimeEq; 20 + use tranquil_db_traits::ScopePreference; 21 + use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; 22 22 use urlencoding::encode as url_encode; 23 23 24 24 const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; ··· 207 207 } 208 208 }; 209 209 let request_id = RequestId::from(request_uri.clone()); 210 - let request_data = match state.oauth_repo.get_authorization_request(&request_id).await { 210 + let request_data = match state 211 + .oauth_repo 212 + .get_authorization_request(&request_id) 213 + .await 214 + { 211 215 Ok(Some(data)) => data, 212 216 Ok(None) => { 213 217 if wants_json(&headers) { ··· 239 243 } 240 244 }; 241 245 if request_data.expires_at < Utc::now() { 242 - let _ = state.oauth_repo.delete_authorization_request(&request_id).await; 246 + let _ = state 247 + .oauth_repo 248 + .delete_authorization_request(&request_id) 249 + .await; 243 250 if wants_json(&headers) { 244 251 return ( 245 252 StatusCode::BAD_REQUEST, ··· 287 294 }; 288 295 tracing::info!(normalized = %normalized, "Normalized login_hint"); 289 296 290 - match state.user_repo.get_login_check_by_handle_or_email(&normalized).await { 297 + match state 298 + .user_repo 299 + .get_login_check_by_handle_or_email(&normalized) 300 + .await 301 + { 291 302 Ok(Some(user)) => { 292 303 tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint"); 293 304 let is_delegated = state ··· 298 309 let has_password = user.password_hash.is_some(); 299 310 tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check"); 300 311 301 - if is_delegated && !has_password { 312 + if is_delegated { 302 313 tracing::info!("Redirecting to delegation auth"); 303 314 return redirect_see_other(&format!( 304 315 "/app/oauth/delegation?request_uri={}&delegated_did={}", ··· 320 331 321 332 if !force_new_account 322 333 && let Some(device_id) = extract_device_cookie(&headers) 323 - && let Ok(accounts) = state.oauth_repo.get_device_accounts(&DeviceIdType::from(device_id.clone())).await 334 + && let Ok(accounts) = state 335 + .oauth_repo 336 + .get_device_accounts(&DeviceIdType::from(device_id.clone())) 337 + .await 324 338 && !accounts.is_empty() 325 339 { 326 340 return redirect_see_other(&format!( ··· 342 356 .request_uri 343 357 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; 344 358 let request_id_json = RequestId::from(request_uri.clone()); 345 - let request_data = state.oauth_repo.get_authorization_request(&request_id_json) 359 + let request_data = state 360 + .oauth_repo 361 + .get_authorization_request(&request_id_json) 346 362 .await 347 363 .map_err(crate::oauth::db_err_to_oauth)? 348 364 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 349 365 if request_data.expires_at < Utc::now() { 350 - let _ = state.oauth_repo.delete_authorization_request(&request_id_json).await; 366 + let _ = state 367 + .oauth_repo 368 + .delete_authorization_request(&request_id_json) 369 + .await; 351 370 return Err(OAuthError::InvalidRequest( 352 371 "request_uri has expired".to_string(), 353 372 )); ··· 474 493 ); 475 494 } 476 495 let form_request_id = RequestId::from(form.request_uri.clone()); 477 - let request_data = match state.oauth_repo.get_authorization_request(&form_request_id).await { 496 + let request_data = match state 497 + .oauth_repo 498 + .get_authorization_request(&form_request_id) 499 + .await 500 + { 478 501 Ok(Some(data)) => data, 479 502 Ok(None) => { 480 503 if json_response { ··· 507 530 } 508 531 }; 509 532 if request_data.expires_at < Utc::now() { 510 - let _ = state.oauth_repo.delete_authorization_request(&form_request_id).await; 533 + let _ = state 534 + .oauth_repo 535 + .delete_authorization_request(&form_request_id) 536 + .await; 511 537 if json_response { 512 538 return ( 513 539 axum::http::StatusCode::BAD_REQUEST, ··· 559 585 pds_hostname = %pds_hostname, 560 586 "Normalized username for lookup" 561 587 ); 562 - let user = match state.user_repo.get_login_info_by_handle_or_email(&normalized_username).await { 588 + let user = match state 589 + .user_repo 590 + .get_login_info_by_handle_or_email(&normalized_username) 591 + .await 592 + { 563 593 Ok(Some(u)) => u, 564 594 Ok(None) => { 565 595 let _ = bcrypt::verify( ··· 588 618 } 589 619 590 620 if user.account_type == "delegated" { 591 - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) 621 + if state 622 + .oauth_repo 623 + .set_authorization_did(&form_request_id, &user.did, None) 592 624 .await 593 625 .is_err() 594 626 { ··· 614 646 } 615 647 616 648 if !user.password_required { 617 - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) 649 + if state 650 + .oauth_repo 651 + .set_authorization_did(&form_request_id, &user.did, None) 618 652 .await 619 653 .is_err() 620 654 { ··· 653 687 if has_totp { 654 688 let device_cookie = extract_device_cookie(&headers); 655 689 let device_is_trusted = if let Some(ref dev_id) = device_cookie { 656 - crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did).await 690 + crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did) 691 + .await 657 692 } else { 658 693 false 659 694 }; 660 695 661 696 if device_is_trusted { 662 697 if let Some(ref dev_id) = device_cookie { 663 - let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id).await; 698 + let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) 699 + .await; 664 700 } 665 701 } else { 666 - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) 702 + if state 703 + .oauth_repo 704 + .set_authorization_did(&form_request_id, &user.did, None) 667 705 .await 668 706 .is_err() 669 707 { ··· 682 720 } 683 721 } 684 722 if user.two_factor_enabled { 685 - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&form_request_id).await; 686 - match state.oauth_repo.create_2fa_challenge(&user.did, &form_request_id).await { 723 + let _ = state 724 + .oauth_repo 725 + .delete_2fa_challenge_by_request_uri(&form_request_id) 726 + .await; 727 + match state 728 + .oauth_repo 729 + .create_2fa_challenge(&user.did, &form_request_id) 730 + .await 731 + { 687 732 Ok(challenge) => { 688 733 let hostname = 689 734 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 690 - if let Err(e) = 691 - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await 735 + if let Err(e) = enqueue_2fa_code( 736 + state.user_repo.as_ref(), 737 + state.infra_repo.as_ref(), 738 + user.id, 739 + &challenge.code, 740 + &hostname, 741 + ) 742 + .await 692 743 { 693 744 tracing::warn!( 694 745 did = %user.did, ··· 729 780 last_seen_at: Utc::now(), 730 781 }; 731 782 let new_device_id_typed = DeviceIdType::from(new_id.0.clone()); 732 - if state.oauth_repo.create_device(&new_device_id_typed, &device_data) 783 + if state 784 + .oauth_repo 785 + .create_device(&new_device_id_typed, &device_data) 733 786 .await 734 787 .is_ok() 735 788 { ··· 739 792 new_id.0 740 793 }; 741 794 let final_device_typed = DeviceIdType::from(final_device_id.clone()); 742 - let _ = state.oauth_repo.upsert_account_device(&user.did, &final_device_typed).await; 795 + let _ = state 796 + .oauth_repo 797 + .upsert_account_device(&user.did, &final_device_typed) 798 + .await; 743 799 } 744 800 let set_auth_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 745 801 if state ··· 796 852 let code = Code::generate(); 797 853 let auth_post_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 798 854 let auth_post_code = AuthorizationCode::from(code.0.clone()); 799 - if state.oauth_repo.update_authorization_request( 800 - &form_request_id, 801 - &user.did, 802 - auth_post_device_id.as_ref(), 803 - &auth_post_code, 804 - ) 805 - .await 806 - .is_err() 855 + if state 856 + .oauth_repo 857 + .update_authorization_request( 858 + &form_request_id, 859 + &user.did, 860 + auth_post_device_id.as_ref(), 861 + &auth_post_code, 862 + ) 863 + .await 864 + .is_err() 807 865 { 808 866 return show_login_error("An error occurred. Please try again.", json_response); 809 867 } ··· 859 917 .into_response() 860 918 }; 861 919 let select_request_id = RequestId::from(form.request_uri.clone()); 862 - let request_data = match state.oauth_repo.get_authorization_request(&select_request_id).await { 920 + let request_data = match state 921 + .oauth_repo 922 + .get_authorization_request(&select_request_id) 923 + .await 924 + { 863 925 Ok(Some(data)) => data, 864 926 Ok(None) => { 865 927 return json_error( ··· 877 939 } 878 940 }; 879 941 if request_data.expires_at < Utc::now() { 880 - let _ = state.oauth_repo.delete_authorization_request(&select_request_id).await; 942 + let _ = state 943 + .oauth_repo 944 + .delete_authorization_request(&select_request_id) 945 + .await; 881 946 return json_error( 882 947 StatusCode::BAD_REQUEST, 883 948 "invalid_request", ··· 905 970 } 906 971 }; 907 972 let verify_device_id = DeviceIdType::from(device_id.clone()); 908 - let account_valid = match state.oauth_repo.verify_account_on_device(&verify_device_id, &did).await { 973 + let account_valid = match state 974 + .oauth_repo 975 + .verify_account_on_device(&verify_device_id, &did) 976 + .await 977 + { 909 978 Ok(valid) => valid, 910 979 Err(_) => { 911 980 return json_error( ··· 953 1022 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await; 954 1023 let select_early_device_typed = DeviceIdType::from(device_id.clone()); 955 1024 if has_totp { 956 - if state.oauth_repo.set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) 1025 + if state 1026 + .oauth_repo 1027 + .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) 957 1028 .await 958 1029 .is_err() 959 1030 { ··· 969 1040 .into_response(); 970 1041 } 971 1042 if user.two_factor_enabled { 972 - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&select_request_id).await; 973 - match state.oauth_repo.create_2fa_challenge(&did, &select_request_id).await { 1043 + let _ = state 1044 + .oauth_repo 1045 + .delete_2fa_challenge_by_request_uri(&select_request_id) 1046 + .await; 1047 + match state 1048 + .oauth_repo 1049 + .create_2fa_challenge(&did, &select_request_id) 1050 + .await 1051 + { 974 1052 Ok(challenge) => { 975 1053 let hostname = 976 1054 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 977 - if let Err(e) = 978 - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await 1055 + if let Err(e) = enqueue_2fa_code( 1056 + state.user_repo.as_ref(), 1057 + state.infra_repo.as_ref(), 1058 + user.id, 1059 + &challenge.code, 1060 + &hostname, 1061 + ) 1062 + .await 979 1063 { 980 1064 tracing::warn!( 981 1065 did = %form.did, ··· 1000 1084 } 1001 1085 } 1002 1086 let select_device_typed = DeviceIdType::from(device_id.clone()); 1003 - let _ = state.oauth_repo.upsert_account_device(&did, &select_device_typed).await; 1087 + let _ = state 1088 + .oauth_repo 1089 + .upsert_account_device(&did, &select_device_typed) 1090 + .await; 1004 1091 let code = Code::generate(); 1005 1092 let select_code = AuthorizationCode::from(code.0.clone()); 1006 - if state.oauth_repo.update_authorization_request( 1007 - &select_request_id, 1008 - &did, 1009 - Some(&select_device_typed), 1010 - &select_code, 1011 - ) 1012 - .await 1013 - .is_err() 1093 + if state 1094 + .oauth_repo 1095 + .update_authorization_request( 1096 + &select_request_id, 1097 + &did, 1098 + Some(&select_device_typed), 1099 + &select_code, 1100 + ) 1101 + .await 1102 + .is_err() 1014 1103 { 1015 1104 return json_error( 1016 1105 StatusCode::INTERNAL_SERVER_ERROR, ··· 1121 1210 Json(form): Json<AuthorizeDenyForm>, 1122 1211 ) -> Response { 1123 1212 let deny_request_id = RequestId::from(form.request_uri.clone()); 1124 - let request_data = match state.oauth_repo.get_authorization_request(&deny_request_id).await { 1213 + let request_data = match state 1214 + .oauth_repo 1215 + .get_authorization_request(&deny_request_id) 1216 + .await 1217 + { 1125 1218 Ok(Some(data)) => data, 1126 1219 Ok(None) => { 1127 1220 return ( ··· 1144 1237 .into_response(); 1145 1238 } 1146 1239 }; 1147 - let _ = state.oauth_repo.delete_authorization_request(&deny_request_id).await; 1240 + let _ = state 1241 + .oauth_repo 1242 + .delete_authorization_request(&deny_request_id) 1243 + .await; 1148 1244 let redirect_uri = &request_data.parameters.redirect_uri; 1149 1245 let mut redirect_url = redirect_uri.to_string(); 1150 1246 let separator = if redirect_url.contains('?') { '&' } else { '?' }; ··· 1208 1304 "2FA code has expired. Please start over.", 1209 1305 ); 1210 1306 } 1211 - let _request_data = match state.oauth_repo.get_authorization_request(&twofa_request_id).await { 1307 + let _request_data = match state 1308 + .oauth_repo 1309 + .get_authorization_request(&twofa_request_id) 1310 + .await 1311 + { 1212 1312 Ok(Some(d)) => d, 1213 1313 Ok(None) => { 1214 1314 return redirect_to_frontend_error( ··· 1252 1352 pub show_consent: bool, 1253 1353 pub did: String, 1254 1354 #[serde(skip_serializing_if = "Option::is_none")] 1355 + pub handle: Option<String>, 1356 + #[serde(skip_serializing_if = "Option::is_none")] 1255 1357 pub is_delegation: Option<bool>, 1256 1358 #[serde(skip_serializing_if = "Option::is_none")] 1257 1359 pub controller_did: Option<String>, ··· 1278 1380 Query(query): Query<ConsentQuery>, 1279 1381 ) -> Response { 1280 1382 let consent_request_id = RequestId::from(query.request_uri.clone()); 1281 - let request_data = 1282 - match state.oauth_repo.get_authorization_request(&consent_request_id).await { 1283 - Ok(Some(data)) => data, 1284 - Ok(None) => { 1285 - return json_error( 1286 - StatusCode::BAD_REQUEST, 1287 - "invalid_request", 1288 - "Invalid or expired request_uri", 1289 - ); 1290 - } 1291 - Err(e) => { 1292 - return json_error( 1293 - StatusCode::INTERNAL_SERVER_ERROR, 1294 - "server_error", 1295 - &format!("Database error: {:?}", e), 1296 - ); 1297 - } 1298 - }; 1383 + let request_data = match state 1384 + .oauth_repo 1385 + .get_authorization_request(&consent_request_id) 1386 + .await 1387 + { 1388 + Ok(Some(data)) => data, 1389 + Ok(None) => { 1390 + return json_error( 1391 + StatusCode::BAD_REQUEST, 1392 + "invalid_request", 1393 + "Invalid or expired request_uri", 1394 + ); 1395 + } 1396 + Err(e) => { 1397 + return json_error( 1398 + StatusCode::INTERNAL_SERVER_ERROR, 1399 + "server_error", 1400 + &format!("Database error: {:?}", e), 1401 + ); 1402 + } 1403 + }; 1299 1404 let flow_state = AuthFlowState::from_request_data(&request_data); 1300 1405 1301 1406 if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1302 1407 if flow_state.is_expired() { 1303 - let _ = state.oauth_repo.delete_authorization_request(&consent_request_id).await; 1408 + let _ = state 1409 + .oauth_repo 1410 + .delete_authorization_request(&consent_request_id) 1411 + .await; 1304 1412 } 1305 1413 return err_response; 1306 1414 } ··· 1328 1436 .filter(|s| !s.trim().is_empty()) 1329 1437 .unwrap_or("atproto"); 1330 1438 1331 - let controller_did_parsed: Option<Did> = request_data.controller_did.as_ref().and_then(|s| s.parse().ok()); 1439 + let controller_did_parsed: Option<Did> = request_data 1440 + .controller_did 1441 + .as_ref() 1442 + .and_then(|s| s.parse().ok()); 1332 1443 let delegation_grant = if let Some(ref ctrl_did) = controller_did_parsed { 1333 1444 state 1334 1445 .delegation_repo ··· 1348 1459 1349 1460 let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect(); 1350 1461 let consent_client_id = ClientId::from(request_data.parameters.client_id.clone()); 1351 - let preferences = 1352 - state.oauth_repo.get_scope_preferences(&did, &consent_client_id) 1353 - .await 1354 - .unwrap_or_default(); 1462 + let preferences = state 1463 + .oauth_repo 1464 + .get_scope_preferences(&did, &consent_client_id) 1465 + .await 1466 + .unwrap_or_default(); 1355 1467 let pref_map: std::collections::HashMap<_, _> = preferences 1356 1468 .iter() 1357 1469 .map(|p| (p.scope.as_str(), p.granted)) ··· 1366 1478 ) 1367 1479 .await 1368 1480 .unwrap_or(true); 1481 + let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); 1369 1482 let scopes: Vec<ScopeInfo> = requested_scopes 1370 1483 .iter() 1371 1484 .map(|scope| { 1372 - let (category, required, description, display_name) = 1373 - if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) { 1374 - ( 1375 - def.category.display_name().to_string(), 1376 - def.required, 1377 - def.description.to_string(), 1378 - def.display_name.to_string(), 1379 - ) 1380 - } else if scope.starts_with("ref:") { 1381 - ( 1382 - "Reference".to_string(), 1383 - false, 1384 - "Referenced scope".to_string(), 1385 - scope.to_string(), 1386 - ) 1485 + let (category, required, description, display_name) = if let Some(def) = 1486 + crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) 1487 + { 1488 + let desc = if *scope == "atproto" && has_granular_scopes { 1489 + "AT Protocol baseline scope (permissions determined by selected options below)" 1490 + .to_string() 1387 1491 } else { 1388 - ( 1389 - "Other".to_string(), 1390 - false, 1391 - format!("Access to {}", scope), 1392 - scope.to_string(), 1393 - ) 1492 + def.description.to_string() 1493 + }; 1494 + let name = if *scope == "atproto" && has_granular_scopes { 1495 + "AT Protocol Access".to_string() 1496 + } else { 1497 + def.display_name.to_string() 1394 1498 }; 1499 + ( 1500 + def.category.display_name().to_string(), 1501 + def.required, 1502 + desc, 1503 + name, 1504 + ) 1505 + } else if scope.starts_with("ref:") { 1506 + ( 1507 + "Reference".to_string(), 1508 + false, 1509 + "Referenced scope".to_string(), 1510 + scope.to_string(), 1511 + ) 1512 + } else { 1513 + ( 1514 + "Other".to_string(), 1515 + false, 1516 + format!("Access to {}", scope), 1517 + scope.to_string(), 1518 + ) 1519 + }; 1395 1520 let granted = pref_map.get(*scope).copied(); 1396 1521 ScopeInfo { 1397 1522 scope: scope.to_string(), ··· 1403 1528 } 1404 1529 }) 1405 1530 .collect(); 1531 + 1532 + let account_handle = state 1533 + .user_repo 1534 + .get_handle_by_did(&did) 1535 + .await 1536 + .ok() 1537 + .flatten() 1538 + .map(|h| h.to_string()); 1539 + 1406 1540 let (is_delegation, controller_did_resp, controller_handle, delegation_level) = 1407 1541 if let Some(ref ctrl_did) = controller_did_parsed { 1408 1542 let ctrl_handle = state ··· 1414 1548 .map(|h| h.to_string()); 1415 1549 1416 1550 let level = if let Some(ref grant) = delegation_grant { 1417 - let preset = crate::delegation::SCOPE_PRESETS.iter().find(|p| p.scopes == grant.granted_scopes); 1551 + let preset = crate::delegation::SCOPE_PRESETS 1552 + .iter() 1553 + .find(|p| p.scopes == grant.granted_scopes); 1418 1554 preset 1419 1555 .map(|p| p.label.to_string()) 1420 1556 .unwrap_or_else(|| "Custom".to_string()) ··· 1422 1558 "Unknown".to_string() 1423 1559 }; 1424 1560 1425 - (Some(true), Some(ctrl_did.to_string()), ctrl_handle, Some(level)) 1561 + ( 1562 + Some(true), 1563 + Some(ctrl_did.to_string()), 1564 + ctrl_handle, 1565 + Some(level), 1566 + ) 1426 1567 } else { 1427 1568 (None, None, None, None) 1428 1569 }; ··· 1436 1577 scopes, 1437 1578 show_consent, 1438 1579 did: did_str, 1580 + handle: account_handle, 1439 1581 is_delegation, 1440 1582 controller_did: controller_did_resp, 1441 1583 controller_handle, ··· 1454 1596 form.remember 1455 1597 ); 1456 1598 let consent_post_request_id = RequestId::from(form.request_uri.clone()); 1457 - let request_data = 1458 - match state.oauth_repo.get_authorization_request(&consent_post_request_id).await { 1459 - Ok(Some(data)) => data, 1460 - Ok(None) => { 1461 - return json_error( 1462 - StatusCode::BAD_REQUEST, 1463 - "invalid_request", 1464 - "Invalid or expired request_uri", 1465 - ); 1466 - } 1467 - Err(e) => { 1468 - return json_error( 1469 - StatusCode::INTERNAL_SERVER_ERROR, 1470 - "server_error", 1471 - &format!("Database error: {:?}", e), 1472 - ); 1473 - } 1474 - }; 1599 + let request_data = match state 1600 + .oauth_repo 1601 + .get_authorization_request(&consent_post_request_id) 1602 + .await 1603 + { 1604 + Ok(Some(data)) => data, 1605 + Ok(None) => { 1606 + return json_error( 1607 + StatusCode::BAD_REQUEST, 1608 + "invalid_request", 1609 + "Invalid or expired request_uri", 1610 + ); 1611 + } 1612 + Err(e) => { 1613 + return json_error( 1614 + StatusCode::INTERNAL_SERVER_ERROR, 1615 + "server_error", 1616 + &format!("Database error: {:?}", e), 1617 + ); 1618 + } 1619 + }; 1475 1620 let flow_state = AuthFlowState::from_request_data(&request_data); 1476 1621 1477 1622 if flow_state.is_expired() { 1478 - let _ = state.oauth_repo.delete_authorization_request(&consent_post_request_id).await; 1623 + let _ = state 1624 + .oauth_repo 1625 + .delete_authorization_request(&consent_post_request_id) 1626 + .await; 1479 1627 return json_error( 1480 1628 StatusCode::BAD_REQUEST, 1481 1629 "invalid_request", ··· 1576 1724 }) 1577 1725 .collect(); 1578 1726 let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); 1579 - let _ = state.oauth_repo.upsert_scope_preferences( 1580 - &did, 1581 - &consent_post_client_id, 1582 - &preferences, 1583 - ) 1584 - .await; 1727 + let _ = state 1728 + .oauth_repo 1729 + .upsert_scope_preferences(&did, &consent_post_client_id, &preferences) 1730 + .await; 1585 1731 } 1586 - if let Err(e) = 1587 - state.oauth_repo.update_request_scope(&consent_post_request_id, &approved_scope_str).await 1732 + if let Err(e) = state 1733 + .oauth_repo 1734 + .update_request_scope(&consent_post_request_id, &approved_scope_str) 1735 + .await 1588 1736 { 1589 1737 tracing::warn!("Failed to update request scope: {:?}", e); 1590 1738 } 1591 1739 let code = Code::generate(); 1592 - let consent_post_device_id = request_data.device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 1740 + let consent_post_device_id = request_data 1741 + .device_id 1742 + .as_ref() 1743 + .map(|d| DeviceIdType::from(d.clone())); 1593 1744 let consent_post_code = AuthorizationCode::from(code.0.clone()); 1594 - if state.oauth_repo.update_authorization_request( 1595 - &consent_post_request_id, 1596 - &did, 1597 - consent_post_device_id.as_ref(), 1598 - &consent_post_code, 1599 - ) 1600 - .await 1601 - .is_err() 1745 + if state 1746 + .oauth_repo 1747 + .update_authorization_request( 1748 + &consent_post_request_id, 1749 + &did, 1750 + consent_post_device_id.as_ref(), 1751 + &consent_post_code, 1752 + ) 1753 + .await 1754 + .is_err() 1602 1755 { 1603 1756 return json_error( 1604 1757 StatusCode::INTERNAL_SERVER_ERROR, ··· 1649 1802 ); 1650 1803 } 1651 1804 let twofa_post_request_id = RequestId::from(form.request_uri.clone()); 1652 - let request_data = match state.oauth_repo.get_authorization_request(&twofa_post_request_id).await { 1805 + let request_data = match state 1806 + .oauth_repo 1807 + .get_authorization_request(&twofa_post_request_id) 1808 + .await 1809 + { 1653 1810 Ok(Some(d)) => d, 1654 1811 Ok(None) => { 1655 1812 return json_error( ··· 1667 1824 } 1668 1825 }; 1669 1826 if request_data.expires_at < Utc::now() { 1670 - let _ = state.oauth_repo.delete_authorization_request(&twofa_post_request_id).await; 1827 + let _ = state 1828 + .oauth_repo 1829 + .delete_authorization_request(&twofa_post_request_id) 1830 + .await; 1671 1831 return json_error( 1672 1832 StatusCode::BAD_REQUEST, 1673 1833 "invalid_request", 1674 1834 "Authorization request has expired.", 1675 1835 ); 1676 1836 } 1677 - let challenge = state.oauth_repo.get_2fa_challenge(&twofa_post_request_id) 1837 + let challenge = state 1838 + .oauth_repo 1839 + .get_2fa_challenge(&twofa_post_request_id) 1678 1840 .await 1679 1841 .ok() 1680 1842 .flatten(); 1681 1843 if let Some(challenge) = challenge { 1682 1844 if challenge.expires_at < Utc::now() { 1683 - let _ = state.oauth_repo.delete_2fa_challenge( challenge.id).await; 1845 + let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1684 1846 return json_error( 1685 1847 StatusCode::BAD_REQUEST, 1686 1848 "invalid_request", ··· 1688 1850 ); 1689 1851 } 1690 1852 if challenge.attempts >= MAX_2FA_ATTEMPTS { 1691 - let _ = state.oauth_repo.delete_2fa_challenge( challenge.id).await; 1853 + let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1692 1854 return json_error( 1693 1855 StatusCode::FORBIDDEN, 1694 1856 "access_denied", ··· 1714 1876 let device_id = extract_device_cookie(&headers); 1715 1877 let twofa_totp_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 1716 1878 let twofa_totp_code = AuthorizationCode::from(code.0.clone()); 1717 - if state.oauth_repo.update_authorization_request( 1718 - &twofa_post_request_id, 1719 - &challenge.did, 1720 - twofa_totp_device_id.as_ref(), 1721 - &twofa_totp_code, 1722 - ) 1723 - .await 1724 - .is_err() 1879 + if state 1880 + .oauth_repo 1881 + .update_authorization_request( 1882 + &twofa_post_request_id, 1883 + &challenge.did, 1884 + twofa_totp_device_id.as_ref(), 1885 + &twofa_totp_code, 1886 + ) 1887 + .await 1888 + .is_err() 1725 1889 { 1726 1890 return json_error( 1727 1891 StatusCode::INTERNAL_SERVER_ERROR, ··· 1753 1917 let did: tranquil_types::Did = match did_str.parse() { 1754 1918 Ok(d) => d, 1755 1919 Err(_) => { 1756 - return json_error(StatusCode::BAD_REQUEST, "invalid_request", "Invalid DID format."); 1920 + return json_error( 1921 + StatusCode::BAD_REQUEST, 1922 + "invalid_request", 1923 + "Invalid DID format.", 1924 + ); 1757 1925 } 1758 1926 }; 1759 1927 if !crate::api::server::has_totp_enabled(&state, &did).await { ··· 1817 1985 let code = Code::generate(); 1818 1986 let twofa_final_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 1819 1987 let twofa_final_code = AuthorizationCode::from(code.0.clone()); 1820 - if state.oauth_repo.update_authorization_request( 1821 - &twofa_post_request_id, 1822 - &did, 1823 - twofa_final_device_id.as_ref(), 1824 - &twofa_final_code, 1825 - ) 1826 - .await 1827 - .is_err() 1988 + if state 1989 + .oauth_repo 1990 + .update_authorization_request( 1991 + &twofa_post_request_id, 1992 + &did, 1993 + twofa_final_device_id.as_ref(), 1994 + &twofa_final_code, 1995 + ) 1996 + .await 1997 + .is_err() 1828 1998 { 1829 1999 return json_error( 1830 2000 StatusCode::INTERNAL_SERVER_ERROR, ··· 1935 2105 .is_delegated_account(&u.did) 1936 2106 .await 1937 2107 .unwrap_or(false); 1938 - (passkeys, totp, has_pw, has_controllers, Some(u.did.to_string())) 2108 + ( 2109 + passkeys, 2110 + totp, 2111 + has_pw, 2112 + has_controllers, 2113 + Some(u.did.to_string()), 2114 + ) 1939 2115 } 1940 2116 _ => (false, false, false, false, None), 1941 2117 }; ··· 1985 2161 } 1986 2162 1987 2163 let passkey_start_request_id = RequestId::from(form.request_uri.clone()); 1988 - let request_data = match state.oauth_repo.get_authorization_request(&passkey_start_request_id).await { 2164 + let request_data = match state 2165 + .oauth_repo 2166 + .get_authorization_request(&passkey_start_request_id) 2167 + .await 2168 + { 1989 2169 Ok(Some(data)) => data, 1990 2170 Ok(None) => { 1991 2171 return ( ··· 2010 2190 }; 2011 2191 2012 2192 if request_data.expires_at < Utc::now() { 2013 - let _ = state.oauth_repo.delete_authorization_request(&passkey_start_request_id).await; 2193 + let _ = state 2194 + .oauth_repo 2195 + .delete_authorization_request(&passkey_start_request_id) 2196 + .await; 2014 2197 return ( 2015 2198 StatusCode::BAD_REQUEST, 2016 2199 Json(serde_json::json!({ ··· 2035 2218 normalized_username.to_string() 2036 2219 }; 2037 2220 2038 - let user = match state.user_repo.get_login_info_by_handle_or_email(&normalized_username).await { 2221 + let user = match state 2222 + .user_repo 2223 + .get_login_info_by_handle_or_email(&normalized_username) 2224 + .await 2225 + { 2039 2226 Ok(Some(u)) => u, 2040 2227 Ok(None) => { 2041 2228 return ( ··· 2200 2387 .into_response(); 2201 2388 } 2202 2389 2203 - if state.oauth_repo.set_authorization_did(&passkey_start_request_id, &user.did, None) 2390 + if state 2391 + .oauth_repo 2392 + .set_authorization_did(&passkey_start_request_id, &user.did, None) 2204 2393 .await 2205 2394 .is_err() 2206 2395 { ··· 2231 2420 Json(form): Json<PasskeyFinishInput>, 2232 2421 ) -> Response { 2233 2422 let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 2234 - let request_data = match state.oauth_repo.get_authorization_request(&passkey_finish_request_id).await { 2423 + let request_data = match state 2424 + .oauth_repo 2425 + .get_authorization_request(&passkey_finish_request_id) 2426 + .await 2427 + { 2235 2428 Ok(Some(data)) => data, 2236 2429 Ok(None) => { 2237 2430 return ( ··· 2256 2449 }; 2257 2450 2258 2451 if request_data.expires_at < Utc::now() { 2259 - let _ = state.oauth_repo.delete_authorization_request(&passkey_finish_request_id).await; 2452 + let _ = state 2453 + .oauth_repo 2454 + .delete_authorization_request(&passkey_finish_request_id) 2455 + .await; 2260 2456 return ( 2261 2457 StatusCode::BAD_REQUEST, 2262 2458 Json(serde_json::json!({ ··· 2434 2630 if let Ok(Some(user)) = user 2435 2631 && user.two_factor_enabled 2436 2632 { 2437 - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&passkey_finish_request_id).await; 2438 - match state.oauth_repo.create_2fa_challenge(&did, &passkey_finish_request_id).await { 2633 + let _ = state 2634 + .oauth_repo 2635 + .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) 2636 + .await; 2637 + match state 2638 + .oauth_repo 2639 + .create_2fa_challenge(&did, &passkey_finish_request_id) 2640 + .await 2641 + { 2439 2642 Ok(challenge) => { 2440 2643 let hostname = 2441 2644 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2442 - if let Err(e) = 2443 - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await 2645 + if let Err(e) = enqueue_2fa_code( 2646 + state.user_repo.as_ref(), 2647 + state.infra_repo.as_ref(), 2648 + user.id, 2649 + &challenge.code, 2650 + &hostname, 2651 + ) 2652 + .await 2444 2653 { 2445 2654 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2446 2655 } ··· 2496 2705 let code = Code::generate(); 2497 2706 let passkey_final_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); 2498 2707 let passkey_final_code = AuthorizationCode::from(code.0.clone()); 2499 - if state.oauth_repo.update_authorization_request( 2500 - &passkey_finish_request_id, 2501 - &did, 2502 - passkey_final_device_id.as_ref(), 2503 - &passkey_final_code, 2504 - ) 2505 - .await 2506 - .is_err() 2708 + if state 2709 + .oauth_repo 2710 + .update_authorization_request( 2711 + &passkey_finish_request_id, 2712 + &did, 2713 + passkey_final_device_id.as_ref(), 2714 + &passkey_final_code, 2715 + ) 2716 + .await 2717 + .is_err() 2507 2718 { 2508 2719 return ( 2509 2720 StatusCode::INTERNAL_SERVER_ERROR, ··· 2547 2758 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2548 2759 2549 2760 let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); 2550 - let request_data = match state.oauth_repo.get_authorization_request(&auth_passkey_start_request_id).await { 2761 + let request_data = match state 2762 + .oauth_repo 2763 + .get_authorization_request(&auth_passkey_start_request_id) 2764 + .await 2765 + { 2551 2766 Ok(Some(d)) => d, 2552 2767 Ok(None) => { 2553 2768 return ( ··· 2572 2787 }; 2573 2788 2574 2789 if request_data.expires_at < Utc::now() { 2575 - let _ = state.oauth_repo.delete_authorization_request(&auth_passkey_start_request_id).await; 2790 + let _ = state 2791 + .oauth_repo 2792 + .delete_authorization_request(&auth_passkey_start_request_id) 2793 + .await; 2576 2794 return ( 2577 2795 StatusCode::BAD_REQUEST, 2578 2796 Json(serde_json::json!({ ··· 2719 2937 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2720 2938 let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 2721 2939 2722 - let request_data = match state.oauth_repo.get_authorization_request(&passkey_finish_request_id).await { 2940 + let request_data = match state 2941 + .oauth_repo 2942 + .get_authorization_request(&passkey_finish_request_id) 2943 + .await 2944 + { 2723 2945 Ok(Some(d)) => d, 2724 2946 Ok(None) => { 2725 2947 return ( ··· 2744 2966 }; 2745 2967 2746 2968 if request_data.expires_at < Utc::now() { 2747 - let _ = state.oauth_repo.delete_authorization_request(&passkey_finish_request_id).await; 2969 + let _ = state 2970 + .oauth_repo 2971 + .delete_authorization_request(&passkey_finish_request_id) 2972 + .await; 2748 2973 return ( 2749 2974 StatusCode::BAD_REQUEST, 2750 2975 Json(serde_json::json!({ ··· 2809 3034 } 2810 3035 }; 2811 3036 2812 - let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 2813 - match serde_json::from_str(&auth_state_json) { 2814 - Ok(s) => s, 2815 - Err(e) => { 2816 - tracing::error!("Failed to deserialize authentication state: {:?}", e); 2817 - return ( 3037 + let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = match serde_json::from_str( 3038 + &auth_state_json, 3039 + ) { 3040 + Ok(s) => s, 3041 + Err(e) => { 3042 + tracing::error!("Failed to deserialize authentication state: {:?}", e); 3043 + return ( 2818 3044 StatusCode::INTERNAL_SERVER_ERROR, 2819 3045 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2820 3046 ) 2821 3047 .into_response(); 2822 - } 2823 - }; 3048 + } 3049 + }; 2824 3050 2825 3051 let credential: webauthn_rs::prelude::PublicKeyCredential = 2826 3052 match serde_json::from_value(form.credential.clone()) { ··· 2892 3118 Ok(true) => {} 2893 3119 } 2894 3120 2895 - let has_totp = state.user_repo.has_totp_enabled(&did).await.unwrap_or(false); 3121 + let has_totp = state 3122 + .user_repo 3123 + .has_totp_enabled(&did) 3124 + .await 3125 + .unwrap_or(false); 2896 3126 if has_totp { 2897 3127 let device_cookie = extract_device_cookie(&headers); 2898 3128 let device_is_trusted = if let Some(ref dev_id) = device_cookie { ··· 2903 3133 2904 3134 if device_is_trusted { 2905 3135 if let Some(ref dev_id) = device_cookie { 2906 - let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id).await; 3136 + let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) 3137 + .await; 2907 3138 } 2908 3139 } else { 2909 3140 let user = match state.user_repo.get_2fa_status_by_did(&did).await { ··· 2917 3148 } 2918 3149 }; 2919 3150 2920 - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&passkey_finish_request_id).await; 2921 - match state.oauth_repo.create_2fa_challenge(&did, &passkey_finish_request_id).await { 3151 + let _ = state 3152 + .oauth_repo 3153 + .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) 3154 + .await; 3155 + match state 3156 + .oauth_repo 3157 + .create_2fa_challenge(&did, &passkey_finish_request_id) 3158 + .await 3159 + { 2922 3160 Ok(challenge) => { 2923 - if let Err(e) = 2924 - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &pds_hostname).await 3161 + if let Err(e) = enqueue_2fa_code( 3162 + state.user_repo.as_ref(), 3163 + state.infra_repo.as_ref(), 3164 + user.id, 3165 + &challenge.code, 3166 + &pds_hostname, 3167 + ) 3168 + .await 2925 3169 { 2926 3170 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2927 3171 }
+197 -1
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 + use crate::auth::{extract_auth_token_from_header, validate_token_with_dpop}; 1 2 use crate::delegation::DelegationActionType; 2 3 use crate::state::{AppState, RateLimitKind}; 3 4 use crate::types::PlainPassword; 4 - use crate::util::extract_client_ip; 5 + use crate::util::{build_full_url, extract_client_ip}; 5 6 use axum::{ 6 7 Json, 7 8 extract::State, ··· 446 447 }) 447 448 .into_response() 448 449 } 450 + 451 + #[derive(Debug, Deserialize)] 452 + pub struct DelegationTokenAuthSubmit { 453 + pub request_uri: String, 454 + pub delegated_did: String, 455 + } 456 + 457 + pub async fn delegation_auth_token( 458 + State(state): State<AppState>, 459 + headers: HeaderMap, 460 + Json(form): Json<DelegationTokenAuthSubmit>, 461 + ) -> Response { 462 + let auth_header = headers 463 + .get("authorization") 464 + .and_then(|v| v.to_str().ok()); 465 + 466 + let extracted = match extract_auth_token_from_header(auth_header) { 467 + Some(e) => e, 468 + None => { 469 + return ( 470 + StatusCode::UNAUTHORIZED, 471 + Json(DelegationAuthResponse { 472 + success: false, 473 + needs_totp: None, 474 + redirect_uri: None, 475 + error: Some("Missing or invalid authorization header".to_string()), 476 + }), 477 + ) 478 + .into_response(); 479 + } 480 + }; 481 + 482 + let dpop_proof = headers.get("dpop").and_then(|h| h.to_str().ok()); 483 + let uri = build_full_url("/oauth/delegation/auth-token"); 484 + 485 + let auth_user = match validate_token_with_dpop( 486 + state.user_repo.as_ref(), 487 + state.oauth_repo.as_ref(), 488 + &extracted.token, 489 + extracted.is_dpop, 490 + dpop_proof, 491 + "POST", 492 + &uri, 493 + false, 494 + false, 495 + ) 496 + .await 497 + { 498 + Ok(user) => user, 499 + Err(_) => { 500 + return ( 501 + StatusCode::UNAUTHORIZED, 502 + Json(DelegationAuthResponse { 503 + success: false, 504 + needs_totp: None, 505 + redirect_uri: None, 506 + error: Some("Invalid or expired access token".to_string()), 507 + }), 508 + ) 509 + .into_response(); 510 + } 511 + }; 512 + 513 + let controller_did = auth_user.did; 514 + 515 + let delegated_did: Did = match form.delegated_did.parse() { 516 + Ok(d) => d, 517 + Err(_) => { 518 + return Json(DelegationAuthResponse { 519 + success: false, 520 + needs_totp: None, 521 + redirect_uri: None, 522 + error: Some("Invalid delegated DID".to_string()), 523 + }) 524 + .into_response(); 525 + } 526 + }; 527 + 528 + let request_id = RequestId::from(form.request_uri.clone()); 529 + let request = match state 530 + .oauth_repo 531 + .get_authorization_request(&request_id) 532 + .await 533 + { 534 + Ok(Some(r)) => r, 535 + Ok(None) => { 536 + return Json(DelegationAuthResponse { 537 + success: false, 538 + needs_totp: None, 539 + redirect_uri: None, 540 + error: Some("Authorization request not found".to_string()), 541 + }) 542 + .into_response(); 543 + } 544 + Err(_) => { 545 + return Json(DelegationAuthResponse { 546 + success: false, 547 + needs_totp: None, 548 + redirect_uri: None, 549 + error: Some("Server error".to_string()), 550 + }) 551 + .into_response(); 552 + } 553 + }; 554 + 555 + let grant = match state 556 + .delegation_repo 557 + .get_delegation(&delegated_did, &controller_did) 558 + .await 559 + { 560 + Ok(Some(g)) => g, 561 + Ok(None) => { 562 + return Json(DelegationAuthResponse { 563 + success: false, 564 + needs_totp: None, 565 + redirect_uri: None, 566 + error: Some("No delegation grant found for this controller".to_string()), 567 + }) 568 + .into_response(); 569 + } 570 + Err(_) => { 571 + return Json(DelegationAuthResponse { 572 + success: false, 573 + needs_totp: None, 574 + redirect_uri: None, 575 + error: Some("Server error".to_string()), 576 + }) 577 + .into_response(); 578 + } 579 + }; 580 + 581 + if state 582 + .oauth_repo 583 + .set_request_did(&request_id, &delegated_did) 584 + .await 585 + .is_err() 586 + { 587 + return Json(DelegationAuthResponse { 588 + success: false, 589 + needs_totp: None, 590 + redirect_uri: None, 591 + error: Some("Failed to update authorization request".to_string()), 592 + }) 593 + .into_response(); 594 + } 595 + 596 + if state 597 + .oauth_repo 598 + .set_controller_did(&request_id, &controller_did) 599 + .await 600 + .is_err() 601 + { 602 + return Json(DelegationAuthResponse { 603 + success: false, 604 + needs_totp: None, 605 + redirect_uri: None, 606 + error: Some("Failed to update authorization request".to_string()), 607 + }) 608 + .into_response(); 609 + } 610 + 611 + let ip = extract_client_ip(&headers); 612 + let user_agent = headers 613 + .get("user-agent") 614 + .and_then(|v| v.to_str().ok()) 615 + .map(|s| s.to_string()); 616 + 617 + let _ = state 618 + .delegation_repo 619 + .log_delegation_action( 620 + &delegated_did, 621 + &controller_did, 622 + Some(&controller_did), 623 + DelegationActionType::TokenIssued, 624 + Some(serde_json::json!({ 625 + "client_id": request.client_id, 626 + "granted_scopes": grant.granted_scopes, 627 + "auth_method": "token" 628 + })), 629 + Some(&ip), 630 + user_agent.as_deref(), 631 + ) 632 + .await; 633 + 634 + Json(DelegationAuthResponse { 635 + success: true, 636 + needs_totp: None, 637 + redirect_uri: Some(format!( 638 + "/app/oauth/consent?request_uri={}", 639 + urlencoding::encode(&form.request_uri) 640 + )), 641 + error: None, 642 + }) 643 + .into_response() 644 + }
+1 -1
crates/tranquil-pds/src/oauth/endpoints/par.rs
··· 4 4 scopes::{ParsedScope, parse_scope}, 5 5 }; 6 6 use crate::state::{AppState, RateLimitKind}; 7 - use tranquil_types::RequestId as RequestIdType; 8 7 use axum::body::Bytes; 9 8 use axum::{Json, extract::State, http::HeaderMap}; 10 9 use chrono::{Duration, Utc}; 11 10 use serde::{Deserialize, Serialize}; 11 + use tranquil_types::RequestId as RequestIdType; 12 12 13 13 const PAR_EXPIRY_SECONDS: i64 = 600; 14 14
+13 -13
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
··· 5 5 use crate::oauth::{ 6 6 AuthFlowState, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, 7 7 TokenData, TokenId, 8 - db::{lookup_refresh_token, enforce_token_limit_for_user}, 8 + db::{enforce_token_limit_for_user, lookup_refresh_token}, 9 9 scopes::expand_include_scopes, 10 10 verify_client_auth, 11 11 }; 12 12 use crate::state::AppState; 13 - use tranquil_db_traits::RefreshTokenLookup; 14 - use tranquil_types::{AuthorizationCode, Did, RefreshToken as RefreshTokenType}; 15 13 use axum::Json; 16 14 use axum::http::HeaderMap; 17 15 use chrono::{Duration, Utc}; 16 + use tranquil_db_traits::RefreshTokenLookup; 17 + use tranquil_types::{AuthorizationCode, Did, RefreshToken as RefreshTokenType}; 18 18 19 19 const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300; 20 20 const REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL: i64 = 60; ··· 136 136 let now = Utc::now(); 137 137 138 138 let (raw_scope, controller_did) = if let Some(ref controller) = auth_request.controller_did { 139 - let did_parsed: Did = did.parse().map_err(|_| { 140 - OAuthError::InvalidRequest("Invalid DID format".to_string()) 141 - })?; 142 - let controller_parsed: Did = controller.parse().map_err(|_| { 143 - OAuthError::InvalidRequest("Invalid controller DID format".to_string()) 144 - })?; 139 + let did_parsed: Did = did 140 + .parse() 141 + .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; 142 + let controller_parsed: Did = controller 143 + .parse() 144 + .map_err(|_| OAuthError::InvalidRequest("Invalid controller DID format".to_string()))?; 145 145 let grant = state 146 146 .delegation_repo 147 147 .get_delegation(&did_parsed, &controller_parsed) ··· 216 216 let oauth_repo = state.oauth_repo.clone(); 217 217 let did_clone = did.clone(); 218 218 async move { 219 - if let Ok(did_typed) = did_clone.parse::<tranquil_types::Did>() { 220 - if let Err(e) = enforce_token_limit_for_user(oauth_repo.as_ref(), &did_typed).await { 221 - tracing::warn!("Failed to enforce token limit for user: {:?}", e); 222 - } 219 + if let Ok(did_typed) = did_clone.parse::<tranquil_types::Did>() 220 + && let Err(e) = enforce_token_limit_for_user(oauth_repo.as_ref(), &did_typed).await 221 + { 222 + tracing::warn!("Failed to enforce token limit for user: {:?}", e); 223 223 } 224 224 } 225 225 });
+4 -1
crates/tranquil-pds/src/oauth/verify.rs
··· 385 385 did: String, 386 386 } 387 387 388 - async fn try_legacy_auth(user_repo: &dyn UserRepository, token: &str) -> Result<LegacyAuthResult, ()> { 388 + async fn try_legacy_auth( 389 + user_repo: &dyn UserRepository, 390 + token: &str, 391 + ) -> Result<LegacyAuthResult, ()> { 389 392 match crate::auth::validate_bearer_token(user_repo, token).await { 390 393 Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { 391 394 did: user.did.to_string(),
+3 -5
crates/tranquil-pds/src/plc/mod.rs
··· 526 526 } 527 527 let cbor_bytes = serde_ipld_dagcbor::to_vec(&unsigned_op) 528 528 .map_err(|e| PlcError::Serialization(e.to_string()))?; 529 - let verified = rotation_keys 530 - .iter() 531 - .any(|key_did| { 532 - verify_signature_with_did_key(key_did, &cbor_bytes, &signature).unwrap_or(false) 533 - }); 529 + let verified = rotation_keys.iter().any(|key_did| { 530 + verify_signature_with_did_key(key_did, &cbor_bytes, &signature).unwrap_or(false) 531 + }); 534 532 Ok(verified) 535 533 } 536 534
+35 -17
crates/tranquil-pds/src/scheduled.rs
··· 76 76 (s + 1, f) 77 77 } 78 78 Err((seq, reason)) => { 79 - warn!(seq = seq, reason = reason, "Failed to process genesis commit"); 79 + warn!( 80 + seq = seq, 81 + reason = reason, 82 + "Failed to process genesis commit" 83 + ); 80 84 (s, f + 1) 81 85 } 82 86 }); ··· 94 98 repo_root_cid: String, 95 99 ) -> Result<uuid::Uuid, uuid::Uuid> { 96 100 let cid = Cid::from_str(&repo_root_cid).map_err(|_| user_id)?; 97 - let block = block_store 98 - .get(&cid) 99 - .await 100 - .ok() 101 - .flatten() 102 - .ok_or(user_id)?; 101 + let block = block_store.get(&cid).await.ok().flatten().ok_or(user_id)?; 103 102 let commit = Commit::from_cbor(&block).map_err(|_| user_id)?; 104 103 let rev = commit.rev().to_string(); 105 104 repo_repo ··· 135 134 let repo_repo = repo_repo.clone(); 136 135 let block_store = block_store.clone(); 137 136 async move { 138 - process_repo_rev(repo_repo.as_ref(), &block_store, repo.user_id, repo.repo_root_cid.to_string()) 139 - .await 137 + process_repo_rev( 138 + repo_repo.as_ref(), 139 + &block_store, 140 + repo.user_id, 141 + repo.repo_root_cid.to_string(), 142 + ) 143 + .await 140 144 } 141 145 })) 142 146 .await; ··· 304 308 blob_refs 305 309 .into_iter() 306 310 .map(|blob_ref| { 307 - let record_uri = 308 - AtUri::from_parts(did.as_str(), record.collection.as_str(), record.rkey.as_str()); 311 + let record_uri = AtUri::from_parts( 312 + did.as_str(), 313 + record.collection.as_str(), 314 + record.rkey.as_str(), 315 + ); 309 316 (record_uri, CidLink::new_unchecked(blob_ref.cid)) 310 317 }) 311 318 .collect::<Vec<_>>(), ··· 335 342 repo_repo: Arc<dyn RepoRepository>, 336 343 block_store: PostgresBlockStore, 337 344 ) { 338 - let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await { 345 + let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await 346 + { 339 347 Ok(rows) => rows, 340 348 Err(e) => { 341 349 error!("Failed to query users for record_blobs backfill: {:?}", e); ··· 449 457 .into_iter() 450 458 .for_each(|(did, handle, result)| match result { 451 459 Ok(()) => info!(did = %did, handle = %handle, "Successfully deleted scheduled account"), 452 - Err(e) => warn!(did = %did, handle = %handle, error = %e, "Failed to delete scheduled account"), 460 + Err(e) => { 461 + warn!(did = %did, handle = %handle, error = %e, "Failed to delete scheduled account") 462 + } 453 463 }); 454 464 455 465 Ok(()) ··· 545 555 Failed(String, String), 546 556 } 547 557 558 + #[allow(clippy::too_many_arguments)] 548 559 async fn process_single_backup( 549 560 repo_repo: &dyn RepoRepository, 550 561 backup_repo: &dyn BackupRepository, ··· 657 668 block_count = result.block_count, 658 669 "Created backup" 659 670 ); 660 - if let Err(e) = 661 - cleanup_old_backups(backup_repo, backup_storage, result.user_id, retention_count) 662 - .await 671 + if let Err(e) = cleanup_old_backups( 672 + backup_repo, 673 + backup_storage, 674 + result.user_id, 675 + retention_count, 676 + ) 677 + .await 663 678 { 664 679 warn!(did = %result.did, error = %e, "Failed to cleanup old backups"); 665 680 } ··· 810 825 match backup_storage.delete_backup(&backup.storage_key).await { 811 826 Ok(()) => match backup_repo.delete_backup(backup.id).await { 812 827 Ok(()) => Ok(()), 813 - Err(e) => Err(format!("DB delete failed for {}: {:?}", backup.storage_key, e)), 828 + Err(e) => Err(format!( 829 + "DB delete failed for {}: {:?}", 830 + backup.storage_key, e 831 + )), 814 832 }, 815 833 Err(e) => { 816 834 warn!(
+13 -15
crates/tranquil-pds/src/sync/blob.rs
··· 47 47 48 48 let blob_result = state.blob_repo.get_blob_metadata(&cid).await; 49 49 match blob_result { 50 - Ok(Some(metadata)) => { 51 - match state.blob_store.get(&metadata.storage_key).await { 52 - Ok(data) => Response::builder() 53 - .status(StatusCode::OK) 54 - .header(header::CONTENT_TYPE, &metadata.mime_type) 55 - .header(header::CONTENT_LENGTH, metadata.size_bytes.to_string()) 56 - .header("x-content-type-options", "nosniff") 57 - .header("content-security-policy", "default-src 'none'; sandbox") 58 - .body(Body::from(data)) 59 - .unwrap(), 60 - Err(e) => { 61 - error!("Failed to fetch blob from storage: {:?}", e); 62 - ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response() 63 - } 50 + Ok(Some(metadata)) => match state.blob_store.get(&metadata.storage_key).await { 51 + Ok(data) => Response::builder() 52 + .status(StatusCode::OK) 53 + .header(header::CONTENT_TYPE, &metadata.mime_type) 54 + .header(header::CONTENT_LENGTH, metadata.size_bytes.to_string()) 55 + .header("x-content-type-options", "nosniff") 56 + .header("content-security-policy", "default-src 'none'; sandbox") 57 + .body(Body::from(data)) 58 + .unwrap(), 59 + Err(e) => { 60 + error!("Failed to fetch blob from storage: {:?}", e); 61 + ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response() 64 62 } 65 - } 63 + }, 66 64 Ok(None) => ApiError::BlobNotFound(Some("Blob not found".into())).into_response(), 67 65 Err(e) => { 68 66 error!("DB error in get_blob: {:?}", e);
+10 -7
crates/tranquil-pds/src/sync/commit.rs
··· 102 102 Query(params): Query<ListReposParams>, 103 103 ) -> Response { 104 104 let limit = params.limit.unwrap_or(50).clamp(1, 1000); 105 - let cursor_did: Option<Did> = params 106 - .cursor 107 - .as_ref() 108 - .and_then(|s| s.parse().ok()); 105 + let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|s| s.parse().ok()); 109 106 let cursor_ref = cursor_did.as_ref(); 110 - let result = state.repo_repo.list_repos_paginated(cursor_ref, limit + 1).await; 107 + let result = state 108 + .repo_repo 109 + .list_repos_paginated(cursor_ref, limit + 1) 110 + .await; 111 111 match result { 112 112 Ok(rows) => { 113 113 let has_more = rows.len() as i64 > limit; ··· 196 196 let account = match get_account_with_status(state.repo_repo.as_ref(), &did).await { 197 197 Ok(Some(a)) => a, 198 198 Ok(None) => { 199 - return ApiError::RepoNotFound(Some(format!("Could not find repo for DID: {}", did_str))) 200 - .into_response(); 199 + return ApiError::RepoNotFound(Some(format!( 200 + "Could not find repo for DID: {}", 201 + did_str 202 + ))) 203 + .into_response(); 201 204 } 202 205 Err(e) => { 203 206 error!("DB error in get_repo_status: {:?}", e);
+11 -9
crates/tranquil-pds/src/sync/deprecated.rs
··· 2 2 use crate::state::AppState; 3 3 use crate::sync::car::encode_car_header; 4 4 use crate::sync::util::assert_repo_availability; 5 - use tranquil_types::Did; 6 5 use axum::{ 7 6 Json, 8 7 extract::{Query, State}, ··· 15 14 use serde::{Deserialize, Serialize}; 16 15 use std::io::Write; 17 16 use std::str::FromStr; 17 + use tranquil_types::Did; 18 18 19 19 const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; 20 20 ··· 69 69 Err(_) => return ApiError::InvalidRequest("invalid did".into()).into_response(), 70 70 }; 71 71 let is_admin_or_self = check_admin_or_self(&state, &headers, did_str).await; 72 - let account = match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await { 73 - Ok(a) => a, 74 - Err(e) => return e.into_response(), 75 - }; 72 + let account = 73 + match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await { 74 + Ok(a) => a, 75 + Err(e) => return e.into_response(), 76 + }; 76 77 match account.repo_root_cid { 77 78 Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(), 78 79 None => ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did_str))) ··· 99 100 Err(_) => return ApiError::InvalidRequest("invalid did".into()).into_response(), 100 101 }; 101 102 let is_admin_or_self = check_admin_or_self(&state, &headers, did_str).await; 102 - let account = match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await { 103 - Ok(a) => a, 104 - Err(e) => return e.into_response(), 105 - }; 103 + let account = 104 + match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await { 105 + Ok(a) => a, 106 + Err(e) => return e.into_response(), 107 + }; 106 108 let Some(head_str) = account.repo_root_cid else { 107 109 return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response(); 108 110 };
+1 -1
crates/tranquil-pds/src/sync/frame.rs
··· 186 186 } 187 187 188 188 fn placeholder_rev() -> String { 189 - use jacquard::types::{integer::LimitedU32, string::Tid}; 189 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 190 190 Tid::now(LimitedU32::MIN).to_string() 191 191 } 192 192
+6 -1
crates/tranquil-pds/src/sync/listener.rs
··· 76 76 }); 77 77 } 78 78 } 79 - let event = state.repo_repo.get_event_by_seq(seq_id).await.ok().flatten(); 79 + let event = state 80 + .repo_repo 81 + .get_event_by_seq(seq_id) 82 + .await 83 + .ok() 84 + .flatten(); 80 85 if let Some(event) = event { 81 86 let seq = event.seq; 82 87 let firehose_event = to_firehose_event(event);
+4 -1
crates/tranquil-pds/src/sync/repo.rs
··· 181 181 } 182 182 }; 183 183 184 - let block_cid_bytes = match state.repo_repo.get_user_block_cids_since_rev(user_id, since).await 184 + let block_cid_bytes = match state 185 + .repo_repo 186 + .get_user_block_cids_since_rev(user_id, since) 187 + .await 185 188 { 186 189 Ok(cids) => cids, 187 190 Err(e) => {
+7 -5
crates/tranquil-pds/src/sync/subscribe_repos.rs
··· 105 105 let _ = socket.send(Message::Binary(info_bytes.into())).await; 106 106 } 107 107 108 - let earliest = state.repo_repo.get_min_seq_since(backfill_time).await.ok().flatten(); 108 + let earliest = state 109 + .repo_repo 110 + .get_min_seq_since(backfill_time) 111 + .await 112 + .ok() 113 + .flatten(); 109 114 110 115 if let Some(earliest_seq) = earliest { 111 116 current_cursor = earliest_seq - 1; ··· 162 167 } 163 168 } 164 169 165 - let cutover_events = state 166 - .repo_repo 167 - .get_events_since_seq(last_seen, None) 168 - .await; 170 + let cutover_events = state.repo_repo.get_events_since_seq(last_seen, None).await; 169 171 170 172 if let Ok(events) = cutover_events 171 173 && !events.is_empty()
+8 -4
crates/tranquil-pds/src/sync/util.rs
··· 12 12 use jacquard_repo::commit::Commit; 13 13 use jacquard_repo::storage::BlockStore; 14 14 use serde::Serialize; 15 - use tranquil_db_traits::RepoRepository; 16 - use tranquil_types::Did; 17 15 use std::collections::{BTreeMap, HashMap}; 18 16 use std::io::Cursor; 19 17 use std::str::FromStr; 20 18 use tokio::io::AsyncWriteExt; 19 + use tranquil_db_traits::RepoRepository; 20 + use tranquil_types::Did; 21 21 22 22 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] 23 23 #[serde(rename_all = "lowercase")] ··· 333 333 frame.prev_data = Some(cid); 334 334 } 335 335 let commit_cid = frame.commit; 336 - let prev_cid = prev_cid_link.as_ref().and_then(|c| Cid::from_str(c.as_str()).ok()); 336 + let prev_cid = prev_cid_link 337 + .as_ref() 338 + .and_then(|c| Cid::from_str(c.as_str()).ok()); 337 339 let mut all_cids: Vec<Cid> = block_cids_str 338 340 .iter() 339 341 .filter_map(|s| Cid::from_str(s).ok()) ··· 473 475 frame.prev_data = Some(cid); 474 476 } 475 477 let commit_cid = frame.commit; 476 - let prev_cid = prev_cid_link.as_ref().and_then(|c| Cid::from_str(c.as_str()).ok()); 478 + let prev_cid = prev_cid_link 479 + .as_ref() 480 + .and_then(|c| Cid::from_str(c.as_str()).ok()); 477 481 let mut all_cids: Vec<Cid> = block_cids_str 478 482 .iter() 479 483 .filter_map(|s| Cid::from_str(s).ok())
+3 -3
crates/tranquil-pds/src/sync/verify.rs
··· 1 1 use bytes::Bytes; 2 2 use cid::Cid; 3 - use jacquard::common::IntoStatic; 4 - use jacquard::common::types::crypto::PublicKey; 5 - use jacquard::common::types::did_doc::DidDocument; 3 + use jacquard_common::IntoStatic; 4 + use jacquard_common::types::crypto::PublicKey; 5 + use jacquard_common::types::did_doc::DidDocument; 6 6 use jacquard_repo::commit::Commit; 7 7 use reqwest::Client; 8 8 use std::collections::HashMap;
+3
crates/tranquil-pds/src/test_new_file.rs
··· 1 + pub fn test_function() -> &'static str { 2 + "NEW_FILE_TEST_67890" 3 + }
+5 -4
crates/tranquil-pds/src/util.rs
··· 193 193 assert_eq!(parts[0].len(), 5); 194 194 assert_eq!(parts[1].len(), 5); 195 195 196 - assert!(code 197 - .chars() 198 - .filter(|&c| c != '-') 199 - .all(|c| BASE32_ALPHABET.contains(c))); 196 + assert!( 197 + code.chars() 198 + .filter(|&c| c != '-') 199 + .all(|c| BASE32_ALPHABET.contains(c)) 200 + ); 200 201 } 201 202 202 203 #[test]
+25 -43
crates/tranquil-pds/src/validation/mod.rs
··· 91 91 validate_datetime(created_at, "createdAt")?; 92 92 } 93 93 match record_type { 94 - "app.bsky.feed.post" => self.validate_post(obj)?, 95 - "app.bsky.actor.profile" => self.validate_profile(obj)?, 96 - "app.bsky.feed.like" => self.validate_like(obj)?, 97 - "app.bsky.feed.repost" => self.validate_repost(obj)?, 98 - "app.bsky.graph.follow" => self.validate_follow(obj)?, 99 - "app.bsky.graph.block" => self.validate_block(obj)?, 100 - "app.bsky.graph.list" => self.validate_list(obj)?, 101 - "app.bsky.graph.listitem" => self.validate_list_item(obj)?, 102 - "app.bsky.feed.generator" => self.validate_feed_generator(obj, rkey)?, 103 - "app.bsky.feed.threadgate" => self.validate_threadgate(obj)?, 104 - "app.bsky.labeler.service" => self.validate_labeler_service(obj)?, 105 - "app.bsky.graph.starterpack" => self.validate_starterpack(obj)?, 94 + "app.bsky.feed.post" => Self::validate_post(obj)?, 95 + "app.bsky.actor.profile" => Self::validate_profile(obj)?, 96 + "app.bsky.feed.like" => Self::validate_like(obj)?, 97 + "app.bsky.feed.repost" => Self::validate_repost(obj)?, 98 + "app.bsky.graph.follow" => Self::validate_follow(obj)?, 99 + "app.bsky.graph.block" => Self::validate_block(obj)?, 100 + "app.bsky.graph.list" => Self::validate_list(obj)?, 101 + "app.bsky.graph.listitem" => Self::validate_list_item(obj)?, 102 + "app.bsky.feed.generator" => Self::validate_feed_generator(obj, rkey)?, 103 + "app.bsky.feed.threadgate" => Self::validate_threadgate(obj)?, 104 + "app.bsky.labeler.service" => Self::validate_labeler_service(obj)?, 105 + "app.bsky.graph.starterpack" => Self::validate_starterpack(obj)?, 106 106 _ => { 107 107 if self.require_lexicon { 108 108 return Err(ValidationError::UnknownType(record_type.to_string())); ··· 113 113 Ok(ValidationStatus::Valid) 114 114 } 115 115 116 - fn validate_post(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 116 + fn validate_post(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 117 117 if !obj.contains_key("text") { 118 118 return Err(ValidationError::MissingField("text".to_string())); 119 119 } ··· 186 186 Ok(()) 187 187 } 188 188 189 - fn validate_profile( 190 - &self, 191 - obj: &serde_json::Map<String, Value>, 192 - ) -> Result<(), ValidationError> { 189 + fn validate_profile(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 193 190 if let Some(display_name) = obj.get("displayName").and_then(|v| v.as_str()) { 194 191 let grapheme_count = display_name.chars().count(); 195 192 if grapheme_count > 640 { ··· 227 224 Ok(()) 228 225 } 229 226 230 - fn validate_like(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 227 + fn validate_like(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 231 228 if !obj.contains_key("subject") { 232 229 return Err(ValidationError::MissingField("subject".to_string())); 233 230 } 234 231 if !obj.contains_key("createdAt") { 235 232 return Err(ValidationError::MissingField("createdAt".to_string())); 236 233 } 237 - self.validate_strong_ref(obj.get("subject"), "subject")?; 234 + Self::validate_strong_ref(obj.get("subject"), "subject")?; 238 235 Ok(()) 239 236 } 240 237 241 - fn validate_repost(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 238 + fn validate_repost(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 242 239 if !obj.contains_key("subject") { 243 240 return Err(ValidationError::MissingField("subject".to_string())); 244 241 } 245 242 if !obj.contains_key("createdAt") { 246 243 return Err(ValidationError::MissingField("createdAt".to_string())); 247 244 } 248 - self.validate_strong_ref(obj.get("subject"), "subject")?; 245 + Self::validate_strong_ref(obj.get("subject"), "subject")?; 249 246 Ok(()) 250 247 } 251 248 252 - fn validate_follow(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 249 + fn validate_follow(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 253 250 if !obj.contains_key("subject") { 254 251 return Err(ValidationError::MissingField("subject".to_string())); 255 252 } ··· 267 264 Ok(()) 268 265 } 269 266 270 - fn validate_block(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 267 + fn validate_block(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 271 268 if !obj.contains_key("subject") { 272 269 return Err(ValidationError::MissingField("subject".to_string())); 273 270 } ··· 285 282 Ok(()) 286 283 } 287 284 288 - fn validate_list(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 285 + fn validate_list(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 289 286 if !obj.contains_key("name") { 290 287 return Err(ValidationError::MissingField("name".to_string())); 291 288 } ··· 311 308 Ok(()) 312 309 } 313 310 314 - fn validate_list_item( 315 - &self, 316 - obj: &serde_json::Map<String, Value>, 317 - ) -> Result<(), ValidationError> { 311 + fn validate_list_item(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 318 312 if !obj.contains_key("subject") { 319 313 return Err(ValidationError::MissingField("subject".to_string())); 320 314 } ··· 328 322 } 329 323 330 324 fn validate_feed_generator( 331 - &self, 332 325 obj: &serde_json::Map<String, Value>, 333 326 rkey: Option<&str>, 334 327 ) -> Result<(), ValidationError> { ··· 364 357 Ok(()) 365 358 } 366 359 367 - fn validate_starterpack( 368 - &self, 369 - obj: &serde_json::Map<String, Value>, 370 - ) -> Result<(), ValidationError> { 360 + fn validate_starterpack(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 371 361 if !obj.contains_key("name") { 372 362 return Err(ValidationError::MissingField("name".to_string())); 373 363 } ··· 403 393 Ok(()) 404 394 } 405 395 406 - fn validate_threadgate( 407 - &self, 408 - obj: &serde_json::Map<String, Value>, 409 - ) -> Result<(), ValidationError> { 396 + fn validate_threadgate(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 410 397 if !obj.contains_key("post") { 411 398 return Err(ValidationError::MissingField("post".to_string())); 412 399 } ··· 417 404 } 418 405 419 406 fn validate_labeler_service( 420 - &self, 421 407 obj: &serde_json::Map<String, Value>, 422 408 ) -> Result<(), ValidationError> { 423 409 if !obj.contains_key("policies") { ··· 429 415 Ok(()) 430 416 } 431 417 432 - fn validate_strong_ref( 433 - &self, 434 - value: Option<&Value>, 435 - path: &str, 436 - ) -> Result<(), ValidationError> { 418 + fn validate_strong_ref(value: Option<&Value>, path: &str) -> Result<(), ValidationError> { 437 419 let obj = 438 420 value 439 421 .and_then(|v| v.as_object())
+10 -10
crates/tranquil-pds/tests/commit_signing.rs
··· 1 1 use cid::Cid; 2 - use jacquard::types::{integer::LimitedU32, string::Tid}; 2 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 3 3 use jacquard_repo::commit::Commit; 4 4 use k256::ecdsa::SigningKey; 5 5 use std::str::FromStr; ··· 14 14 Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap(); 15 15 let rev = Tid::now(LimitedU32::MIN); 16 16 17 - let did_typed = jacquard::types::string::Did::new(did).unwrap(); 17 + let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 18 18 let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None); 19 19 let signed = unsigned.sign(&signing_key).unwrap(); 20 20 21 21 let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true); 22 - let pubkey = jacquard::types::crypto::PublicKey { 23 - codec: jacquard::types::crypto::KeyCodec::Secp256k1, 22 + let pubkey = jacquard_common::types::crypto::PublicKey { 23 + codec: jacquard_common::types::crypto::KeyCodec::Secp256k1, 24 24 bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()), 25 25 }; 26 26 ··· 38 38 Cid::from_str("bafyreigxmvutyl3k5m4guzwxv3xf34gfxjlykgfdqkjmf32vwb5vcjxlui").unwrap(); 39 39 let rev = Tid::now(LimitedU32::MIN); 40 40 41 - let did_typed = jacquard::types::string::Did::new(did).unwrap(); 41 + let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 42 42 let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, Some(prev_cid)); 43 43 let signed = unsigned.sign(&signing_key).unwrap(); 44 44 45 45 let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true); 46 - let pubkey = jacquard::types::crypto::PublicKey { 47 - codec: jacquard::types::crypto::KeyCodec::Secp256k1, 46 + let pubkey = jacquard_common::types::crypto::PublicKey { 47 + codec: jacquard_common::types::crypto::KeyCodec::Secp256k1, 48 48 bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()), 49 49 }; 50 50 ··· 58 58 Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap(); 59 59 let rev = Tid::from_str("3masrxv55po22").unwrap(); 60 60 61 - let did_typed = jacquard::types::string::Did::new(did).unwrap(); 61 + let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 62 62 let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None); 63 63 64 64 let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap(); ··· 113 113 let commit = Commit::from_cbor(&signed_bytes).expect("should parse as valid commit"); 114 114 115 115 let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true); 116 - let pubkey = jacquard::types::crypto::PublicKey { 117 - codec: jacquard::types::crypto::KeyCodec::Secp256k1, 116 + let pubkey = jacquard_common::types::crypto::PublicKey { 117 + codec: jacquard_common::types::crypto::KeyCodec::Secp256k1, 118 118 bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()), 119 119 }; 120 120
+4 -16
crates/tranquil-pds/tests/did_web.rs
··· 149 149 "signingKey": signing_key 150 150 }); 151 151 let res = client 152 - .post(format!( 153 - "{}/xrpc/com.atproto.server.createAccount", 154 - base 155 - )) 152 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 156 153 .json(&payload) 157 154 .send() 158 155 .await ··· 431 428 "did": did 432 429 }); 433 430 let res = client 434 - .post(format!( 435 - "{}/xrpc/com.atproto.server.createAccount", 436 - base 437 - )) 431 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 438 432 .header("Authorization", format!("Bearer {}", service_jwt)) 439 433 .json(&payload) 440 434 .send() ··· 494 488 ); 495 489 496 490 let res = client 497 - .post(format!( 498 - "{}/xrpc/com.atproto.server.activateAccount", 499 - base 500 - )) 491 + .post(format!("{}/xrpc/com.atproto.server.activateAccount", base)) 501 492 .bearer_auth(&access_jwt) 502 493 .send() 503 494 .await ··· 525 516 ); 526 517 527 518 let res = client 528 - .post(format!( 529 - "{}/xrpc/com.atproto.repo.createRecord", 530 - base 531 - )) 519 + .post(format!("{}/xrpc/com.atproto.repo.createRecord", base)) 532 520 .bearer_auth(&access_jwt) 533 521 .json(&json!({ 534 522 "repo": did,
+3 -12
crates/tranquil-pds/tests/identity.rs
··· 51 51 let _base = base_url().await; 52 52 let params = [("handle", "nonexistent.handle.test")]; 53 53 let res = client 54 - .get(format!( 55 - "{}/xrpc/com.atproto.identity.resolveHandle", 56 - _base 57 - )) 54 + .get(format!("{}/xrpc/com.atproto.identity.resolveHandle", _base)) 58 55 .query(&params) 59 56 .send() 60 57 .await ··· 149 146 "signingKey": signing_key 150 147 }); 151 148 let res = client 152 - .post(format!( 153 - "{}/xrpc/com.atproto.server.createAccount", 154 - base 155 - )) 149 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 156 150 .json(&payload) 157 151 .send() 158 152 .await ··· 274 268 "signingKey": signing_key 275 269 }); 276 270 let res = client 277 - .post(format!( 278 - "{}/xrpc/com.atproto.server.createAccount", 279 - base 280 - )) 271 + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 281 272 .json(&create_payload) 282 273 .send() 283 274 .await
+2 -2
crates/tranquil-pds/tests/import_with_verification.rs
··· 2 2 use cid::Cid; 3 3 use common::*; 4 4 use ipld_core::ipld::Ipld; 5 - use jacquard::types::{integer::LimitedU32, string::Tid}; 5 + use jacquard_common::types::{integer::LimitedU32, string::Tid}; 6 6 use jacquard_repo::commit::Commit; 7 7 use k256::ecdsa::SigningKey; 8 8 use reqwest::StatusCode; ··· 91 91 92 92 fn create_signed_commit(did: &str, data_cid: &Cid, signing_key: &SigningKey) -> (Vec<u8>, Cid) { 93 93 let rev = Tid::now(LimitedU32::MIN); 94 - let did = jacquard::types::string::Did::new(did).expect("valid DID"); 94 + let did = jacquard_common::types::string::Did::new(did).expect("valid DID"); 95 95 let unsigned = Commit::new_unsigned(did, *data_cid, rev, None); 96 96 let signed = unsigned.sign(signing_key).expect("signing failed"); 97 97 let signed_bytes = signed.to_cbor().expect("serialization failed");
+2 -2
crates/tranquil-pds/tests/verify_live_commit.rs
··· 43 43 .expect("Failed to fetch DID doc"); 44 44 let did_doc_text = resp.text().await.expect("Failed to read body"); 45 45 println!("DID doc: {}", did_doc_text); 46 - let did_doc: jacquard::common::types::did_doc::DidDocument<'_> = 46 + let did_doc: jacquard_common::types::did_doc::DidDocument<'_> = 47 47 serde_json::from_str(&did_doc_text).expect("Failed to parse DID doc"); 48 48 let pubkey = did_doc 49 49 .atproto_public_key() ··· 68 68 did: &'a str, 69 69 version: i64, 70 70 data: &'a cid::Cid, 71 - rev: &'a jacquard::types::string::Tid, 71 + rev: &'a jacquard_common::types::string::Tid, 72 72 prev: Option<&'a cid::Cid>, 73 73 #[serde(with = "serde_bytes")] 74 74 sig: &'a [u8],
-2
crates/tranquil-repo/Cargo.toml
··· 5 5 license.workspace = true 6 6 7 7 [dependencies] 8 - tranquil-types = { workspace = true } 9 - 10 8 bytes = { workspace = true } 11 9 cid = { workspace = true } 12 10 jacquard-repo = { workspace = true }
+5 -4
crates/tranquil-scopes/src/definitions.rs
··· 33 33 pub display_name: &'static str, 34 34 } 35 35 36 - pub static SCOPE_DEFINITIONS: LazyLock<HashMap<&'static str, ScopeDefinition>> = 37 - LazyLock::new(|| { 36 + pub static SCOPE_DEFINITIONS: LazyLock<HashMap<&'static str, ScopeDefinition>> = LazyLock::new( 37 + || { 38 38 let definitions = vec![ 39 39 ScopeDefinition { 40 40 scope: "atproto", 41 41 category: ScopeCategory::Core, 42 42 required: true, 43 - description: "Full access to read, write, and manage this account", 43 + description: "Full access to read, write, and manage this account (when no granular permissions are specified)", 44 44 display_name: "Full Account Access", 45 45 }, 46 46 ScopeDefinition { ··· 109 109 ]; 110 110 111 111 definitions.into_iter().map(|d| (d.scope, d)).collect() 112 - }); 112 + }, 113 + ); 113 114 114 115 #[allow(dead_code)] 115 116 pub fn get_scope_definition(scope: &str) -> Option<&'static ScopeDefinition> {
+5 -5
crates/tranquil-scopes/src/parser.rs
··· 57 57 } 58 58 self.accept.iter().any(|pattern| { 59 59 pattern == mime 60 - || pattern 61 - .strip_suffix("/*") 62 - .is_some_and(|prefix| { 63 - mime.starts_with(prefix) && mime.chars().nth(prefix.len()) == Some('/') 64 - }) 60 + || pattern.strip_suffix("/*").is_some_and(|prefix| { 61 + mime.starts_with(prefix) && mime.chars().nth(prefix.len()) == Some('/') 62 + }) 65 63 }) 66 64 } 67 65 } ··· 84 82 Handle, 85 83 Repo, 86 84 Status, 85 + Wildcard, 87 86 } 88 87 89 88 #[derive(Debug, Clone, PartialEq, Eq)] ··· 104 103 "handle" => Some(Self::Handle), 105 104 "repo" => Some(Self::Repo), 106 105 "status" => Some(Self::Status), 106 + "*" => Some(Self::Wildcard), 107 107 _ => None, 108 108 } 109 109 }
+57 -21
crates/tranquil-scopes/src/permissions.rs
··· 9 9 pub struct ScopePermissions { 10 10 scopes: HashSet<String>, 11 11 parsed: Vec<ParsedScope>, 12 - has_atproto: bool, 13 12 has_transition_generic: bool, 14 13 has_transition_chat: bool, 15 14 has_transition_email: bool, ··· 26 25 let parsed = parse_scope_string(scope_str); 27 26 28 27 let has_atproto = parsed.iter().any(|p| matches!(p, ParsedScope::Atproto)); 29 - let has_transition_generic = parsed 28 + let mut has_transition_generic = parsed 30 29 .iter() 31 30 .any(|p| matches!(p, ParsedScope::TransitionGeneric)); 32 31 let has_transition_chat = parsed ··· 36 35 .iter() 37 36 .any(|p| matches!(p, ParsedScope::TransitionEmail)); 38 37 38 + let has_granular_scopes = parsed.iter().any(|p| { 39 + matches!( 40 + p, 41 + ParsedScope::Repo(_) 42 + | ParsedScope::Blob(_) 43 + | ParsedScope::Rpc(_) 44 + | ParsedScope::Account(_) 45 + | ParsedScope::Identity(_) 46 + ) 47 + }); 48 + 49 + if has_atproto && !has_granular_scopes { 50 + has_transition_generic = true; 51 + } 52 + 39 53 Self { 40 54 scopes, 41 55 parsed, 42 - has_atproto, 43 56 has_transition_generic, 44 57 has_transition_chat, 45 58 has_transition_email, ··· 55 68 } 56 69 57 70 pub fn has_full_access(&self) -> bool { 58 - self.has_atproto 71 + self.has_transition_generic 59 72 } 60 73 61 74 fn find_repo_scopes(&self) -> impl Iterator<Item = &RepoScope> { ··· 109 122 } 110 123 111 124 pub fn assert_repo(&self, action: RepoAction, collection: &str) -> Result<(), ScopeError> { 112 - if self.has_atproto || self.has_transition_generic { 125 + if self.has_transition_generic { 113 126 return Ok(()); 114 127 } 115 128 ··· 142 155 } 143 156 144 157 pub fn assert_blob(&self, mime: &str) -> Result<(), ScopeError> { 145 - if self.has_atproto || self.has_transition_generic { 158 + if self.has_transition_generic { 146 159 return Ok(()); 147 160 } 148 161 149 - if self.find_blob_scopes().any(|blob_scope| blob_scope.matches_mime(mime)) { 162 + if self 163 + .find_blob_scopes() 164 + .any(|blob_scope| blob_scope.matches_mime(mime)) 165 + { 150 166 Ok(()) 151 167 } else { 152 168 Err(ScopeError::InsufficientScope { ··· 157 173 } 158 174 159 175 pub fn assert_rpc(&self, aud: &str, lxm: &str) -> Result<(), ScopeError> { 160 - if self.has_atproto || self.has_transition_generic { 176 + if self.has_transition_generic { 161 177 return Ok(()); 162 178 } 163 179 ··· 200 216 attr: AccountAttr, 201 217 action: AccountAction, 202 218 ) -> Result<(), ScopeError> { 203 - if self.has_atproto || self.has_transition_generic { 219 + if self.has_transition_generic { 204 220 return Ok(()); 205 221 } 206 222 ··· 210 226 } 211 227 212 228 let has_permission = self.find_account_scopes().any(|account_scope| { 213 - account_scope.attr == attr 214 - && (account_scope.action == action 215 - || account_scope.action == AccountAction::Manage) 229 + (account_scope.attr == attr || account_scope.attr == AccountAttr::Wildcard) 230 + && (account_scope.action == action || account_scope.action == AccountAction::Manage) 216 231 }); 217 232 218 233 if has_permission { ··· 234 249 } 235 250 236 251 pub fn allows_email_read(&self) -> bool { 237 - self.has_atproto 238 - || self.has_transition_generic 252 + self.has_transition_generic 239 253 || self.has_transition_email 240 254 || self 241 255 .find_account_scopes() 242 - .any(|a| a.attr == AccountAttr::Email) 256 + .any(|a| a.attr == AccountAttr::Email || a.attr == AccountAttr::Wildcard) 243 257 } 244 258 245 259 pub fn allows_repo(&self, action: RepoAction, collection: &str) -> bool { ··· 259 273 } 260 274 261 275 pub fn assert_identity(&self, attr: IdentityAttr) -> Result<(), ScopeError> { 262 - if self.has_atproto || self.has_transition_generic { 276 + if self.has_transition_generic { 263 277 return Ok(()); 264 278 } 265 279 266 - let has_permission = self 267 - .find_identity_scopes() 268 - .any(|identity_scope| { 269 - identity_scope.attr == IdentityAttr::Wildcard || identity_scope.attr == attr 270 - }); 280 + let has_permission = self.find_identity_scopes().any(|identity_scope| { 281 + identity_scope.attr == IdentityAttr::Wildcard || identity_scope.attr == attr 282 + }); 271 283 272 284 if has_permission { 273 285 Ok(()) ··· 301 313 AccountAttr::Handle => "handle", 302 314 AccountAttr::Repo => "repo", 303 315 AccountAttr::Status => "status", 316 + AccountAttr::Wildcard => "*", 304 317 } 305 318 } 306 319 ··· 484 497 let perms = ScopePermissions::from_scope_string(Some("account:status?action=read")); 485 498 assert!(perms.allows_account(AccountAttr::Status, AccountAction::Read)); 486 499 assert!(!perms.allows_account(AccountAttr::Status, AccountAction::Manage)); 500 + } 501 + 502 + #[test] 503 + fn test_atproto_with_granular_scopes_uses_granular() { 504 + let perms = 505 + ScopePermissions::from_scope_string(Some("atproto repo:*?action=create blob:*/*")); 506 + assert!(!perms.has_full_access()); 507 + assert!(perms.allows_repo(RepoAction::Create, "any.collection")); 508 + assert!(!perms.allows_repo(RepoAction::Delete, "any.collection")); 509 + assert!(!perms.allows_repo(RepoAction::Update, "any.collection")); 510 + assert!(perms.allows_blob("image/png")); 511 + assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 512 + } 513 + 514 + #[test] 515 + fn test_atproto_alone_has_full_access() { 516 + let perms = ScopePermissions::from_scope_string(Some("atproto")); 517 + assert!(perms.has_full_access()); 518 + assert!(perms.allows_repo(RepoAction::Create, "any.collection")); 519 + assert!(perms.allows_repo(RepoAction::Delete, "any.collection")); 520 + assert!(perms.allows_repo(RepoAction::Update, "any.collection")); 521 + assert!(perms.allows_blob("image/png")); 522 + assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 487 523 } 488 524 }
-2
crates/tranquil-storage/Cargo.toml
··· 13 13 bytes = { workspace = true } 14 14 futures = { workspace = true } 15 15 sha2 = { workspace = true } 16 - thiserror = { workspace = true } 17 - tracing = { workspace = true }
+1 -1
crates/tranquil-types/Cargo.toml
··· 7 7 [dependencies] 8 8 chrono = { workspace = true } 9 9 cid = { workspace = true } 10 - jacquard = { workspace = true } 10 + jacquard-common = { workspace = true } 11 11 serde = { workspace = true } 12 12 serde_json = { workspace = true } 13 13 sqlx = { workspace = true }
+299 -1553
crates/tranquil-types/src/lib.rs
··· 4 4 use std::ops::Deref; 5 5 use std::str::FromStr; 6 6 7 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] 8 - #[serde(transparent)] 9 - #[sqlx(transparent)] 10 - pub struct Did(String); 7 + macro_rules! impl_string_common { 8 + ($name:ident) => { 9 + impl $name { 10 + pub fn as_str(&self) -> &str { 11 + &self.0 12 + } 11 13 12 - impl<'de> Deserialize<'de> for Did { 13 - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 14 - where 15 - D: serde::Deserializer<'de>, 16 - { 17 - let s = String::deserialize(deserializer)?; 18 - Did::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) 19 - } 20 - } 21 - 22 - impl From<Did> for String { 23 - fn from(did: Did) -> Self { 24 - did.0 25 - } 26 - } 14 + pub fn into_inner(self) -> String { 15 + self.0 16 + } 17 + } 27 18 28 - impl From<String> for Did { 29 - fn from(s: String) -> Self { 30 - Did(s) 31 - } 32 - } 19 + impl AsRef<str> for $name { 20 + fn as_ref(&self) -> &str { 21 + &self.0 22 + } 23 + } 33 24 34 - impl<'a> From<&'a Did> for Cow<'a, str> { 35 - fn from(did: &'a Did) -> Self { 36 - Cow::Borrowed(&did.0) 37 - } 38 - } 25 + impl Deref for $name { 26 + type Target = str; 39 27 40 - impl Did { 41 - pub fn new(s: impl Into<String>) -> Result<Self, DidError> { 42 - let s = s.into(); 43 - jacquard::types::string::Did::new(&s).map_err(|_| DidError::Invalid(s.clone()))?; 44 - Ok(Self(s)) 45 - } 28 + fn deref(&self) -> &Self::Target { 29 + &self.0 30 + } 31 + } 46 32 47 - pub fn new_unchecked(s: impl Into<String>) -> Self { 48 - Self(s.into()) 49 - } 33 + impl From<$name> for String { 34 + fn from(val: $name) -> Self { 35 + val.0 36 + } 37 + } 50 38 51 - pub fn as_str(&self) -> &str { 52 - &self.0 53 - } 39 + impl From<String> for $name { 40 + fn from(s: String) -> Self { 41 + Self(s) 42 + } 43 + } 54 44 55 - pub fn into_inner(self) -> String { 56 - self.0 57 - } 45 + impl<'a> From<&'a $name> for Cow<'a, str> { 46 + fn from(val: &'a $name) -> Self { 47 + Cow::Borrowed(&val.0) 48 + } 49 + } 58 50 59 - pub fn is_plc(&self) -> bool { 60 - self.0.starts_with("did:plc:") 61 - } 51 + impl PartialEq<str> for $name { 52 + fn eq(&self, other: &str) -> bool { 53 + self.0 == other 54 + } 55 + } 62 56 63 - pub fn is_web(&self) -> bool { 64 - self.0.starts_with("did:web:") 65 - } 66 - } 57 + impl PartialEq<&str> for $name { 58 + fn eq(&self, other: &&str) -> bool { 59 + self.0 == *other 60 + } 61 + } 67 62 68 - impl AsRef<str> for Did { 69 - fn as_ref(&self) -> &str { 70 - &self.0 71 - } 72 - } 63 + impl PartialEq<String> for $name { 64 + fn eq(&self, other: &String) -> bool { 65 + self.0 == *other 66 + } 67 + } 73 68 74 - impl PartialEq<str> for Did { 75 - fn eq(&self, other: &str) -> bool { 76 - self.0 == other 77 - } 78 - } 69 + impl PartialEq<$name> for String { 70 + fn eq(&self, other: &$name) -> bool { 71 + *self == other.0 72 + } 73 + } 79 74 80 - impl PartialEq<&str> for Did { 81 - fn eq(&self, other: &&str) -> bool { 82 - self.0 == *other 83 - } 84 - } 75 + impl PartialEq<$name> for &str { 76 + fn eq(&self, other: &$name) -> bool { 77 + *self == other.0 78 + } 79 + } 85 80 86 - impl PartialEq<String> for Did { 87 - fn eq(&self, other: &String) -> bool { 88 - self.0 == *other 89 - } 81 + impl fmt::Display for $name { 82 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 + write!(f, "{}", self.0) 84 + } 85 + } 86 + }; 90 87 } 91 88 92 - impl PartialEq<Did> for String { 93 - fn eq(&self, other: &Did) -> bool { 94 - *self == other.0 95 - } 96 - } 89 + macro_rules! simple_string_newtype { 90 + ( 91 + $(#[$meta:meta])* 92 + $vis:vis struct $name:ident; 93 + ) => { 94 + $(#[$meta])* 95 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 96 + #[serde(transparent)] 97 + #[sqlx(transparent)] 98 + $vis struct $name(String); 97 99 98 - impl PartialEq<Did> for &str { 99 - fn eq(&self, other: &Did) -> bool { 100 - *self == other.0 101 - } 102 - } 100 + impl $name { 101 + pub fn new(s: impl Into<String>) -> Self { 102 + Self(s.into()) 103 + } 103 104 104 - impl Deref for Did { 105 - type Target = str; 105 + pub fn new_unchecked(s: impl Into<String>) -> Self { 106 + Self(s.into()) 107 + } 108 + } 106 109 107 - fn deref(&self) -> &Self::Target { 108 - &self.0 109 - } 110 + impl_string_common!($name); 111 + }; 110 112 } 111 113 112 - impl fmt::Display for Did { 113 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 - write!(f, "{}", self.0) 115 - } 116 - } 114 + macro_rules! simple_string_newtype_no_sqlx { 115 + ( 116 + $(#[$meta:meta])* 117 + $vis:vis struct $name:ident; 118 + ) => { 119 + $(#[$meta])* 120 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 121 + #[serde(transparent)] 122 + $vis struct $name(String); 117 123 118 - impl FromStr for Did { 119 - type Err = DidError; 124 + impl $name { 125 + pub fn new(s: impl Into<String>) -> Self { 126 + Self(s.into()) 127 + } 120 128 121 - fn from_str(s: &str) -> Result<Self, Self::Err> { 122 - Self::new(s) 123 - } 124 - } 129 + pub fn new_unchecked(s: impl Into<String>) -> Self { 130 + Self(s.into()) 131 + } 132 + } 125 133 126 - #[derive(Debug, Clone, thiserror::Error)] 127 - pub enum DidError { 128 - #[error("invalid DID: {0}")] 129 - Invalid(String), 134 + impl_string_common!($name); 135 + }; 130 136 } 131 137 132 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 133 - #[serde(transparent)] 134 - #[sqlx(transparent)] 135 - pub struct Handle(String); 138 + macro_rules! validated_string_newtype { 139 + ( 140 + $(#[$meta:meta])* 141 + $vis:vis struct $name:ident; 142 + error = $error:ident; 143 + validator = $validator:expr; 144 + ) => { 145 + $(#[$meta])* 146 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] 147 + #[serde(transparent)] 148 + #[sqlx(transparent)] 149 + $vis struct $name(String); 136 150 137 - impl From<Handle> for String { 138 - fn from(handle: Handle) -> Self { 139 - handle.0 140 - } 141 - } 151 + impl<'de> Deserialize<'de> for $name { 152 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 153 + where 154 + D: serde::Deserializer<'de>, 155 + { 156 + let s = String::deserialize(deserializer)?; 157 + $name::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) 158 + } 159 + } 142 160 143 - impl From<String> for Handle { 144 - fn from(s: String) -> Self { 145 - Handle(s) 146 - } 147 - } 148 - 149 - impl<'a> From<&'a Handle> for Cow<'a, str> { 150 - fn from(handle: &'a Handle) -> Self { 151 - Cow::Borrowed(&handle.0) 152 - } 153 - } 161 + impl $name { 162 + pub fn new(s: impl Into<String>) -> Result<Self, $error> { 163 + let s = s.into(); 164 + let validator: fn(&str) -> Result<(), ()> = $validator; 165 + validator(&s).map_err(|_| $error::Invalid(s.clone()))?; 166 + Ok(Self(s)) 167 + } 154 168 155 - impl Handle { 156 - pub fn new(s: impl Into<String>) -> Result<Self, HandleError> { 157 - let s = s.into(); 158 - jacquard::types::string::Handle::new(&s).map_err(|_| HandleError::Invalid(s.clone()))?; 159 - Ok(Self(s)) 160 - } 169 + pub fn new_unchecked(s: impl Into<String>) -> Self { 170 + Self(s.into()) 171 + } 172 + } 161 173 162 - pub fn new_unchecked(s: impl Into<String>) -> Self { 163 - Self(s.into()) 164 - } 174 + impl FromStr for $name { 175 + type Err = $error; 165 176 166 - pub fn as_str(&self) -> &str { 167 - &self.0 168 - } 177 + fn from_str(s: &str) -> Result<Self, Self::Err> { 178 + Self::new(s) 179 + } 180 + } 169 181 170 - pub fn into_inner(self) -> String { 171 - self.0 172 - } 173 - } 182 + impl_string_common!($name); 174 183 175 - impl AsRef<str> for Handle { 176 - fn as_ref(&self) -> &str { 177 - &self.0 178 - } 184 + #[derive(Debug, Clone, thiserror::Error)] 185 + pub enum $error { 186 + #[error("invalid: {0}")] 187 + Invalid(String), 188 + } 189 + }; 179 190 } 180 191 181 - impl Deref for Handle { 182 - type Target = str; 183 - 184 - fn deref(&self) -> &Self::Target { 185 - &self.0 186 - } 192 + validated_string_newtype! { 193 + pub struct Did; 194 + error = DidError; 195 + validator = |s| jacquard_common::types::string::Did::new(s).map(|_| ()).map_err(|_| ()); 187 196 } 188 197 189 - impl PartialEq<str> for Handle { 190 - fn eq(&self, other: &str) -> bool { 191 - self.0 == other 198 + impl Did { 199 + pub fn is_plc(&self) -> bool { 200 + self.0.starts_with("did:plc:") 192 201 } 193 - } 194 202 195 - impl PartialEq<&str> for Handle { 196 - fn eq(&self, other: &&str) -> bool { 197 - self.0 == *other 203 + pub fn is_web(&self) -> bool { 204 + self.0.starts_with("did:web:") 198 205 } 199 206 } 200 207 201 - impl PartialEq<String> for Handle { 202 - fn eq(&self, other: &String) -> bool { 203 - self.0 == *other 204 - } 205 - } 208 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] 209 + #[serde(transparent)] 210 + #[sqlx(transparent)] 211 + pub struct Handle(String); 206 212 207 - impl PartialEq<Handle> for String { 208 - fn eq(&self, other: &Handle) -> bool { 209 - *self == other.0 213 + impl<'de> Deserialize<'de> for Handle { 214 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 215 + where 216 + D: serde::Deserializer<'de>, 217 + { 218 + let s = String::deserialize(deserializer)?; 219 + Ok(Handle(s)) 210 220 } 211 221 } 212 222 213 - impl PartialEq<Handle> for &str { 214 - fn eq(&self, other: &Handle) -> bool { 215 - *self == other.0 223 + impl Handle { 224 + pub fn new(s: impl Into<String>) -> Result<Self, HandleError> { 225 + let s = s.into(); 226 + jacquard_common::types::string::Handle::new(&s) 227 + .map_err(|_| HandleError::Invalid(s.clone()))?; 228 + Ok(Self(s)) 216 229 } 217 - } 218 230 219 - impl fmt::Display for Handle { 220 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 221 - write!(f, "{}", self.0) 231 + pub fn new_unchecked(s: impl Into<String>) -> Self { 232 + Self(s.into()) 222 233 } 223 234 } 224 235 ··· 230 241 } 231 242 } 232 243 244 + impl_string_common!(Handle); 245 + 233 246 #[derive(Debug, Clone, thiserror::Error)] 234 247 pub enum HandleError { 235 248 #[error("invalid handle: {0}")] ··· 325 338 } 326 339 } 327 340 328 - impl FromStr for AtIdentifier { 329 - type Err = AtIdentifierError; 330 - 331 - fn from_str(s: &str) -> Result<Self, Self::Err> { 332 - Self::new(s) 333 - } 334 - } 335 - 336 341 impl Serialize for AtIdentifier { 337 342 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 338 343 where ··· 358 363 Invalid(String), 359 364 } 360 365 361 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 362 - #[serde(transparent)] 363 - #[sqlx(type_name = "rkey")] 364 - pub struct Rkey(String); 365 - 366 - impl From<Rkey> for String { 367 - fn from(rkey: Rkey) -> Self { 368 - rkey.0 369 - } 370 - } 371 - 372 - impl From<String> for Rkey { 373 - fn from(s: String) -> Self { 374 - Rkey(s) 375 - } 376 - } 377 - 378 - impl<'a> From<&'a Rkey> for Cow<'a, str> { 379 - fn from(rkey: &'a Rkey) -> Self { 380 - Cow::Borrowed(&rkey.0) 381 - } 366 + validated_string_newtype! { 367 + pub struct Rkey; 368 + error = RkeyError; 369 + validator = |s| jacquard_common::types::string::Rkey::new(s).map(|_| ()).map_err(|_| ()); 382 370 } 383 371 384 372 impl Rkey { 385 - pub fn new(s: impl Into<String>) -> Result<Self, RkeyError> { 386 - let s = s.into(); 387 - jacquard::types::string::Rkey::new(&s).map_err(|_| RkeyError::Invalid(s.clone()))?; 388 - Ok(Self(s)) 389 - } 390 - 391 - pub fn new_unchecked(s: impl Into<String>) -> Self { 392 - Self(s.into()) 393 - } 394 - 395 373 pub fn generate() -> Self { 396 - use jacquard::types::integer::LimitedU32; 397 - Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) 398 - } 399 - 400 - pub fn as_str(&self) -> &str { 401 - &self.0 402 - } 403 - 404 - pub fn into_inner(self) -> String { 405 - self.0 374 + use jacquard_common::types::integer::LimitedU32; 375 + Self(jacquard_common::types::string::Tid::now(LimitedU32::MIN).to_string()) 406 376 } 407 377 408 378 pub fn is_tid(&self) -> bool { 409 - jacquard::types::string::Tid::from_str(&self.0).is_ok() 410 - } 411 - } 412 - 413 - impl AsRef<str> for Rkey { 414 - fn as_ref(&self) -> &str { 415 - &self.0 416 - } 417 - } 418 - 419 - impl Deref for Rkey { 420 - type Target = str; 421 - 422 - fn deref(&self) -> &Self::Target { 423 - &self.0 424 - } 425 - } 426 - 427 - impl PartialEq<str> for Rkey { 428 - fn eq(&self, other: &str) -> bool { 429 - self.0 == other 430 - } 431 - } 432 - 433 - impl PartialEq<&str> for Rkey { 434 - fn eq(&self, other: &&str) -> bool { 435 - self.0 == *other 436 - } 437 - } 438 - 439 - impl PartialEq<String> for Rkey { 440 - fn eq(&self, other: &String) -> bool { 441 - self.0 == *other 442 - } 443 - } 444 - 445 - impl PartialEq<Rkey> for String { 446 - fn eq(&self, other: &Rkey) -> bool { 447 - *self == other.0 448 - } 449 - } 450 - 451 - impl PartialEq<Rkey> for &str { 452 - fn eq(&self, other: &Rkey) -> bool { 453 - *self == other.0 454 - } 455 - } 456 - 457 - impl fmt::Display for Rkey { 458 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 459 - write!(f, "{}", self.0) 460 - } 461 - } 462 - 463 - impl FromStr for Rkey { 464 - type Err = RkeyError; 465 - 466 - fn from_str(s: &str) -> Result<Self, Self::Err> { 467 - Self::new(s) 379 + Tid::new(&self.0).is_ok() 468 380 } 469 - } 470 381 471 - #[derive(Debug, Clone, thiserror::Error)] 472 - pub enum RkeyError { 473 - #[error("invalid rkey: {0}")] 474 - Invalid(String), 475 - } 476 - 477 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 478 - #[serde(transparent)] 479 - #[sqlx(type_name = "nsid")] 480 - pub struct Nsid(String); 481 - 482 - impl From<Nsid> for String { 483 - fn from(nsid: Nsid) -> Self { 484 - nsid.0 382 + pub fn to_tid(&self) -> Option<Tid> { 383 + Tid::new(&self.0).ok() 485 384 } 486 385 } 487 386 488 - impl From<String> for Nsid { 489 - fn from(s: String) -> Self { 490 - Nsid(s) 491 - } 492 - } 493 - 494 - impl<'a> From<&'a Nsid> for Cow<'a, str> { 495 - fn from(nsid: &'a Nsid) -> Self { 496 - Cow::Borrowed(&nsid.0) 497 - } 387 + validated_string_newtype! { 388 + pub struct Nsid; 389 + error = NsidError; 390 + validator = |s| jacquard_common::types::string::Nsid::new(s).map(|_| ()).map_err(|_| ()); 498 391 } 499 392 500 393 impl Nsid { 501 - pub fn new(s: impl Into<String>) -> Result<Self, NsidError> { 502 - let s = s.into(); 503 - jacquard::types::string::Nsid::new(&s).map_err(|_| NsidError::Invalid(s.clone()))?; 504 - Ok(Self(s)) 505 - } 506 - 507 - pub fn new_unchecked(s: impl Into<String>) -> Self { 508 - Self(s.into()) 509 - } 510 - 511 - pub fn as_str(&self) -> &str { 512 - &self.0 513 - } 514 - 515 - pub fn into_inner(self) -> String { 516 - self.0 517 - } 518 - 519 - pub fn authority(&self) -> Option<&str> { 520 - let parts: Vec<&str> = self.0.rsplitn(2, '.').collect(); 521 - if parts.len() == 2 { 522 - Some(parts[1]) 523 - } else { 524 - None 525 - } 526 - } 527 - 528 - pub fn name(&self) -> Option<&str> { 529 - self.0.rsplit('.').next() 530 - } 531 - } 532 - 533 - impl AsRef<str> for Nsid { 534 - fn as_ref(&self) -> &str { 535 - &self.0 536 - } 537 - } 538 - 539 - impl Deref for Nsid { 540 - type Target = str; 541 - 542 - fn deref(&self) -> &Self::Target { 543 - &self.0 544 - } 545 - } 546 - 547 - impl PartialEq<str> for Nsid { 548 - fn eq(&self, other: &str) -> bool { 549 - self.0 == other 550 - } 551 - } 552 - 553 - impl PartialEq<&str> for Nsid { 554 - fn eq(&self, other: &&str) -> bool { 555 - self.0 == *other 556 - } 557 - } 558 - 559 - impl PartialEq<String> for Nsid { 560 - fn eq(&self, other: &String) -> bool { 561 - self.0 == *other 562 - } 563 - } 564 - 565 - impl PartialEq<Nsid> for String { 566 - fn eq(&self, other: &Nsid) -> bool { 567 - *self == other.0 568 - } 569 - } 570 - 571 - impl PartialEq<Nsid> for &str { 572 - fn eq(&self, other: &Nsid) -> bool { 573 - *self == other.0 574 - } 575 - } 576 - 577 - impl fmt::Display for Nsid { 578 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 579 - write!(f, "{}", self.0) 580 - } 581 - } 582 - 583 - impl FromStr for Nsid { 584 - type Err = NsidError; 585 - 586 - fn from_str(s: &str) -> Result<Self, Self::Err> { 587 - Self::new(s) 394 + pub fn authority(&self) -> &str { 395 + self.0.split('.').rev().nth(1).unwrap_or("") 588 396 } 589 - } 590 397 591 - #[derive(Debug, Clone, thiserror::Error)] 592 - pub enum NsidError { 593 - #[error("invalid NSID: {0}")] 594 - Invalid(String), 595 - } 596 - 597 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 598 - #[serde(transparent)] 599 - #[sqlx(type_name = "at_uri")] 600 - pub struct AtUri(String); 601 - 602 - impl From<AtUri> for String { 603 - fn from(uri: AtUri) -> Self { 604 - uri.0 398 + pub fn name(&self) -> &str { 399 + self.0.split('.').next_back().unwrap_or("") 605 400 } 606 401 } 607 402 608 - impl From<String> for AtUri { 609 - fn from(s: String) -> Self { 610 - AtUri(s) 611 - } 612 - } 613 - 614 - impl<'a> From<&'a AtUri> for Cow<'a, str> { 615 - fn from(uri: &'a AtUri) -> Self { 616 - Cow::Borrowed(&uri.0) 617 - } 403 + validated_string_newtype! { 404 + pub struct AtUri; 405 + error = AtUriError; 406 + validator = |s| jacquard_common::types::string::AtUri::new(s).map(|_| ()).map_err(|_| ()); 618 407 } 619 408 620 409 impl AtUri { 621 - pub fn new(s: impl Into<String>) -> Result<Self, AtUriError> { 622 - let s = s.into(); 623 - jacquard::types::string::AtUri::new(&s).map_err(|_| AtUriError::Invalid(s.clone()))?; 624 - Ok(Self(s)) 625 - } 626 - 627 - pub fn new_unchecked(s: impl Into<String>) -> Self { 628 - Self(s.into()) 629 - } 630 - 631 410 pub fn from_parts(did: &str, collection: &str, rkey: &str) -> Self { 632 411 Self(format!("at://{}/{}/{}", did, collection, rkey)) 633 - } 634 - 635 - pub fn as_str(&self) -> &str { 636 - &self.0 637 - } 638 - 639 - pub fn into_inner(self) -> String { 640 - self.0 641 412 } 642 413 643 414 pub fn did(&self) -> Option<&str> { ··· 659 430 } 660 431 } 661 432 662 - impl AsRef<str> for AtUri { 663 - fn as_ref(&self) -> &str { 664 - &self.0 665 - } 666 - } 667 - 668 - impl Deref for AtUri { 669 - type Target = str; 670 - 671 - fn deref(&self) -> &Self::Target { 672 - &self.0 673 - } 674 - } 675 - 676 - impl PartialEq<str> for AtUri { 677 - fn eq(&self, other: &str) -> bool { 678 - self.0 == other 679 - } 680 - } 681 - 682 - impl PartialEq<&str> for AtUri { 683 - fn eq(&self, other: &&str) -> bool { 684 - self.0 == *other 685 - } 686 - } 687 - 688 - impl PartialEq<String> for AtUri { 689 - fn eq(&self, other: &String) -> bool { 690 - self.0 == *other 691 - } 692 - } 693 - 694 - impl PartialEq<AtUri> for String { 695 - fn eq(&self, other: &AtUri) -> bool { 696 - *self == other.0 697 - } 698 - } 699 - 700 - impl PartialEq<AtUri> for &str { 701 - fn eq(&self, other: &AtUri) -> bool { 702 - *self == other.0 703 - } 704 - } 705 - 706 - impl fmt::Display for AtUri { 707 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 708 - write!(f, "{}", self.0) 709 - } 710 - } 711 - 712 - impl FromStr for AtUri { 713 - type Err = AtUriError; 714 - 715 - fn from_str(s: &str) -> Result<Self, Self::Err> { 716 - Self::new(s) 717 - } 718 - } 719 - 720 - #[derive(Debug, Clone, thiserror::Error)] 721 - pub enum AtUriError { 722 - #[error("invalid AT URI: {0}")] 723 - Invalid(String), 724 - } 725 - 726 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 727 - #[serde(transparent)] 728 - #[sqlx(transparent)] 729 - pub struct Tid(String); 730 - 731 - impl From<Tid> for String { 732 - fn from(tid: Tid) -> Self { 733 - tid.0 734 - } 735 - } 736 - 737 - impl From<String> for Tid { 738 - fn from(s: String) -> Self { 739 - Tid(s) 740 - } 741 - } 742 - 743 - impl<'a> From<&'a Tid> for Cow<'a, str> { 744 - fn from(tid: &'a Tid) -> Self { 745 - Cow::Borrowed(&tid.0) 746 - } 433 + validated_string_newtype! { 434 + pub struct Tid; 435 + error = TidError; 436 + validator = |s| jacquard_common::types::string::Tid::from_str(s).map(|_| ()).map_err(|_| ()); 747 437 } 748 438 749 439 impl Tid { 750 - pub fn new(s: impl Into<String>) -> Result<Self, TidError> { 751 - let s = s.into(); 752 - jacquard::types::string::Tid::from_str(&s).map_err(|_| TidError::Invalid(s.clone()))?; 753 - Ok(Self(s)) 754 - } 755 - 756 - pub fn new_unchecked(s: impl Into<String>) -> Self { 757 - Self(s.into()) 758 - } 759 - 760 440 pub fn now() -> Self { 761 - use jacquard::types::integer::LimitedU32; 762 - Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) 763 - } 764 - 765 - pub fn as_str(&self) -> &str { 766 - &self.0 767 - } 768 - 769 - pub fn into_inner(self) -> String { 770 - self.0 771 - } 772 - } 773 - 774 - impl AsRef<str> for Tid { 775 - fn as_ref(&self) -> &str { 776 - &self.0 777 - } 778 - } 779 - 780 - impl Deref for Tid { 781 - type Target = str; 782 - 783 - fn deref(&self) -> &Self::Target { 784 - &self.0 785 - } 786 - } 787 - 788 - impl PartialEq<str> for Tid { 789 - fn eq(&self, other: &str) -> bool { 790 - self.0 == other 791 - } 792 - } 793 - 794 - impl PartialEq<&str> for Tid { 795 - fn eq(&self, other: &&str) -> bool { 796 - self.0 == *other 797 - } 798 - } 799 - 800 - impl PartialEq<String> for Tid { 801 - fn eq(&self, other: &String) -> bool { 802 - self.0 == *other 803 - } 804 - } 805 - 806 - impl PartialEq<Tid> for String { 807 - fn eq(&self, other: &Tid) -> bool { 808 - *self == other.0 809 - } 810 - } 811 - 812 - impl PartialEq<Tid> for &str { 813 - fn eq(&self, other: &Tid) -> bool { 814 - *self == other.0 441 + use jacquard_common::types::integer::LimitedU32; 442 + Self(jacquard_common::types::string::Tid::now(LimitedU32::MIN).to_string()) 815 443 } 816 444 } 817 445 818 - impl fmt::Display for Tid { 819 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 820 - write!(f, "{}", self.0) 821 - } 822 - } 823 - 824 - impl FromStr for Tid { 825 - type Err = TidError; 826 - 827 - fn from_str(s: &str) -> Result<Self, Self::Err> { 828 - Self::new(s) 829 - } 830 - } 831 - 832 - #[derive(Debug, Clone, thiserror::Error)] 833 - pub enum TidError { 834 - #[error("invalid TID: {0}")] 835 - Invalid(String), 836 - } 837 - 838 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 839 - #[serde(transparent)] 840 - #[sqlx(transparent)] 841 - pub struct Datetime(String); 842 - 843 - impl From<Datetime> for String { 844 - fn from(dt: Datetime) -> Self { 845 - dt.0 846 - } 847 - } 848 - 849 - impl From<String> for Datetime { 850 - fn from(s: String) -> Self { 851 - Datetime(s) 852 - } 853 - } 854 - 855 - impl<'a> From<&'a Datetime> for Cow<'a, str> { 856 - fn from(dt: &'a Datetime) -> Self { 857 - Cow::Borrowed(&dt.0) 858 - } 446 + validated_string_newtype! { 447 + pub struct Datetime; 448 + error = DatetimeError; 449 + validator = |s| jacquard_common::types::string::Datetime::from_str(s).map(|_| ()).map_err(|_| ()); 859 450 } 860 451 861 452 impl Datetime { 862 - pub fn new(s: impl Into<String>) -> Result<Self, DatetimeError> { 863 - let s = s.into(); 864 - jacquard::types::string::Datetime::from_str(&s) 865 - .map_err(|_| DatetimeError::Invalid(s.clone()))?; 866 - Ok(Self(s)) 867 - } 868 - 869 - pub fn new_unchecked(s: impl Into<String>) -> Self { 870 - Self(s.into()) 871 - } 872 - 873 453 pub fn now() -> Self { 874 - Self( 875 - chrono::Utc::now() 876 - .format("%Y-%m-%dT%H:%M:%S%.3fZ") 877 - .to_string(), 878 - ) 879 - } 880 - 881 - pub fn as_str(&self) -> &str { 882 - &self.0 883 - } 884 - 885 - pub fn into_inner(self) -> String { 886 - self.0 887 - } 888 - } 889 - 890 - impl AsRef<str> for Datetime { 891 - fn as_ref(&self) -> &str { 892 - &self.0 893 - } 894 - } 895 - 896 - impl Deref for Datetime { 897 - type Target = str; 898 - 899 - fn deref(&self) -> &Self::Target { 900 - &self.0 901 - } 902 - } 903 - 904 - impl PartialEq<str> for Datetime { 905 - fn eq(&self, other: &str) -> bool { 906 - self.0 == other 907 - } 908 - } 909 - 910 - impl PartialEq<&str> for Datetime { 911 - fn eq(&self, other: &&str) -> bool { 912 - self.0 == *other 913 - } 914 - } 915 - 916 - impl PartialEq<String> for Datetime { 917 - fn eq(&self, other: &String) -> bool { 918 - self.0 == *other 919 - } 920 - } 921 - 922 - impl PartialEq<Datetime> for String { 923 - fn eq(&self, other: &Datetime) -> bool { 924 - *self == other.0 925 - } 926 - } 927 - 928 - impl PartialEq<Datetime> for &str { 929 - fn eq(&self, other: &Datetime) -> bool { 930 - *self == other.0 931 - } 932 - } 933 - 934 - impl fmt::Display for Datetime { 935 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 936 - write!(f, "{}", self.0) 937 - } 938 - } 939 - 940 - impl FromStr for Datetime { 941 - type Err = DatetimeError; 942 - 943 - fn from_str(s: &str) -> Result<Self, Self::Err> { 944 - Self::new(s) 945 - } 946 - } 947 - 948 - #[derive(Debug, Clone, thiserror::Error)] 949 - pub enum DatetimeError { 950 - #[error("invalid datetime: {0}")] 951 - Invalid(String), 952 - } 953 - 954 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 955 - #[serde(transparent)] 956 - #[sqlx(transparent)] 957 - pub struct Language(String); 958 - 959 - impl From<Language> for String { 960 - fn from(lang: Language) -> Self { 961 - lang.0 962 - } 963 - } 964 - 965 - impl From<String> for Language { 966 - fn from(s: String) -> Self { 967 - Language(s) 968 - } 969 - } 970 - 971 - impl<'a> From<&'a Language> for Cow<'a, str> { 972 - fn from(lang: &'a Language) -> Self { 973 - Cow::Borrowed(&lang.0) 974 - } 975 - } 976 - 977 - impl Language { 978 - pub fn new(s: impl Into<String>) -> Result<Self, LanguageError> { 979 - let s = s.into(); 980 - jacquard::types::string::Language::from_str(&s) 981 - .map_err(|_| LanguageError::Invalid(s.clone()))?; 982 - Ok(Self(s)) 983 - } 984 - 985 - pub fn new_unchecked(s: impl Into<String>) -> Self { 986 - Self(s.into()) 987 - } 988 - 989 - pub fn as_str(&self) -> &str { 990 - &self.0 991 - } 992 - 993 - pub fn into_inner(self) -> String { 994 - self.0 995 - } 996 - } 997 - 998 - impl AsRef<str> for Language { 999 - fn as_ref(&self) -> &str { 1000 - &self.0 1001 - } 1002 - } 1003 - 1004 - impl Deref for Language { 1005 - type Target = str; 1006 - 1007 - fn deref(&self) -> &Self::Target { 1008 - &self.0 1009 - } 1010 - } 1011 - 1012 - impl PartialEq<str> for Language { 1013 - fn eq(&self, other: &str) -> bool { 1014 - self.0 == other 1015 - } 1016 - } 1017 - 1018 - impl PartialEq<&str> for Language { 1019 - fn eq(&self, other: &&str) -> bool { 1020 - self.0 == *other 1021 - } 1022 - } 1023 - 1024 - impl PartialEq<String> for Language { 1025 - fn eq(&self, other: &String) -> bool { 1026 - self.0 == *other 1027 - } 1028 - } 1029 - 1030 - impl PartialEq<Language> for String { 1031 - fn eq(&self, other: &Language) -> bool { 1032 - *self == other.0 1033 - } 1034 - } 1035 - 1036 - impl PartialEq<Language> for &str { 1037 - fn eq(&self, other: &Language) -> bool { 1038 - *self == other.0 1039 - } 1040 - } 1041 - 1042 - impl fmt::Display for Language { 1043 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1044 - write!(f, "{}", self.0) 454 + Self(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)) 1045 455 } 1046 - } 1047 456 1048 - impl FromStr for Language { 1049 - type Err = LanguageError; 1050 - 1051 - fn from_str(s: &str) -> Result<Self, Self::Err> { 1052 - Self::new(s) 457 + pub fn to_chrono(&self) -> Option<chrono::DateTime<chrono::Utc>> { 458 + chrono::DateTime::parse_from_rfc3339(&self.0) 459 + .ok() 460 + .map(|dt| dt.with_timezone(&chrono::Utc)) 1053 461 } 1054 462 } 1055 463 1056 - #[derive(Debug, Clone, thiserror::Error)] 1057 - pub enum LanguageError { 1058 - #[error("invalid language tag: {0}")] 1059 - Invalid(String), 464 + validated_string_newtype! { 465 + pub struct Language; 466 + error = LanguageError; 467 + validator = |s| jacquard_common::types::string::Language::from_str(s).map(|_| ()).map_err(|_| ()); 1060 468 } 1061 469 1062 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 470 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] 1063 471 #[serde(transparent)] 1064 472 #[sqlx(transparent)] 1065 473 pub struct CidLink(String); 1066 474 1067 - impl From<CidLink> for String { 1068 - fn from(cid: CidLink) -> Self { 1069 - cid.0 1070 - } 1071 - } 1072 - 1073 - impl From<String> for CidLink { 1074 - fn from(s: String) -> Self { 1075 - CidLink(s) 1076 - } 1077 - } 1078 - 1079 - impl<'a> From<&'a CidLink> for Cow<'a, str> { 1080 - fn from(cid: &'a CidLink) -> Self { 1081 - Cow::Borrowed(&cid.0) 475 + impl<'de> Deserialize<'de> for CidLink { 476 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 477 + where 478 + D: serde::Deserializer<'de>, 479 + { 480 + let s = String::deserialize(deserializer)?; 481 + CidLink::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) 1082 482 } 1083 483 } 1084 484 ··· 1093 493 Self(s.into()) 1094 494 } 1095 495 1096 - pub fn as_str(&self) -> &str { 1097 - &self.0 1098 - } 1099 - 1100 - pub fn into_inner(self) -> String { 1101 - self.0 1102 - } 1103 - 1104 - pub fn to_cid(&self) -> Result<cid::Cid, cid::Error> { 1105 - cid::Cid::from_str(&self.0) 1106 - } 1107 - } 1108 - 1109 - impl AsRef<str> for CidLink { 1110 - fn as_ref(&self) -> &str { 1111 - &self.0 1112 - } 1113 - } 1114 - 1115 - impl Deref for CidLink { 1116 - type Target = str; 1117 - 1118 - fn deref(&self) -> &Self::Target { 1119 - &self.0 1120 - } 1121 - } 1122 - 1123 - impl PartialEq<str> for CidLink { 1124 - fn eq(&self, other: &str) -> bool { 1125 - self.0 == other 1126 - } 1127 - } 1128 - 1129 - impl PartialEq<&str> for CidLink { 1130 - fn eq(&self, other: &&str) -> bool { 1131 - self.0 == *other 1132 - } 1133 - } 1134 - 1135 - impl PartialEq<String> for CidLink { 1136 - fn eq(&self, other: &String) -> bool { 1137 - self.0 == *other 1138 - } 1139 - } 1140 - 1141 - impl PartialEq<CidLink> for String { 1142 - fn eq(&self, other: &CidLink) -> bool { 1143 - *self == other.0 1144 - } 1145 - } 1146 - 1147 - impl PartialEq<CidLink> for &str { 1148 - fn eq(&self, other: &CidLink) -> bool { 1149 - *self == other.0 1150 - } 1151 - } 1152 - 1153 - impl fmt::Display for CidLink { 1154 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1155 - write!(f, "{}", self.0) 496 + pub fn to_cid(&self) -> Option<cid::Cid> { 497 + cid::Cid::from_str(&self.0).ok() 1156 498 } 1157 499 } 1158 500 ··· 1163 505 Self::new(s) 1164 506 } 1165 507 } 508 + 509 + impl_string_common!(CidLink); 1166 510 1167 511 #[derive(Debug, Clone, thiserror::Error)] 1168 512 pub enum CidLinkError { ··· 1264 608 } 1265 609 } 1266 610 1267 - #[derive(Debug, Clone, Deserialize)] 611 + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 1268 612 #[serde(transparent)] 1269 613 pub struct PlainPassword(String); 1270 614 ··· 1306 650 } 1307 651 } 1308 652 1309 - #[derive(Debug, Clone, Serialize, sqlx::Type)] 653 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] 1310 654 #[serde(transparent)] 1311 655 #[sqlx(transparent)] 1312 656 pub struct PasswordHash(String); 1313 657 1314 658 impl PasswordHash { 1315 - pub fn from_hash(hash: impl Into<String>) -> Self { 1316 - Self(hash.into()) 659 + pub fn new(s: impl Into<String>) -> Self { 660 + Self(s.into()) 1317 661 } 1318 662 1319 663 pub fn as_str(&self) -> &str { 1320 664 &self.0 1321 - } 1322 - 1323 - pub fn into_inner(self) -> String { 1324 - self.0 1325 665 } 1326 666 } 1327 667 ··· 1331 671 } 1332 672 } 1333 673 1334 - impl From<String> for PasswordHash { 1335 - fn from(s: String) -> Self { 1336 - Self(s) 1337 - } 1338 - } 1339 - 1340 - #[derive(Debug, Clone, PartialEq, Eq)] 674 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 675 + #[serde(tag = "type")] 1341 676 pub enum TokenSource { 677 + #[serde(rename = "session")] 1342 678 Session, 1343 - OAuth { 1344 - client_id: Option<String>, 1345 - }, 1346 - ServiceAuth { 1347 - lxm: Option<String>, 1348 - aud: Option<String>, 1349 - }, 679 + #[serde(rename = "oauth")] 680 + OAuth { client_id: ClientId, scope: String }, 681 + #[serde(rename = "service_auth")] 682 + ServiceAuth { aud: Did, lxm: Option<Nsid> }, 1350 683 } 1351 684 1352 685 impl TokenSource { ··· 1363 696 } 1364 697 } 1365 698 1366 - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 1367 - #[serde(transparent)] 1368 - pub struct JwkThumbprint(String); 1369 - 1370 - impl JwkThumbprint { 1371 - pub fn new(s: impl Into<String>) -> Self { 1372 - Self(s.into()) 1373 - } 1374 - 1375 - pub fn as_str(&self) -> &str { 1376 - &self.0 1377 - } 1378 - 1379 - pub fn into_inner(self) -> String { 1380 - self.0 1381 - } 1382 - } 1383 - 1384 - impl AsRef<str> for JwkThumbprint { 1385 - fn as_ref(&self) -> &str { 1386 - &self.0 1387 - } 1388 - } 1389 - 1390 - impl Deref for JwkThumbprint { 1391 - type Target = str; 1392 - 1393 - fn deref(&self) -> &Self::Target { 1394 - &self.0 1395 - } 1396 - } 1397 - 1398 - impl From<String> for JwkThumbprint { 1399 - fn from(s: String) -> Self { 1400 - Self(s) 1401 - } 1402 - } 1403 - 1404 - impl PartialEq<str> for JwkThumbprint { 1405 - fn eq(&self, other: &str) -> bool { 1406 - self.0 == other 1407 - } 1408 - } 1409 - 1410 - impl PartialEq<String> for JwkThumbprint { 1411 - fn eq(&self, other: &String) -> bool { 1412 - &self.0 == other 1413 - } 1414 - } 1415 - 1416 - impl PartialEq<JwkThumbprint> for String { 1417 - fn eq(&self, other: &JwkThumbprint) -> bool { 1418 - self == &other.0 1419 - } 1420 - } 1421 - 1422 - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 1423 - #[serde(transparent)] 1424 - pub struct DPoPProofId(String); 1425 - 1426 - impl DPoPProofId { 1427 - pub fn new(s: impl Into<String>) -> Self { 1428 - Self(s.into()) 1429 - } 1430 - 1431 - pub fn as_str(&self) -> &str { 1432 - &self.0 1433 - } 1434 - 1435 - pub fn into_inner(self) -> String { 1436 - self.0 1437 - } 1438 - } 1439 - 1440 - impl AsRef<str> for DPoPProofId { 1441 - fn as_ref(&self) -> &str { 1442 - &self.0 1443 - } 1444 - } 1445 - 1446 - impl Deref for DPoPProofId { 1447 - type Target = str; 1448 - 1449 - fn deref(&self) -> &Self::Target { 1450 - &self.0 1451 - } 1452 - } 1453 - 1454 - impl From<String> for DPoPProofId { 1455 - fn from(s: String) -> Self { 1456 - Self(s) 1457 - } 1458 - } 1459 - 1460 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1461 - #[serde(transparent)] 1462 - #[sqlx(transparent)] 1463 - pub struct TokenId(String); 1464 - 1465 - impl TokenId { 1466 - pub fn new(s: impl Into<String>) -> Self { 1467 - Self(s.into()) 1468 - } 1469 - 1470 - pub fn as_str(&self) -> &str { 1471 - &self.0 1472 - } 1473 - 1474 - pub fn into_inner(self) -> String { 1475 - self.0 1476 - } 1477 - } 1478 - 1479 - impl AsRef<str> for TokenId { 1480 - fn as_ref(&self) -> &str { 1481 - &self.0 1482 - } 1483 - } 1484 - 1485 - impl Deref for TokenId { 1486 - type Target = str; 1487 - 1488 - fn deref(&self) -> &Self::Target { 1489 - &self.0 1490 - } 1491 - } 1492 - 1493 - impl From<String> for TokenId { 1494 - fn from(s: String) -> Self { 1495 - Self(s) 1496 - } 1497 - } 1498 - 1499 - impl fmt::Display for TokenId { 1500 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1501 - write!(f, "{}", self.0) 1502 - } 1503 - } 1504 - 1505 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1506 - #[serde(transparent)] 1507 - #[sqlx(transparent)] 1508 - pub struct ClientId(String); 1509 - 1510 - impl ClientId { 1511 - pub fn new(s: impl Into<String>) -> Self { 1512 - Self(s.into()) 1513 - } 1514 - 1515 - pub fn as_str(&self) -> &str { 1516 - &self.0 1517 - } 1518 - 1519 - pub fn into_inner(self) -> String { 1520 - self.0 1521 - } 1522 - } 1523 - 1524 - impl AsRef<str> for ClientId { 1525 - fn as_ref(&self) -> &str { 1526 - &self.0 1527 - } 1528 - } 1529 - 1530 - impl Deref for ClientId { 1531 - type Target = str; 1532 - 1533 - fn deref(&self) -> &Self::Target { 1534 - &self.0 1535 - } 1536 - } 1537 - 1538 - impl From<String> for ClientId { 1539 - fn from(s: String) -> Self { 1540 - Self(s) 1541 - } 1542 - } 1543 - 1544 - impl fmt::Display for ClientId { 1545 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1546 - write!(f, "{}", self.0) 1547 - } 1548 - } 1549 - 1550 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1551 - #[serde(transparent)] 1552 - #[sqlx(transparent)] 1553 - pub struct DeviceId(String); 1554 - 1555 - impl DeviceId { 1556 - pub fn new(s: impl Into<String>) -> Self { 1557 - Self(s.into()) 1558 - } 1559 - 1560 - pub fn as_str(&self) -> &str { 1561 - &self.0 1562 - } 1563 - 1564 - pub fn into_inner(self) -> String { 1565 - self.0 1566 - } 1567 - } 1568 - 1569 - impl AsRef<str> for DeviceId { 1570 - fn as_ref(&self) -> &str { 1571 - &self.0 1572 - } 1573 - } 1574 - 1575 - impl Deref for DeviceId { 1576 - type Target = str; 1577 - 1578 - fn deref(&self) -> &Self::Target { 1579 - &self.0 1580 - } 1581 - } 1582 - 1583 - impl From<String> for DeviceId { 1584 - fn from(s: String) -> Self { 1585 - Self(s) 1586 - } 1587 - } 1588 - 1589 - impl fmt::Display for DeviceId { 1590 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1591 - write!(f, "{}", self.0) 1592 - } 1593 - } 1594 - 1595 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1596 - #[serde(transparent)] 1597 - #[sqlx(transparent)] 1598 - pub struct RequestId(String); 1599 - 1600 - impl RequestId { 1601 - pub fn new(s: impl Into<String>) -> Self { 1602 - Self(s.into()) 1603 - } 1604 - 1605 - pub fn as_str(&self) -> &str { 1606 - &self.0 1607 - } 1608 - 1609 - pub fn into_inner(self) -> String { 1610 - self.0 1611 - } 1612 - } 1613 - 1614 - impl AsRef<str> for RequestId { 1615 - fn as_ref(&self) -> &str { 1616 - &self.0 1617 - } 1618 - } 1619 - 1620 - impl Deref for RequestId { 1621 - type Target = str; 1622 - 1623 - fn deref(&self) -> &Self::Target { 1624 - &self.0 1625 - } 1626 - } 1627 - 1628 - impl From<String> for RequestId { 1629 - fn from(s: String) -> Self { 1630 - Self(s) 1631 - } 1632 - } 1633 - 1634 - impl fmt::Display for RequestId { 1635 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1636 - write!(f, "{}", self.0) 1637 - } 1638 - } 1639 - 1640 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1641 - #[serde(transparent)] 1642 - #[sqlx(transparent)] 1643 - pub struct Jti(String); 1644 - 1645 - impl Jti { 1646 - pub fn new(s: impl Into<String>) -> Self { 1647 - Self(s.into()) 1648 - } 1649 - 1650 - pub fn as_str(&self) -> &str { 1651 - &self.0 1652 - } 1653 - 1654 - pub fn into_inner(self) -> String { 1655 - self.0 1656 - } 699 + simple_string_newtype_no_sqlx! { 700 + pub struct JwkThumbprint; 1657 701 } 1658 702 1659 - impl AsRef<str> for Jti { 1660 - fn as_ref(&self) -> &str { 1661 - &self.0 1662 - } 703 + simple_string_newtype_no_sqlx! { 704 + pub struct DPoPProofId; 1663 705 } 1664 706 1665 - impl Deref for Jti { 1666 - type Target = str; 1667 - 1668 - fn deref(&self) -> &Self::Target { 1669 - &self.0 1670 - } 707 + simple_string_newtype! { 708 + pub struct TokenId; 1671 709 } 1672 710 1673 - impl From<String> for Jti { 1674 - fn from(s: String) -> Self { 1675 - Self(s) 1676 - } 711 + simple_string_newtype! { 712 + pub struct ClientId; 1677 713 } 1678 714 1679 - impl fmt::Display for Jti { 1680 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1681 - write!(f, "{}", self.0) 1682 - } 715 + simple_string_newtype! { 716 + pub struct DeviceId; 1683 717 } 1684 718 1685 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1686 - #[serde(transparent)] 1687 - #[sqlx(transparent)] 1688 - pub struct AuthorizationCode(String); 1689 - 1690 - impl AuthorizationCode { 1691 - pub fn new(s: impl Into<String>) -> Self { 1692 - Self(s.into()) 1693 - } 1694 - 1695 - pub fn as_str(&self) -> &str { 1696 - &self.0 1697 - } 1698 - 1699 - pub fn into_inner(self) -> String { 1700 - self.0 1701 - } 719 + simple_string_newtype! { 720 + pub struct RequestId; 1702 721 } 1703 722 1704 - impl AsRef<str> for AuthorizationCode { 1705 - fn as_ref(&self) -> &str { 1706 - &self.0 1707 - } 723 + simple_string_newtype! { 724 + pub struct Jti; 1708 725 } 1709 726 1710 - impl Deref for AuthorizationCode { 1711 - type Target = str; 1712 - 1713 - fn deref(&self) -> &Self::Target { 1714 - &self.0 1715 - } 727 + simple_string_newtype! { 728 + pub struct AuthorizationCode; 1716 729 } 1717 730 1718 - impl From<String> for AuthorizationCode { 1719 - fn from(s: String) -> Self { 1720 - Self(s) 1721 - } 731 + simple_string_newtype! { 732 + pub struct RefreshToken; 1722 733 } 1723 734 1724 - impl fmt::Display for AuthorizationCode { 1725 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1726 - write!(f, "{}", self.0) 1727 - } 735 + simple_string_newtype! { 736 + pub struct InviteCode; 1728 737 } 1729 738 1730 739 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1731 - #[serde(transparent)] 1732 - #[sqlx(transparent)] 1733 - pub struct RefreshToken(String); 1734 - 1735 - impl RefreshToken { 1736 - pub fn new(s: impl Into<String>) -> Self { 1737 - Self(s.into()) 1738 - } 1739 - 1740 - pub fn as_str(&self) -> &str { 1741 - &self.0 1742 - } 1743 - 1744 - pub fn into_inner(self) -> String { 1745 - self.0 1746 - } 740 + #[sqlx(type_name = "comms_channel", rename_all = "snake_case")] 741 + #[serde(rename_all = "snake_case")] 742 + #[derive(Copy)] 743 + pub enum CommsChannel { 744 + Email, 745 + Discord, 746 + Telegram, 747 + Signal, 1747 748 } 1748 749 1749 - impl AsRef<str> for RefreshToken { 1750 - fn as_ref(&self) -> &str { 1751 - &self.0 1752 - } 1753 - } 1754 - 1755 - impl Deref for RefreshToken { 1756 - type Target = str; 1757 - 1758 - fn deref(&self) -> &Self::Target { 1759 - &self.0 750 + impl CommsChannel { 751 + pub fn as_str(&self) -> &'static str { 752 + match self { 753 + CommsChannel::Email => "email", 754 + CommsChannel::Discord => "discord", 755 + CommsChannel::Telegram => "telegram", 756 + CommsChannel::Signal => "signal", 757 + } 1760 758 } 1761 - } 1762 759 1763 - impl From<String> for RefreshToken { 1764 - fn from(s: String) -> Self { 1765 - Self(s) 760 + pub fn from_str_opt(s: &str) -> Option<Self> { 761 + match s { 762 + "email" => Some(CommsChannel::Email), 763 + "discord" => Some(CommsChannel::Discord), 764 + "telegram" => Some(CommsChannel::Telegram), 765 + "signal" => Some(CommsChannel::Signal), 766 + _ => None, 767 + } 1766 768 } 1767 769 } 1768 770 1769 - impl fmt::Display for RefreshToken { 771 + impl fmt::Display for CommsChannel { 1770 772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1771 - write!(f, "{}", self.0) 773 + write!(f, "{}", self.as_str()) 1772 774 } 1773 775 } 1774 776 1775 777 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1776 - #[serde(transparent)] 1777 - #[sqlx(transparent)] 1778 - pub struct InviteCode(String); 1779 - 1780 - impl InviteCode { 1781 - pub fn new(s: impl Into<String>) -> Self { 1782 - Self(s.into()) 1783 - } 1784 - 1785 - pub fn as_str(&self) -> &str { 1786 - &self.0 1787 - } 1788 - 1789 - pub fn into_inner(self) -> String { 1790 - self.0 1791 - } 1792 - } 1793 - 1794 - impl AsRef<str> for InviteCode { 1795 - fn as_ref(&self) -> &str { 1796 - &self.0 1797 - } 1798 - } 1799 - 1800 - impl Deref for InviteCode { 1801 - type Target = str; 1802 - 1803 - fn deref(&self) -> &Self::Target { 1804 - &self.0 1805 - } 1806 - } 1807 - 1808 - impl From<String> for InviteCode { 1809 - fn from(s: String) -> Self { 1810 - Self(s) 1811 - } 1812 - } 1813 - 1814 - impl fmt::Display for InviteCode { 1815 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1816 - write!(f, "{}", self.0) 1817 - } 1818 - } 1819 - 1820 - #[cfg(test)] 1821 - mod tests { 1822 - use super::*; 1823 - 1824 - #[test] 1825 - fn test_did_validation() { 1826 - assert!(Did::new("did:plc:abc123").is_ok()); 1827 - assert!(Did::new("did:web:example.com").is_ok()); 1828 - assert!(Did::new("not-a-did").is_err()); 1829 - assert!(Did::new("").is_err()); 1830 - } 1831 - 1832 - #[test] 1833 - fn test_did_methods() { 1834 - let plc = Did::new("did:plc:abc123").unwrap(); 1835 - assert!(plc.is_plc()); 1836 - assert!(!plc.is_web()); 1837 - assert_eq!(plc.as_str(), "did:plc:abc123"); 1838 - 1839 - let web = Did::new("did:web:example.com").unwrap(); 1840 - assert!(!web.is_plc()); 1841 - assert!(web.is_web()); 1842 - } 1843 - 1844 - #[test] 1845 - fn test_did_conversions() { 1846 - let did = Did::new("did:plc:test123").unwrap(); 1847 - let s: String = did.clone().into(); 1848 - assert_eq!(s, "did:plc:test123"); 1849 - assert_eq!(format!("{}", did), "did:plc:test123"); 1850 - } 1851 - 1852 - #[test] 1853 - fn test_did_serde() { 1854 - let did = Did::new("did:plc:test123").unwrap(); 1855 - let json = serde_json::to_string(&did).unwrap(); 1856 - assert_eq!(json, "\"did:plc:test123\""); 1857 - 1858 - let parsed: Did = serde_json::from_str(&json).unwrap(); 1859 - assert_eq!(parsed, did); 1860 - } 1861 - 1862 - #[test] 1863 - fn test_handle_validation() { 1864 - assert!(Handle::new("user.bsky.social").is_ok()); 1865 - assert!(Handle::new("test.example.com").is_ok()); 1866 - assert!(Handle::new("invalid handle with spaces").is_err()); 1867 - assert!(Handle::new("alice.pds.test").is_ok()); 1868 - } 1869 - 1870 - #[test] 1871 - fn test_rkey_validation() { 1872 - assert!(Rkey::new("self").is_ok()); 1873 - assert!(Rkey::new("3jzfcijpj2z2a").is_ok()); 1874 - assert!(Rkey::new("invalid/rkey").is_err()); 1875 - } 1876 - 1877 - #[test] 1878 - fn test_rkey_generate() { 1879 - let rkey = Rkey::generate(); 1880 - assert!(rkey.is_tid()); 1881 - assert!(!rkey.as_str().is_empty()); 1882 - } 1883 - 1884 - #[test] 1885 - fn test_nsid_validation() { 1886 - assert!(Nsid::new("app.bsky.feed.post").is_ok()); 1887 - assert!(Nsid::new("com.atproto.repo.createRecord").is_ok()); 1888 - assert!(Nsid::new("invalid").is_err()); 1889 - } 1890 - 1891 - #[test] 1892 - fn test_nsid_parts() { 1893 - let nsid = Nsid::new("app.bsky.feed.post").unwrap(); 1894 - assert_eq!(nsid.name(), Some("post")); 1895 - } 1896 - 1897 - #[test] 1898 - fn test_at_uri_validation() { 1899 - assert!(AtUri::new("at://did:plc:abc123/app.bsky.feed.post/xyz").is_ok()); 1900 - assert!(AtUri::new("not-an-at-uri").is_err()); 1901 - } 1902 - 1903 - #[test] 1904 - fn test_at_uri_from_parts() { 1905 - let uri = AtUri::from_parts("did:plc:abc123", "app.bsky.feed.post", "xyz"); 1906 - assert_eq!(uri.as_str(), "at://did:plc:abc123/app.bsky.feed.post/xyz"); 1907 - } 1908 - 1909 - #[test] 1910 - fn test_type_safety() { 1911 - fn takes_did(_: &Did) {} 1912 - fn takes_handle(_: &Handle) {} 1913 - 1914 - let did = Did::new("did:plc:test").unwrap(); 1915 - let handle = Handle::new("test.bsky.social").unwrap(); 1916 - 1917 - takes_did(&did); 1918 - takes_handle(&handle); 1919 - } 1920 - 1921 - #[test] 1922 - fn test_tid_validation() { 1923 - let tid = Tid::now(); 1924 - assert!(!tid.as_str().is_empty()); 1925 - assert!(Tid::new(tid.as_str()).is_ok()); 1926 - assert!(Tid::new("invalid").is_err()); 1927 - } 1928 - 1929 - #[test] 1930 - fn test_datetime_validation() { 1931 - assert!(Datetime::new("2024-01-15T12:30:45.123Z").is_ok()); 1932 - assert!(Datetime::new("not-a-date").is_err()); 1933 - let now = Datetime::now(); 1934 - assert!(!now.as_str().is_empty()); 1935 - } 1936 - 1937 - #[test] 1938 - fn test_language_validation() { 1939 - assert!(Language::new("en").is_ok()); 1940 - assert!(Language::new("en-US").is_ok()); 1941 - assert!(Language::new("ja").is_ok()); 1942 - } 1943 - 1944 - #[test] 1945 - fn test_cidlink_validation() { 1946 - assert!( 1947 - CidLink::new("bafyreib74ckyq525l3y6an5txykwwtb3dgex6ofzakml53di77oxwr5pfe").is_ok() 1948 - ); 1949 - assert!(CidLink::new("not-a-cid").is_err()); 1950 - } 1951 - 1952 - #[test] 1953 - fn test_at_identifier_validation() { 1954 - let did_ident = AtIdentifier::new("did:plc:abc123").unwrap(); 1955 - assert!(did_ident.is_did()); 1956 - assert!(!did_ident.is_handle()); 1957 - assert!(did_ident.as_did().is_some()); 1958 - assert!(did_ident.as_handle().is_none()); 1959 - 1960 - let handle_ident = AtIdentifier::new("user.bsky.social").unwrap(); 1961 - assert!(!handle_ident.is_did()); 1962 - assert!(handle_ident.is_handle()); 1963 - assert!(handle_ident.as_did().is_none()); 1964 - assert!(handle_ident.as_handle().is_some()); 1965 - 1966 - assert!(AtIdentifier::new("invalid identifier").is_err()); 1967 - } 1968 - 1969 - #[test] 1970 - fn test_at_identifier_serde() { 1971 - let ident = AtIdentifier::new("did:plc:test123").unwrap(); 1972 - let json = serde_json::to_string(&ident).unwrap(); 1973 - assert_eq!(json, "\"did:plc:test123\""); 1974 - 1975 - let parsed: AtIdentifier = serde_json::from_str(&json).unwrap(); 1976 - assert_eq!(parsed.as_str(), "did:plc:test123"); 1977 - } 1978 - 1979 - #[test] 1980 - fn test_account_state_active() { 1981 - let state = AccountState::from_db_fields(None, None, None, None); 1982 - assert!(state.is_active()); 1983 - assert!(!state.is_deactivated()); 1984 - assert!(!state.is_takendown()); 1985 - assert!(!state.is_migrated()); 1986 - assert!(state.can_login()); 1987 - assert!(state.can_access_repo()); 1988 - assert_eq!(state.status_string(), "active"); 1989 - } 1990 - 1991 - #[test] 1992 - fn test_account_state_deactivated() { 1993 - let now = chrono::Utc::now(); 1994 - let state = AccountState::from_db_fields(Some(now), None, None, None); 1995 - assert!(!state.is_active()); 1996 - assert!(state.is_deactivated()); 1997 - assert!(!state.is_takendown()); 1998 - assert!(!state.is_migrated()); 1999 - assert!(!state.can_login()); 2000 - assert!(state.can_access_repo()); 2001 - assert_eq!(state.status_string(), "deactivated"); 2002 - } 2003 - 2004 - #[test] 2005 - fn test_account_state_takendown() { 2006 - let state = AccountState::from_db_fields(None, Some("mod-action-123".into()), None, None); 2007 - assert!(!state.is_active()); 2008 - assert!(!state.is_deactivated()); 2009 - assert!(state.is_takendown()); 2010 - assert!(!state.is_migrated()); 2011 - assert!(!state.can_login()); 2012 - assert!(!state.can_access_repo()); 2013 - assert_eq!(state.status_string(), "takendown"); 2014 - } 2015 - 2016 - #[test] 2017 - fn test_account_state_migrated() { 2018 - let now = chrono::Utc::now(); 2019 - let state = 2020 - AccountState::from_db_fields(Some(now), None, Some("https://other.pds".into()), None); 2021 - assert!(!state.is_active()); 2022 - assert!(!state.is_deactivated()); 2023 - assert!(!state.is_takendown()); 2024 - assert!(state.is_migrated()); 2025 - assert!(!state.can_login()); 2026 - assert!(!state.can_access_repo()); 2027 - assert_eq!(state.status_string(), "deactivated"); 2028 - } 2029 - 2030 - #[test] 2031 - fn test_account_state_takedown_priority() { 2032 - let now = chrono::Utc::now(); 2033 - let state = AccountState::from_db_fields( 2034 - Some(now), 2035 - Some("mod-action".into()), 2036 - Some("https://other.pds".into()), 2037 - None, 2038 - ); 2039 - assert!(state.is_takendown()); 2040 - } 778 + #[sqlx(type_name = "comms_type", rename_all = "snake_case")] 779 + #[serde(rename_all = "snake_case")] 780 + pub enum CommsType { 781 + Verification, 782 + PasswordReset, 783 + AccountDeleted, 784 + AccountMigrated, 785 + PasskeyRecovery, 786 + MigrationVerification, 2041 787 }
+29 -11
frontend/src/lib/oauth.ts
··· 9 9 "repo:*?action=update", 10 10 "repo:*?action=delete", 11 11 "blob:*/*", 12 + "identity:*", 13 + "account:*", 12 14 ].join(" "); 13 15 14 16 const CLIENT_ID = !(import.meta.env.DEV) ··· 259 261 } 260 262 } 261 263 262 - export async function startOAuthLogin(): Promise<void> { 264 + export async function startOAuthLogin(loginHint?: string): Promise<void> { 263 265 clearAllOAuthState(); 264 266 265 267 const state = generateState(); ··· 271 273 272 274 saveOAuthState({ state, codeVerifier }); 273 275 276 + const parParams: Record<string, string> = { 277 + client_id: CLIENT_ID, 278 + redirect_uri: REDIRECT_URI, 279 + response_type: "code", 280 + scope: SCOPES, 281 + state: state, 282 + code_challenge: codeChallenge, 283 + code_challenge_method: "S256", 284 + dpop_jkt: dpopJkt, 285 + }; 286 + if (loginHint) { 287 + parParams.login_hint = loginHint; 288 + } 289 + 274 290 const parResponse = await fetch("/oauth/par", { 275 291 method: "POST", 276 292 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 277 - body: new URLSearchParams({ 278 - client_id: CLIENT_ID, 279 - redirect_uri: REDIRECT_URI, 280 - response_type: "code", 281 - scope: SCOPES, 282 - state: state, 283 - code_challenge: codeChallenge, 284 - code_challenge_method: "S256", 285 - dpop_jkt: dpopJkt, 286 - }), 293 + body: new URLSearchParams(parParams), 287 294 }); 288 295 289 296 if (!parResponse.ok) { ··· 415 422 url.search = ""; 416 423 globalThis.history.replaceState({}, "", url.toString()); 417 424 } 425 + 426 + export async function createDPoPProofForRequest( 427 + method: string, 428 + url: string, 429 + accessToken: string, 430 + ): Promise<string> { 431 + const keyPair = await getOrCreateDPoPKeyPair(); 432 + const tokenHash = await sha256(accessToken); 433 + const ath = base64UrlEncode(tokenHash); 434 + return createDPoPProof(keyPair, method, url, getDPoPNonce() ?? undefined, ath); 435 + }
+45
frontend/src/lib/registration/flow.svelte.ts
··· 18 18 RegistrationStep, 19 19 SessionState, 20 20 } from "./types.ts"; 21 + import { 22 + saveRegistrationState, 23 + loadRegistrationState, 24 + clearRegistrationState, 25 + } from "./storage.ts"; 21 26 22 27 export interface RegistrationFlowState { 23 28 mode: RegistrationMode; ··· 76 81 return did.replace("did:web:", "").replace(/%3A/g, ":"); 77 82 } 78 83 84 + function persistState() { 85 + if (state.step !== "info" && state.step !== "creating") { 86 + saveRegistrationState( 87 + state.mode, 88 + state.step, 89 + state.pdsHostname, 90 + state.info, 91 + state.externalDidWeb, 92 + state.account, 93 + state.session, 94 + ); 95 + } 96 + } 97 + 79 98 function setError(err: unknown) { 80 99 if (err instanceof ApiError) { 81 100 state.error = err.message || "An error occurred"; ··· 129 148 "\t", 130 149 ); 131 150 state.step = "initial-did-doc"; 151 + persistState(); 132 152 } catch (err) { 133 153 setError(err); 134 154 } finally { ··· 184 204 handle: result.handle, 185 205 }; 186 206 state.step = "verify"; 207 + persistState(); 187 208 } catch (err) { 188 209 setError(err); 189 210 } finally { ··· 237 258 setupToken: result.setupToken, 238 259 }; 239 260 state.step = "passkey"; 261 + persistState(); 240 262 } catch (err) { 241 263 setError(err); 242 264 } finally { ··· 250 272 state.account.appPasswordName = appPasswordName; 251 273 } 252 274 state.step = "app-password"; 275 + persistState(); 253 276 } 254 277 255 278 function proceedFromAppPassword() { 256 279 state.step = "verify"; 280 + persistState(); 257 281 } 258 282 259 283 async function verifyAccount(code: string) { ··· 296 320 "\t", 297 321 ); 298 322 state.step = "updated-did-doc"; 323 + persistState(); 299 324 } else { 300 325 await api.activateAccount(session.accessJwt); 301 326 await finalizeSession(); ··· 349 374 350 375 async function finalizeSession() { 351 376 if (!state.session || !state.account) return; 377 + clearRegistrationState(); 352 378 setSession({ 353 379 did: state.account.did, 354 380 handle: state.account.handle, ··· 404 430 } 405 431 406 432 export type RegistrationFlow = ReturnType<typeof createRegistrationFlow>; 433 + 434 + export function restoreRegistrationFlow(): RegistrationFlow | null { 435 + const saved = loadRegistrationState(); 436 + if (!saved || saved.step === "info" || saved.step === "redirect-to-dashboard") { 437 + return null; 438 + } 439 + 440 + const flow = createRegistrationFlow(saved.mode, saved.pdsHostname); 441 + 442 + flow.state.step = saved.step; 443 + flow.state.info = { ...flow.state.info, ...saved.info }; 444 + flow.state.externalDidWeb = { ...flow.state.externalDidWeb, ...saved.externalDidWeb }; 445 + flow.state.account = saved.account; 446 + flow.state.session = saved.session; 447 + 448 + return flow; 449 + } 450 + 451 + export { hasPendingRegistration, getRegistrationResumeInfo, clearRegistrationState } from "./storage.ts";
+195
frontend/src/lib/registration/storage.ts
··· 1 + import type { 2 + RegistrationMode, 3 + RegistrationStep, 4 + RegistrationInfo, 5 + ExternalDidWebState, 6 + AccountResult, 7 + SessionState, 8 + } from "./types.ts"; 9 + 10 + const STORAGE_KEY = "tranquil_registration_state"; 11 + const MAX_AGE_MS = 60 * 60 * 1000; 12 + 13 + interface StoredRegistrationState { 14 + version: 1; 15 + startedAt: string; 16 + mode: RegistrationMode; 17 + step: RegistrationStep; 18 + pdsHostname: string; 19 + info: RegistrationInfo; 20 + externalDidWeb: StoredExternalDidWebState; 21 + account: StoredAccountResult | null; 22 + session: StoredSessionState | null; 23 + } 24 + 25 + interface StoredExternalDidWebState { 26 + keyMode: "reserved" | "byod"; 27 + reservedSigningKey?: string; 28 + byodPrivateKeyBase64?: string; 29 + byodPublicKeyMultibase?: string; 30 + initialDidDocument?: string; 31 + updatedDidDocument?: string; 32 + } 33 + 34 + interface StoredAccountResult { 35 + did: string; 36 + handle: string; 37 + setupToken?: string; 38 + appPassword?: string; 39 + appPasswordName?: string; 40 + } 41 + 42 + interface StoredSessionState { 43 + accessJwt: string; 44 + refreshJwt: string; 45 + } 46 + 47 + function uint8ArrayToBase64(arr: Uint8Array): string { 48 + return btoa(Array.from(arr, (byte) => String.fromCharCode(byte)).join("")); 49 + } 50 + 51 + function base64ToUint8Array(base64: string): Uint8Array { 52 + const binary = atob(base64); 53 + return Uint8Array.from(binary, (char) => char.charCodeAt(0)); 54 + } 55 + 56 + export function saveRegistrationState( 57 + mode: RegistrationMode, 58 + step: RegistrationStep, 59 + pdsHostname: string, 60 + info: RegistrationInfo, 61 + externalDidWeb: ExternalDidWebState, 62 + account: AccountResult | null, 63 + session: SessionState | null, 64 + ): void { 65 + const stored: StoredRegistrationState = { 66 + version: 1, 67 + startedAt: new Date().toISOString(), 68 + mode, 69 + step, 70 + pdsHostname, 71 + info: { ...info, password: undefined }, 72 + externalDidWeb: { 73 + keyMode: externalDidWeb.keyMode, 74 + reservedSigningKey: externalDidWeb.reservedSigningKey, 75 + byodPrivateKeyBase64: externalDidWeb.byodPrivateKey 76 + ? uint8ArrayToBase64(externalDidWeb.byodPrivateKey) 77 + : undefined, 78 + byodPublicKeyMultibase: externalDidWeb.byodPublicKeyMultibase, 79 + initialDidDocument: externalDidWeb.initialDidDocument, 80 + updatedDidDocument: externalDidWeb.updatedDidDocument, 81 + }, 82 + account: account 83 + ? { 84 + did: account.did, 85 + handle: account.handle, 86 + setupToken: account.setupToken, 87 + appPassword: account.appPassword, 88 + appPasswordName: account.appPasswordName, 89 + } 90 + : null, 91 + session: session 92 + ? { 93 + accessJwt: session.accessJwt, 94 + refreshJwt: session.refreshJwt, 95 + } 96 + : null, 97 + }; 98 + 99 + try { 100 + localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); 101 + } catch { /* localStorage unavailable */ } 102 + } 103 + 104 + export function loadRegistrationState(): { 105 + mode: RegistrationMode; 106 + step: RegistrationStep; 107 + pdsHostname: string; 108 + info: RegistrationInfo; 109 + externalDidWeb: ExternalDidWebState; 110 + account: AccountResult | null; 111 + session: SessionState | null; 112 + } | null { 113 + try { 114 + const stored = localStorage.getItem(STORAGE_KEY); 115 + if (!stored) return null; 116 + 117 + const state = JSON.parse(stored) as StoredRegistrationState; 118 + 119 + if (state.version !== 1) { 120 + clearRegistrationState(); 121 + return null; 122 + } 123 + 124 + const startedAt = new Date(state.startedAt).getTime(); 125 + if (Date.now() - startedAt > MAX_AGE_MS) { 126 + clearRegistrationState(); 127 + return null; 128 + } 129 + 130 + return { 131 + mode: state.mode, 132 + step: state.step, 133 + pdsHostname: state.pdsHostname, 134 + info: state.info, 135 + externalDidWeb: { 136 + keyMode: state.externalDidWeb.keyMode, 137 + reservedSigningKey: state.externalDidWeb.reservedSigningKey, 138 + byodPrivateKey: state.externalDidWeb.byodPrivateKeyBase64 139 + ? base64ToUint8Array(state.externalDidWeb.byodPrivateKeyBase64) 140 + : undefined, 141 + byodPublicKeyMultibase: state.externalDidWeb.byodPublicKeyMultibase, 142 + initialDidDocument: state.externalDidWeb.initialDidDocument, 143 + updatedDidDocument: state.externalDidWeb.updatedDidDocument, 144 + }, 145 + account: state.account 146 + ? { 147 + did: state.account.did as AccountResult["did"], 148 + handle: state.account.handle as AccountResult["handle"], 149 + setupToken: state.account.setupToken, 150 + appPassword: state.account.appPassword, 151 + appPasswordName: state.account.appPasswordName, 152 + } 153 + : null, 154 + session: state.session 155 + ? { 156 + accessJwt: state.session.accessJwt as SessionState["accessJwt"], 157 + refreshJwt: state.session.refreshJwt as SessionState["refreshJwt"], 158 + } 159 + : null, 160 + }; 161 + } catch { 162 + clearRegistrationState(); 163 + return null; 164 + } 165 + } 166 + 167 + export function clearRegistrationState(): void { 168 + try { 169 + localStorage.removeItem(STORAGE_KEY); 170 + } catch { /* localStorage unavailable */ } 171 + } 172 + 173 + export function hasPendingRegistration(): boolean { 174 + const state = loadRegistrationState(); 175 + return state !== null && state.step !== "info" && state.step !== "redirect-to-dashboard"; 176 + } 177 + 178 + export function getRegistrationResumeInfo(): { 179 + mode: RegistrationMode; 180 + handle: string; 181 + step: RegistrationStep; 182 + did?: string; 183 + } | null { 184 + const state = loadRegistrationState(); 185 + if (!state || state.step === "info" || state.step === "redirect-to-dashboard") { 186 + return null; 187 + } 188 + 189 + return { 190 + mode: state.mode, 191 + handle: state.info.handle, 192 + step: state.step, 193 + did: state.account?.did, 194 + }; 195 + }
+53 -1
frontend/src/locales/en.json
··· 703 703 "signingInAs": "Signing in as:", 704 704 "permissionsRequested": "Permissions Requested", 705 705 "required": "Required", 706 - "rememberChoiceLabel": "Remember my choice for this application" 706 + "rememberChoiceLabel": "Remember my choice for this application", 707 + "scopes": { 708 + "atproto": { 709 + "name": "Full Account Access", 710 + "description": "Full access to read, write, and manage this account" 711 + }, 712 + "atprotoWithGranular": { 713 + "name": "AT Protocol Access", 714 + "description": "AT Protocol baseline scope (permissions determined by selected options below)" 715 + }, 716 + "transitionGeneric": { 717 + "name": "Transition Access", 718 + "description": "Generic transition scope for compatibility" 719 + }, 720 + "transitionChat": { 721 + "name": "Chat Access", 722 + "description": "Access to Bluesky chat features" 723 + }, 724 + "transitionEmail": { 725 + "name": "Email Access", 726 + "description": "Read your account email address" 727 + }, 728 + "repoCreate": { 729 + "name": "Create Records", 730 + "description": "Create new records in your repository" 731 + }, 732 + "repoUpdate": { 733 + "name": "Update Records", 734 + "description": "Update existing records in your repository" 735 + }, 736 + "repoDelete": { 737 + "name": "Delete Records", 738 + "description": "Delete records from your repository" 739 + }, 740 + "blobAll": { 741 + "name": "Upload Media", 742 + "description": "Upload images, videos, and other media files" 743 + }, 744 + "repoFull": { 745 + "name": "Full Repository Access", 746 + "description": "Full read and write access to all repository records" 747 + }, 748 + "accountManage": { 749 + "name": "Manage Account", 750 + "description": "Manage account settings and preferences" 751 + } 752 + } 707 753 }, 708 754 "accounts": { 709 755 "title": "Choose Account", ··· 999 1045 "accessLevel": "Access Level", 1000 1046 "adding": "Adding...", 1001 1047 "addControllerButton": "+ Add Controller", 1048 + "addControllerWarningTitle": "Important: This will change how you log in", 1049 + "addControllerWarningText": "Adding a controller means that only the controller account will be able to log into this account via OAuth. You will no longer be able to log in directly with your own credentials through third-party apps or the web interface.", 1050 + "addControllerWarningBullet1": "The controller will be able to act on your behalf with the permissions you grant", 1051 + "addControllerWarningBullet2": "You will need to log in as the controller first, then switch to this account", 1052 + "addControllerWarningBullet3": "You can remove the controller later to regain direct login access", 1053 + "addControllerConfirm": "I understand that I will no longer be able to log in directly", 1002 1054 "controllerAdded": "Controller added successfully", 1003 1055 "controllerRemoved": "Controller removed successfully", 1004 1056 "failedToAddController": "Failed to add controller",
+47 -1
frontend/src/locales/fi.json
··· 703 703 "signingInAs": "Kirjaudutaan käyttäjänä:", 704 704 "permissionsRequested": "Pyydetyt oikeudet", 705 705 "required": "Vaaditaan", 706 - "rememberChoiceLabel": "Muista valintani tälle sovellukselle" 706 + "rememberChoiceLabel": "Muista valintani tälle sovellukselle", 707 + "scopes": { 708 + "atproto": { 709 + "name": "Täysi käyttöoikeus", 710 + "description": "Täysi oikeus lukea, kirjoittaa ja hallita tätä tiliä" 711 + }, 712 + "atprotoWithGranular": { 713 + "name": "AT Protocol -käyttöoikeus", 714 + "description": "AT Protocol -peruslaajuus (oikeudet määräytyvät alla valittujen vaihtoehtojen mukaan)" 715 + }, 716 + "transitionGeneric": { 717 + "name": "Siirtymäkäyttöoikeus", 718 + "description": "Yleinen siirtymälaajuus yhteensopivuutta varten" 719 + }, 720 + "transitionChat": { 721 + "name": "Chat-käyttöoikeus", 722 + "description": "Pääsy Bluesky-chat-ominaisuuksiin" 723 + }, 724 + "transitionEmail": { 725 + "name": "Sähköpostikäyttöoikeus", 726 + "description": "Lue tilisi sähköpostiosoite" 727 + }, 728 + "repoCreate": { 729 + "name": "Luo tietueita", 730 + "description": "Luo uusia tietueita tietovarastoosi" 731 + }, 732 + "repoUpdate": { 733 + "name": "Päivitä tietueita", 734 + "description": "Päivitä olemassa olevia tietueita tietovarastossasi" 735 + }, 736 + "repoDelete": { 737 + "name": "Poista tietueita", 738 + "description": "Poista tietueita tietovarastostasi" 739 + }, 740 + "blobAll": { 741 + "name": "Lataa mediaa", 742 + "description": "Lataa kuvia, videoita ja muita mediatiedostoja" 743 + }, 744 + "repoFull": { 745 + "name": "Täysi tietovarastokäyttö", 746 + "description": "Täysi luku- ja kirjoitusoikeus kaikkiin tietovaraston tietueisiin" 747 + }, 748 + "accountManage": { 749 + "name": "Hallitse tiliä", 750 + "description": "Hallitse tilin asetuksia ja asetuksia" 751 + } 752 + } 707 753 }, 708 754 "accounts": { 709 755 "title": "Valitse tili",
+47 -1
frontend/src/locales/ja.json
··· 696 696 "signingInAs": "サインイン中のアカウント:", 697 697 "permissionsRequested": "リクエストされた権限", 698 698 "required": "必須", 699 - "rememberChoiceLabel": "このアプリに対する選択を記憶する" 699 + "rememberChoiceLabel": "このアプリに対する選択を記憶する", 700 + "scopes": { 701 + "atproto": { 702 + "name": "フルアクセス", 703 + "description": "このアカウントの読み取り、書き込み、管理へのフルアクセス" 704 + }, 705 + "atprotoWithGranular": { 706 + "name": "AT Protocol アクセス", 707 + "description": "AT Protocol 基本スコープ(権限は以下で選択したオプションによって決まります)" 708 + }, 709 + "transitionGeneric": { 710 + "name": "移行アクセス", 711 + "description": "互換性のための汎用移行スコープ" 712 + }, 713 + "transitionChat": { 714 + "name": "チャットアクセス", 715 + "description": "Blueskyチャット機能へのアクセス" 716 + }, 717 + "transitionEmail": { 718 + "name": "メールアクセス", 719 + "description": "アカウントのメールアドレスを読み取る" 720 + }, 721 + "repoCreate": { 722 + "name": "レコード作成", 723 + "description": "リポジトリに新しいレコードを作成" 724 + }, 725 + "repoUpdate": { 726 + "name": "レコード更新", 727 + "description": "リポジトリの既存レコードを更新" 728 + }, 729 + "repoDelete": { 730 + "name": "レコード削除", 731 + "description": "リポジトリからレコードを削除" 732 + }, 733 + "blobAll": { 734 + "name": "メディアアップロード", 735 + "description": "画像、動画、その他のメディアファイルをアップロード" 736 + }, 737 + "repoFull": { 738 + "name": "リポジトリフルアクセス", 739 + "description": "すべてのリポジトリレコードへのフル読み書きアクセス" 740 + }, 741 + "accountManage": { 742 + "name": "アカウント管理", 743 + "description": "アカウント設定と設定を管理" 744 + } 745 + } 700 746 }, 701 747 "accounts": { 702 748 "title": "アカウントを選択",
+47 -1
frontend/src/locales/ko.json
··· 696 696 "signingInAs": "로그인 계정:", 697 697 "permissionsRequested": "요청된 권한", 698 698 "required": "필수", 699 - "rememberChoiceLabel": "이 앱에 대한 선택 기억하기" 699 + "rememberChoiceLabel": "이 앱에 대한 선택 기억하기", 700 + "scopes": { 701 + "atproto": { 702 + "name": "전체 액세스", 703 + "description": "이 계정을 읽고, 쓰고, 관리하는 전체 액세스" 704 + }, 705 + "atprotoWithGranular": { 706 + "name": "AT Protocol 액세스", 707 + "description": "AT Protocol 기본 범위 (권한은 아래 선택한 옵션에 의해 결정됨)" 708 + }, 709 + "transitionGeneric": { 710 + "name": "전환 액세스", 711 + "description": "호환성을 위한 일반 전환 범위" 712 + }, 713 + "transitionChat": { 714 + "name": "채팅 액세스", 715 + "description": "Bluesky 채팅 기능 액세스" 716 + }, 717 + "transitionEmail": { 718 + "name": "이메일 액세스", 719 + "description": "계정 이메일 주소 읽기" 720 + }, 721 + "repoCreate": { 722 + "name": "레코드 생성", 723 + "description": "저장소에 새 레코드 생성" 724 + }, 725 + "repoUpdate": { 726 + "name": "레코드 업데이트", 727 + "description": "저장소의 기존 레코드 업데이트" 728 + }, 729 + "repoDelete": { 730 + "name": "레코드 삭제", 731 + "description": "저장소에서 레코드 삭제" 732 + }, 733 + "blobAll": { 734 + "name": "미디어 업로드", 735 + "description": "이미지, 비디오 및 기타 미디어 파일 업로드" 736 + }, 737 + "repoFull": { 738 + "name": "전체 저장소 액세스", 739 + "description": "모든 저장소 레코드에 대한 전체 읽기 및 쓰기 액세스" 740 + }, 741 + "accountManage": { 742 + "name": "계정 관리", 743 + "description": "계정 설정 및 환경설정 관리" 744 + } 745 + } 700 746 }, 701 747 "accounts": { 702 748 "title": "계정 선택",
+47 -1
frontend/src/locales/sv.json
··· 696 696 "signingInAs": "Loggar in som:", 697 697 "permissionsRequested": "Begärda behörigheter", 698 698 "required": "Krävs", 699 - "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation" 699 + "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation", 700 + "scopes": { 701 + "atproto": { 702 + "name": "Full åtkomst", 703 + "description": "Full åtkomst för att läsa, skriva och hantera detta konto" 704 + }, 705 + "atprotoWithGranular": { 706 + "name": "AT Protocol-åtkomst", 707 + "description": "AT Protocol basomfattning (behörigheter bestäms av valda alternativ nedan)" 708 + }, 709 + "transitionGeneric": { 710 + "name": "Övergångsåtkomst", 711 + "description": "Generisk övergångsomfattning för kompatibilitet" 712 + }, 713 + "transitionChat": { 714 + "name": "Chattåtkomst", 715 + "description": "Åtkomst till Bluesky-chattfunktioner" 716 + }, 717 + "transitionEmail": { 718 + "name": "E-poståtkomst", 719 + "description": "Läs din kontots e-postadress" 720 + }, 721 + "repoCreate": { 722 + "name": "Skapa poster", 723 + "description": "Skapa nya poster i ditt arkiv" 724 + }, 725 + "repoUpdate": { 726 + "name": "Uppdatera poster", 727 + "description": "Uppdatera befintliga poster i ditt arkiv" 728 + }, 729 + "repoDelete": { 730 + "name": "Ta bort poster", 731 + "description": "Ta bort poster från ditt arkiv" 732 + }, 733 + "blobAll": { 734 + "name": "Ladda upp media", 735 + "description": "Ladda upp bilder, videor och andra mediefiler" 736 + }, 737 + "repoFull": { 738 + "name": "Full arkivåtkomst", 739 + "description": "Full läs- och skrivåtkomst till alla arkivposter" 740 + }, 741 + "accountManage": { 742 + "name": "Hantera konto", 743 + "description": "Hantera kontoinställningar och preferenser" 744 + } 745 + } 700 746 }, 701 747 "accounts": { 702 748 "title": "Välj konto",
+47 -1
frontend/src/locales/zh.json
··· 696 696 "signingInAs": "登录账户:", 697 697 "permissionsRequested": "请求的权限", 698 698 "required": "必需", 699 - "rememberChoiceLabel": "记住对此应用的授权选择" 699 + "rememberChoiceLabel": "记住对此应用的授权选择", 700 + "scopes": { 701 + "atproto": { 702 + "name": "完全访问", 703 + "description": "完全访问权限以读取、写入和管理此账户" 704 + }, 705 + "atprotoWithGranular": { 706 + "name": "AT Protocol 访问", 707 + "description": "AT Protocol 基础范围(权限由下方选择的选项决定)" 708 + }, 709 + "transitionGeneric": { 710 + "name": "过渡访问", 711 + "description": "用于兼容性的通用过渡范围" 712 + }, 713 + "transitionChat": { 714 + "name": "聊天访问", 715 + "description": "访问 Bluesky 聊天功能" 716 + }, 717 + "transitionEmail": { 718 + "name": "邮箱访问", 719 + "description": "读取您的账户邮箱地址" 720 + }, 721 + "repoCreate": { 722 + "name": "创建记录", 723 + "description": "在您的仓库中创建新记录" 724 + }, 725 + "repoUpdate": { 726 + "name": "更新记录", 727 + "description": "更新您仓库中的现有记录" 728 + }, 729 + "repoDelete": { 730 + "name": "删除记录", 731 + "description": "从您的仓库中删除记录" 732 + }, 733 + "blobAll": { 734 + "name": "上传媒体", 735 + "description": "上传图片、视频和其他媒体文件" 736 + }, 737 + "repoFull": { 738 + "name": "完全仓库访问", 739 + "description": "对所有仓库记录的完全读写访问权限" 740 + }, 741 + "accountManage": { 742 + "name": "管理账户", 743 + "description": "管理账户设置和偏好" 744 + } 745 + } 700 746 }, 701 747 "accounts": { 702 748 "title": "选择账户",
+28 -8
frontend/src/routes/ActAs.svelte
··· 1 1 <script lang="ts"> 2 - import { getAuthState, logout } from '../lib/auth.svelte' 2 + import { getAuthState } from '../lib/auth.svelte' 3 3 import { navigate, routes, getFullUrl } from '../lib/router.svelte' 4 - import { generateCodeVerifier, generateCodeChallenge, saveOAuthState, generateState } from '../lib/oauth' 4 + import { generateCodeVerifier, generateCodeChallenge, saveOAuthState, generateState, createDPoPProofForRequest } from '../lib/oauth' 5 5 import { _ } from '../lib/i18n' 6 6 import type { Session } from '../lib/types/api' 7 7 ··· 70 70 return 71 71 } 72 72 73 - await logout() 74 - 75 73 const hostname = window.location.origin 76 74 const state = generateState() 77 75 const codeVerifier = generateCodeVerifier() ··· 83 81 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 84 82 body: new URLSearchParams({ 85 83 client_id: `${hostname}/oauth/client-metadata.json`, 86 - redirect_uri: `${hostname}/`, 84 + redirect_uri: `${hostname}/app/`, 87 85 response_type: 'code', 88 86 scope: 'atproto', 89 87 state: state, ··· 100 98 } 101 99 102 100 const parData = await parResponse.json() 103 - if (parData.request_uri) { 104 - window.location.href = `/app/oauth/login?request_uri=${encodeURIComponent(parData.request_uri)}` 105 - } else { 101 + if (!parData.request_uri) { 106 102 error = $_('actAs.invalidResponse') 103 + loading = false 104 + return 105 + } 106 + 107 + const authUrl = `${window.location.origin}/oauth/delegation/auth-token` 108 + const dpopProof = await createDPoPProofForRequest('POST', authUrl, session!.accessJwt) 109 + const authResponse = await fetch('/oauth/delegation/auth-token', { 110 + method: 'POST', 111 + headers: { 112 + 'Content-Type': 'application/json', 113 + 'Authorization': `DPoP ${session!.accessJwt}`, 114 + 'DPoP': dpopProof 115 + }, 116 + body: JSON.stringify({ 117 + request_uri: parData.request_uri, 118 + delegated_did: did 119 + }) 120 + }) 121 + 122 + const authData = await authResponse.json() 123 + if (authData.success && authData.redirect_uri) { 124 + window.location.href = authData.redirect_uri 125 + } else { 126 + error = authData.error || $_('actAs.failedToInitiate') 107 127 loading = false 108 128 } 109 129 } catch (e) {
+99 -2
frontend/src/routes/Controllers.svelte
··· 55 55 let addControllerDid = $state('') 56 56 let addControllerScopes = $state('atproto') 57 57 let addingController = $state(false) 58 + let addControllerConfirmed = $state(false) 58 59 59 60 let showCreateDelegated = $state(false) 60 61 let newDelegatedHandle = $state('') ··· 151 152 toast.success($_('delegation.controllerAdded')) 152 153 addControllerDid = '' 153 154 addControllerScopes = 'atproto' 155 + addControllerConfirmed = false 154 156 showAddController = false 155 157 await loadControllers() 156 158 } catch (e) { ··· 295 297 {:else if showAddController} 296 298 <div class="form-card"> 297 299 <h3>{$_('delegation.addController')}</h3> 300 + 301 + <div class="warning-box"> 302 + <div class="warning-header"> 303 + <svg class="warning-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 304 + <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> 305 + <line x1="12" y1="9" x2="12" y2="13"></line> 306 + <line x1="12" y1="17" x2="12.01" y2="17"></line> 307 + </svg> 308 + <span>{$_('delegation.addControllerWarningTitle')}</span> 309 + </div> 310 + <p class="warning-text">{$_('delegation.addControllerWarningText')}</p> 311 + <ul class="warning-bullets"> 312 + <li>{$_('delegation.addControllerWarningBullet1')}</li> 313 + <li>{$_('delegation.addControllerWarningBullet2')}</li> 314 + <li>{$_('delegation.addControllerWarningBullet3')}</li> 315 + </ul> 316 + </div> 317 + 298 318 <div class="field"> 299 319 <label for="controllerDid">{$_('delegation.controllerDid')}</label> 300 320 <input ··· 313 333 {/each} 314 334 </select> 315 335 </div> 336 + <label class="confirm-checkbox"> 337 + <input type="checkbox" bind:checked={addControllerConfirmed} disabled={addingController} /> 338 + <span>{$_('delegation.addControllerConfirm')}</span> 339 + </label> 316 340 <div class="form-actions"> 317 - <button class="ghost" onclick={() => showAddController = false} disabled={addingController}> 341 + <button class="ghost" onclick={() => { showAddController = false; addControllerConfirmed = false }} disabled={addingController}> 318 342 {$_('common.cancel')} 319 343 </button> 320 - <button onclick={addController} disabled={addingController || !addControllerDid.trim()}> 344 + <button onclick={addController} disabled={addingController || !addControllerDid.trim() || !addControllerConfirmed}> 321 345 {addingController ? $_('delegation.adding') : $_('delegation.addController')} 322 346 </button> 323 347 </div> ··· 618 642 619 643 .form-card h3 { 620 644 margin: 0 0 var(--space-4) 0; 645 + } 646 + 647 + .warning-box { 648 + background: var(--warning-bg, #fef3c7); 649 + border: 1px solid var(--warning-border, #f59e0b); 650 + border-radius: var(--radius-md); 651 + padding: var(--space-4); 652 + margin-bottom: var(--space-5); 653 + } 654 + 655 + .warning-header { 656 + display: flex; 657 + align-items: center; 658 + gap: var(--space-2); 659 + font-weight: var(--font-semibold); 660 + color: var(--warning-text, #92400e); 661 + margin-bottom: var(--space-2); 662 + } 663 + 664 + .warning-icon { 665 + width: 20px; 666 + height: 20px; 667 + flex-shrink: 0; 668 + stroke: var(--warning-text, #92400e); 669 + } 670 + 671 + .warning-text { 672 + margin: 0 0 var(--space-3) 0; 673 + color: var(--warning-text, #92400e); 674 + font-size: var(--text-sm); 675 + line-height: 1.5; 676 + } 677 + 678 + .warning-bullets { 679 + margin: 0; 680 + padding-left: var(--space-5); 681 + color: var(--warning-text, #92400e); 682 + font-size: var(--text-sm); 683 + line-height: 1.6; 684 + } 685 + 686 + .warning-bullets li { 687 + margin-bottom: var(--space-1); 688 + } 689 + 690 + .warning-bullets li:last-child { 691 + margin-bottom: 0; 692 + } 693 + 694 + .confirm-checkbox { 695 + display: flex; 696 + align-items: flex-start; 697 + gap: var(--space-2); 698 + cursor: pointer; 699 + padding: var(--space-3); 700 + background: var(--bg-tertiary); 701 + border: 1px solid var(--border-color); 702 + border-radius: var(--radius-md); 703 + margin-bottom: var(--space-4); 704 + } 705 + 706 + .confirm-checkbox input { 707 + width: 18px; 708 + height: 18px; 709 + flex-shrink: 0; 710 + margin-top: 2px; 711 + } 712 + 713 + .confirm-checkbox span { 714 + font-size: var(--text-sm); 715 + font-weight: var(--font-medium); 716 + color: var(--text-primary); 717 + line-height: 1.4; 621 718 } 622 719 623 720 .field {
+86 -10
frontend/src/routes/OAuthConsent.svelte
··· 11 11 granted: boolean | null 12 12 } 13 13 14 + const SCOPE_LOCALE_MAP: Record<string, string> = { 15 + 'atproto': 'atproto', 16 + 'transition:generic': 'transitionGeneric', 17 + 'transition:chat.bsky': 'transitionChat', 18 + 'transition:email': 'transitionEmail', 19 + 'repo:*?action=create': 'repoCreate', 20 + 'repo:*?action=update': 'repoUpdate', 21 + 'repo:*?action=delete': 'repoDelete', 22 + 'blob:*/*': 'blobAll', 23 + 'repo:*': 'repoFull', 24 + 'account:*?action=manage': 'accountManage', 25 + } 26 + 27 + function isGranularScope(scope: string): boolean { 28 + return scope.startsWith('repo:') || 29 + scope.startsWith('blob') || 30 + scope.startsWith('rpc:') || 31 + scope.startsWith('account:') || 32 + scope.startsWith('identity:') 33 + } 34 + 14 35 interface ConsentData { 15 36 request_uri: string 16 37 client_id: string ··· 20 41 scopes: ScopeInfo[] 21 42 show_consent: boolean 22 43 did: string 44 + handle?: string 23 45 is_delegation?: boolean 24 46 controller_did?: string 25 47 controller_handle?: string ··· 147 169 scopeSelections[scope] = !scopeSelections[scope] 148 170 } 149 171 150 - function groupScopesByCategory(scopes: ScopeInfo[]): Record<string, ScopeInfo[]> { 151 - return scopes.reduce( 152 - (groups, scope) => ({ 153 - ...groups, 154 - [scope.category]: [...(groups[scope.category] ?? []), scope], 172 + const CATEGORY_ORDER = [ 173 + 'Core Access', 174 + 'Transition', 175 + 'Account', 176 + 'Repository', 177 + 'Media', 178 + 'API Access', 179 + 'Reference', 180 + 'Other' 181 + ] 182 + 183 + function groupScopesByCategory(scopes: ScopeInfo[]): [string, ScopeInfo[]][] { 184 + const groups = scopes.reduce( 185 + (acc, scope) => ({ 186 + ...acc, 187 + [scope.category]: [...(acc[scope.category] ?? []), scope], 155 188 }), 156 189 {} as Record<string, ScopeInfo[]> 157 190 ) 191 + return Object.entries(groups).sort(([a], [b]) => { 192 + const aIndex = CATEGORY_ORDER.indexOf(a) 193 + const bIndex = CATEGORY_ORDER.indexOf(b) 194 + const aOrder = aIndex === -1 ? CATEGORY_ORDER.length : aIndex 195 + const bOrder = bIndex === -1 ? CATEGORY_ORDER.length : bIndex 196 + return aOrder - bOrder 197 + }) 158 198 } 159 199 160 200 $effect(() => { ··· 171 211 } 172 212 }) 173 213 174 - let scopeGroups = $derived(consentData ? groupScopesByCategory(consentData.scopes) : {}) 214 + let scopeGroups = $derived(consentData ? groupScopesByCategory(consentData.scopes) : []) 215 + let hasGranularScopes = $derived(consentData?.scopes.some(s => isGranularScope(s.scope)) ?? false) 216 + 217 + function getLocalizedScopeName(scope: ScopeInfo): string { 218 + const localeKey = SCOPE_LOCALE_MAP[scope.scope] 219 + if (!localeKey) return scope.display_name 220 + 221 + if (scope.scope === 'atproto' && hasGranularScopes) { 222 + const localized = $_(`oauth.consent.scopes.atprotoWithGranular.name`) 223 + return localized !== `oauth.consent.scopes.atprotoWithGranular.name` ? localized : scope.display_name 224 + } 225 + 226 + const localized = $_(`oauth.consent.scopes.${localeKey}.name`) 227 + return localized !== `oauth.consent.scopes.${localeKey}.name` ? localized : scope.display_name 228 + } 229 + 230 + function getLocalizedScopeDescription(scope: ScopeInfo): string { 231 + const localeKey = SCOPE_LOCALE_MAP[scope.scope] 232 + if (!localeKey) return scope.description 233 + 234 + if (scope.scope === 'atproto' && hasGranularScopes) { 235 + const localized = $_(`oauth.consent.scopes.atprotoWithGranular.description`) 236 + return localized !== `oauth.consent.scopes.atprotoWithGranular.description` ? localized : scope.description 237 + } 238 + 239 + const localized = $_(`oauth.consent.scopes.${localeKey}.description`) 240 + return localized !== `oauth.consent.scopes.${localeKey}.description` ? localized : scope.description 241 + } 175 242 </script> 176 243 177 244 <div class="consent-container"> ··· 244 311 {/if} 245 312 {:else} 246 313 <span class="label">{$_('oauth.consent.signingInAs')}</span> 314 + {#if consentData.handle} 315 + <span class="handle">@{consentData.handle}</span> 316 + {/if} 247 317 <span class="did">{consentData.did}</span> 248 318 {/if} 249 319 </div> ··· 262 332 </div> 263 333 </div> 264 334 {:else} 265 - {#each Object.entries(scopeGroups) as [category, scopes]} 335 + {#each scopeGroups as [category, scopes]} 266 336 <div class="scope-group"> 267 337 <h3 class="category-title">{category}</h3> 268 338 {#each scopes as scope} ··· 274 344 onchange={() => handleScopeToggle(scope.scope)} 275 345 /> 276 346 <div class="scope-info"> 277 - <span class="scope-name">{scope.display_name}</span> 278 - <span class="scope-description">{scope.description}</span> 347 + <span class="scope-name">{getLocalizedScopeName(scope)}</span> 348 + <span class="scope-description">{getLocalizedScopeDescription(scope)}</span> 279 349 {#if scope.required} 280 350 <span class="required-badge">{$_('oauth.consent.required')}</span> 281 351 {/if} ··· 419 489 .account-info .did { 420 490 font-family: monospace; 421 491 font-size: var(--text-sm); 422 - color: var(--text-primary); 492 + color: var(--text-secondary); 423 493 word-break: break-all; 494 + } 495 + 496 + .account-info .handle { 497 + font-size: var(--text-base); 498 + font-weight: var(--font-medium); 499 + color: var(--text-primary); 424 500 } 425 501 426 502 .delegation-badge {
+5 -1
frontend/src/routes/OAuthLogin.svelte
··· 1 1 <script lang="ts"> 2 2 import { navigate, routes, getFullUrl } from '../lib/router.svelte' 3 3 import { _ } from '../lib/i18n' 4 + import { startOAuthLogin } from '../lib/oauth' 4 5 import { 5 6 prepareRequestOptions, 6 7 serializeAssertionResponse, ··· 97 98 userDid = data.did || null 98 99 securityStatusChecked = true 99 100 100 - if (!hasPassword && !hasPasskeys && isDelegated && data.did) { 101 + if (isDelegated && data.did) { 101 102 const requestUri = getRequestUri() 102 103 if (requestUri) { 103 104 navigate(routes.oauthDelegation, { params: { request_uri: requestUri, delegated_did: data.did } }) 105 + return 106 + } else { 107 + await startOAuthLogin(username) 104 108 return 105 109 } 106 110 }
+10 -3
frontend/src/routes/Register.svelte
··· 4 4 import { _ } from '../lib/i18n' 5 5 import { 6 6 createRegistrationFlow, 7 + restoreRegistrationFlow, 7 8 VerificationStep, 8 9 KeyChoiceStep, 9 10 DidDocStep, ··· 45 46 46 47 async function loadServerInfo() { 47 48 try { 48 - serverInfo = await api.describeServer() 49 - const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname 50 - flow = createRegistrationFlow('password', hostname) 49 + const restored = restoreRegistrationFlow() 50 + if (restored && restored.state.mode === 'password') { 51 + flow = restored 52 + serverInfo = await api.describeServer() 53 + } else { 54 + serverInfo = await api.describeServer() 55 + const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname 56 + flow = createRegistrationFlow('password', hostname) 57 + } 51 58 } catch (e) { 52 59 console.error('Failed to load server info:', e) 53 60 } finally {
+12 -5
frontend/src/routes/RegisterPasskey.svelte
··· 4 4 import { _ } from '../lib/i18n' 5 5 import { 6 6 createRegistrationFlow, 7 + restoreRegistrationFlow, 7 8 VerificationStep, 8 9 KeyChoiceStep, 9 10 DidDocStep, ··· 12 13 import { 13 14 prepareCreationOptions, 14 15 serializeAttestationResponse, 15 - type PublicKeyCredentialCreationOptionsJSON, 16 + type WebAuthnCreationOptionsResponse, 16 17 } from '../lib/webauthn' 17 18 import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte' 18 19 ··· 51 52 52 53 async function loadServerInfo() { 53 54 try { 54 - serverInfo = await api.describeServer() 55 - const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname 56 - flow = createRegistrationFlow('passkey', hostname) 55 + const restored = restoreRegistrationFlow() 56 + if (restored && restored.state.mode === 'passkey') { 57 + flow = restored 58 + serverInfo = await api.describeServer() 59 + } else { 60 + serverInfo = await api.describeServer() 61 + const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname 62 + flow = createRegistrationFlow('passkey', hostname) 63 + } 57 64 } catch (e) { 58 65 console.error('Failed to load server info:', e) 59 66 } finally { ··· 127 134 passkeyName || undefined 128 135 ) 129 136 130 - const publicKeyOptions = prepareCreationOptions({ publicKey: options as unknown as PublicKeyCredentialCreationOptionsJSON }) 137 + const publicKeyOptions = prepareCreationOptions(options as unknown as WebAuthnCreationOptionsResponse) 131 138 const credential = await navigator.credentials.create({ 132 139 publicKey: publicKeyOptions 133 140 })