From 8f595b5ffb4bb2681a95018eb2447bc6c2277e9c Mon Sep 17 00:00:00 2001 From: lewis Date: Sat, 10 Jan 2026 21:13:41 +0200 Subject: [PATCH] Separate crates for separate concerns --- ...7cdf535cd70e09fde0a8a28249df0070eb2fc.json | 22 +++ ...e2e5feaa9c59c38ec9175568abdacda167107.json | 15 ++ ...445b65c8cc8c723baca221d85f5e4f2478b99.json | 22 +++ ...fce2209635090252ac3692823450431d03dc6.json | 22 +++ ...4abcf8bc8268a348d3be0da9840d1708d20b5.json | 14 ++ ...79a3c411917144a011f50849b737130b24dbe.json | 54 ++++++ ...70b3360a3ac71e649b293efb88d92c3254068.json | 22 +++ ...813f13f5892f653128469be727b686e6a0f0a.json | 28 +++ ...294cd58f64b4fb431b54b5deda13d64525e88.json | 28 +++ ...9580ff03b717586c4ca2d5343709e2dac86b6.json | 22 +++ ...92881ba343c73a9a6e513e205c801c5943ec0.json | 28 +++ ...b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json | 22 +++ ...d3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json | 22 +++ ...6f6fba9fe338e4a6a10636b5389d426df1631.json | 22 +++ ...413743ba168d1e0d8b85566711e54d4048f81.json | 28 +++ ...94246ead214ca794760d3fe77bb5cf4f27be5.json | 22 +++ ...2ac0f84c73de8c244ba4560f004ee3f4b7002.json | 28 +++ ...eadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json | 108 +++++++++++ ...d5171e70fa25e3b8393e142cebcbff752f0f5.json | 34 ++++ ...7b633cec7287c1cc177f0e1d47ec6571564d5.json | 22 +++ ...cae4825dec587b1387f0fee401458aea2a2e5.json | 60 +++++++ ...b29f7abfe18ee6f559bd94ab16274b1cfdfee.json | 22 +++ ...ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json | 22 +++ ...90c41b5e5534a3db9c137e3ef8e66fdf0a87.json} | 6 +- ...a3b40041b043bae57e95002d4bf5df46a4ab4.json | 14 ++ ...80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json | 22 +++ ...5a1a627809f444b0faff7e611d985f31b90e9.json | 22 +++ ...bf8b34ca6a2246c20fc96f76f0e95530762a7.json | 22 +++ ...fac26435b0feec65cf1c56f851d1c4d6b1814.json | 14 ++ ...03699bb3a90f39bc9c4c0f738a37827e8f382.json | 28 +++ Cargo.lock | 168 ++++++++++++++++++ Cargo.toml | 156 +++++++++------- crates/tranquil-auth/Cargo.toml | 25 +++ crates/tranquil-auth/src/lib.rs | 29 +++ .../tranquil-auth/src}/token.rs | 31 ++-- .../auth => crates/tranquil-auth/src}/totp.rs | 43 ++--- crates/tranquil-auth/src/types.rs | 63 +++++++ .../tranquil-auth/src}/verify.rs | 52 ++---- crates/tranquil-cache/Cargo.toml | 14 ++ .../tranquil-cache/src/lib.rs | 44 ++--- crates/tranquil-comms/Cargo.toml | 18 ++ crates/tranquil-comms/src/lib.rs | 13 ++ .../tranquil-comms/src}/locale.rs | 0 .../tranquil-comms/src}/sender.rs | 0 .../tranquil-comms/src}/types.rs | 12 +- crates/tranquil-crypto/Cargo.toml | 18 ++ crates/tranquil-crypto/src/encryption.rs | 78 ++++++++ .../tranquil-crypto/src/jwk.rs | 0 crates/tranquil-crypto/src/lib.rs | 19 ++ crates/tranquil-crypto/src/signing.rs | 150 ++++++++++++++++ crates/tranquil-infra/Cargo.toml | 13 ++ crates/tranquil-infra/src/lib.rs | 58 ++++++ crates/tranquil-oauth/Cargo.toml | 25 +++ .../tranquil-oauth/src}/client.rs | 56 +++--- .../tranquil-oauth/src}/dpop.rs | 4 +- .../tranquil-oauth/src}/error.rs | 14 +- crates/tranquil-oauth/src/lib.rs | 17 ++ .../tranquil-oauth/src}/types.rs | 0 crates/tranquil-pds/.sqlx | 1 + crates/tranquil-pds/Cargo.toml | 92 ++++++++++ crates/tranquil-pds/migrations | 1 + .../tranquil-pds/src}/api/actor/mod.rs | 0 .../src}/api/actor/preferences.rs | 0 .../src}/api/admin/account/delete.rs | 9 +- .../src}/api/admin/account/email.rs | 0 .../src}/api/admin/account/info.rs | 0 .../src}/api/admin/account/mod.rs | 0 .../src}/api/admin/account/search.rs | 0 .../src}/api/admin/account/update.rs | 9 +- .../tranquil-pds/src}/api/admin/config.rs | 0 .../tranquil-pds/src}/api/admin/invite.rs | 0 .../tranquil-pds/src}/api/admin/mod.rs | 0 .../src}/api/admin/server_stats.rs | 0 .../tranquil-pds/src}/api/admin/status.rs | 9 +- .../tranquil-pds/src}/api/age_assurance.rs | 0 .../tranquil-pds/src}/api/backup.rs | 0 .../tranquil-pds/src}/api/delegation.rs | 6 +- {src => crates/tranquil-pds/src}/api/error.rs | 0 .../tranquil-pds/src}/api/identity/account.rs | 8 +- .../tranquil-pds/src}/api/identity/did.rs | 3 +- .../tranquil-pds/src}/api/identity/mod.rs | 0 .../tranquil-pds/src}/api/identity/plc/mod.rs | 0 .../src}/api/identity/plc/request.rs | 0 .../src}/api/identity/plc/sign.rs | 0 .../src}/api/identity/plc/submit.rs | 0 {src => crates/tranquil-pds/src}/api/mod.rs | 0 .../tranquil-pds/src}/api/moderation/mod.rs | 0 .../src}/api/notification_prefs.rs | 0 {src => crates/tranquil-pds/src}/api/proxy.rs | 3 +- .../tranquil-pds/src}/api/proxy_client.rs | 0 .../tranquil-pds/src}/api/repo/blob.rs | 0 .../tranquil-pds/src}/api/repo/import.rs | 0 .../tranquil-pds/src}/api/repo/meta.rs | 0 .../tranquil-pds/src}/api/repo/mod.rs | 0 .../src}/api/repo/record/batch.rs | 9 +- .../src}/api/repo/record/delete.rs | 0 .../tranquil-pds/src}/api/repo/record/mod.rs | 0 .../tranquil-pds/src}/api/repo/record/read.rs | 14 +- .../src}/api/repo/record/utils.rs | 6 +- .../src}/api/repo/record/validation.rs | 0 .../src}/api/repo/record/write.rs | 0 .../tranquil-pds/src}/api/responses.rs | 0 .../src}/api/server/account_status.rs | 5 +- .../src}/api/server/app_password.rs | 0 .../tranquil-pds/src}/api/server/email.rs | 0 .../tranquil-pds/src}/api/server/invite.rs | 0 .../tranquil-pds/src}/api/server/logo.rs | 0 .../tranquil-pds/src}/api/server/meta.rs | 0 .../tranquil-pds/src}/api/server/migration.rs | 0 .../tranquil-pds/src}/api/server/mod.rs | 0 .../src}/api/server/passkey_account.rs | 10 +- .../tranquil-pds/src}/api/server/passkeys.rs | 0 .../tranquil-pds/src}/api/server/password.rs | 0 .../tranquil-pds/src}/api/server/reauth.rs | 0 .../src}/api/server/service_auth.rs | 0 .../tranquil-pds/src}/api/server/session.rs | 4 +- .../src}/api/server/signing_key.rs | 0 .../tranquil-pds/src}/api/server/totp.rs | 2 +- .../src}/api/server/trusted_devices.rs | 0 .../src}/api/server/verify_email.rs | 0 .../src}/api/server/verify_token.rs | 0 {src => crates/tranquil-pds/src}/api/temp.rs | 0 .../tranquil-pds/src}/api/validation.rs | 0 .../tranquil-pds/src}/api/verification.rs | 0 .../tranquil-pds/src}/appview/mod.rs | 0 .../tranquil-pds/src}/auth/extractor.rs | 0 {src => crates/tranquil-pds/src}/auth/mod.rs | 74 +++----- .../tranquil-pds/src}/auth/scope_check.rs | 2 +- .../tranquil-pds/src}/auth/service.rs | 0 .../src}/auth/verification_token.rs | 0 .../tranquil-pds/src}/auth/webauthn.rs | 0 crates/tranquil-pds/src/cache/mod.rs | 4 + .../tranquil-pds/src}/circuit_breaker.rs | 0 {src => crates/tranquil-pds/src}/comms/mod.rs | 13 +- .../tranquil-pds/src}/comms/service.rs | 36 ++-- {src => crates/tranquil-pds/src}/config.rs | 0 {src => crates/tranquil-pds/src}/crawlers.rs | 0 .../tranquil-pds/src}/delegation/audit.rs | 0 .../tranquil-pds/src}/delegation/db.rs | 0 .../tranquil-pds/src}/delegation/mod.rs | 0 .../tranquil-pds/src}/delegation/scopes.rs | 0 .../tranquil-pds/src}/handle/mod.rs | 0 .../tranquil-pds/src}/handle/reserved.rs | 0 {src => crates/tranquil-pds/src}/image/mod.rs | 0 {src => crates/tranquil-pds/src}/lib.rs | 0 {src => crates/tranquil-pds/src}/main.rs | 0 {src => crates/tranquil-pds/src}/metrics.rs | 0 .../tranquil-pds/src}/moderation/mod.rs | 0 .../tranquil-pds/src}/oauth/db/client.rs | 0 .../tranquil-pds/src}/oauth/db/device.rs | 0 .../tranquil-pds/src}/oauth/db/dpop.rs | 0 .../tranquil-pds/src}/oauth/db/helpers.rs | 0 .../tranquil-pds/src}/oauth/db/mod.rs | 0 .../tranquil-pds/src}/oauth/db/request.rs | 0 .../src}/oauth/db/scope_preference.rs | 0 .../tranquil-pds/src}/oauth/db/token.rs | 0 .../tranquil-pds/src}/oauth/db/two_factor.rs | 0 .../src}/oauth/endpoints/authorize.rs | 3 +- .../src}/oauth/endpoints/delegation.rs | 0 .../src}/oauth/endpoints/metadata.rs | 0 .../tranquil-pds/src}/oauth/endpoints/mod.rs | 0 .../tranquil-pds/src}/oauth/endpoints/par.rs | 7 +- .../src}/oauth/endpoints/token/grants.rs | 9 +- .../src}/oauth/endpoints/token/helpers.rs | 0 .../src}/oauth/endpoints/token/introspect.rs | 0 .../src}/oauth/endpoints/token/mod.rs | 0 .../src}/oauth/endpoints/token/types.rs | 0 crates/tranquil-pds/src/oauth/jwks.rs | 1 + crates/tranquil-pds/src/oauth/mod.rs | 20 +++ crates/tranquil-pds/src/oauth/scopes/mod.rs | 6 + .../tranquil-pds/src}/oauth/verify.rs | 3 +- {src => crates/tranquil-pds/src}/plc/mod.rs | 0 .../tranquil-pds/src}/rate_limit.rs | 0 crates/tranquil-pds/src/repo/mod.rs | 5 + {src => crates/tranquil-pds/src}/scheduled.rs | 4 +- {src => crates/tranquil-pds/src}/state.rs | 0 crates/tranquil-pds/src/storage/mod.rs | 3 + {src => crates/tranquil-pds/src}/sync/blob.rs | 0 {src => crates/tranquil-pds/src}/sync/car.rs | 0 .../tranquil-pds/src}/sync/commit.rs | 0 .../tranquil-pds/src}/sync/crawl.rs | 0 .../tranquil-pds/src}/sync/deprecated.rs | 0 .../tranquil-pds/src}/sync/firehose.rs | 0 .../tranquil-pds/src}/sync/frame.rs | 0 .../tranquil-pds/src}/sync/import.rs | 0 .../tranquil-pds/src}/sync/listener.rs | 0 {src => crates/tranquil-pds/src}/sync/mod.rs | 0 {src => crates/tranquil-pds/src}/sync/repo.rs | 0 .../tranquil-pds/src}/sync/subscribe_repos.rs | 0 {src => crates/tranquil-pds/src}/sync/util.rs | 0 .../tranquil-pds/src}/sync/verify.rs | 0 .../tranquil-pds/src}/sync/verify_tests.rs | 0 crates/tranquil-pds/src/types.rs | 1 + {src => crates/tranquil-pds/src}/util.rs | 0 .../tranquil-pds/src}/validation/mod.rs | 0 .../tranquil-pds/tests}/account_lifecycle.rs | 0 .../tests}/account_notifications.rs | 0 {tests => crates/tranquil-pds/tests}/actor.rs | 0 .../tranquil-pds/tests}/admin_email.rs | 0 .../tranquil-pds/tests}/admin_invite.rs | 0 .../tranquil-pds/tests}/admin_moderation.rs | 0 .../tranquil-pds/tests}/admin_search.rs | 0 .../tranquil-pds/tests}/admin_stats.rs | 0 .../tranquil-pds/tests}/backup.rs | 0 .../tranquil-pds/tests}/banned_words.rs | 0 .../tranquil-pds/tests}/change_password.rs | 0 .../tranquil-pds/tests}/commit_signing.rs | 0 .../tranquil-pds/tests}/common/mod.rs | 0 .../tranquil-pds/tests}/delete_account.rs | 0 .../tranquil-pds/tests}/did_web.rs | 0 .../tranquil-pds/tests}/dpop_unit.rs | 2 +- .../tranquil-pds/tests}/email_update.rs | 0 .../tests}/firehose_validation.rs | 0 .../tranquil-pds/tests}/helpers/mod.rs | 0 .../tranquil-pds/tests}/identity.rs | 0 .../tranquil-pds/tests}/image_processing.rs | 0 .../tests}/import_verification.rs | 0 .../tests}/import_with_verification.rs | 0 .../tranquil-pds/tests}/invite.rs | 0 .../tranquil-pds/tests}/jwt_security.rs | 0 .../tranquil-pds/tests}/lifecycle_record.rs | 0 .../tranquil-pds/tests}/lifecycle_session.rs | 0 .../tranquil-pds/tests}/lifecycle_social.rs | 0 .../tranquil-pds/tests}/moderation.rs | 0 .../tranquil-pds/tests}/notifications.rs | 0 {tests => crates/tranquil-pds/tests}/oauth.rs | 0 .../tests}/oauth_client_metadata.rs | 0 .../tranquil-pds/tests}/oauth_lifecycle.rs | 0 .../tranquil-pds/tests}/oauth_scopes.rs | 0 .../tranquil-pds/tests}/oauth_security.rs | 2 +- .../tranquil-pds/tests}/password_reset.rs | 0 .../tranquil-pds/tests}/plc_migration.rs | 0 .../tranquil-pds/tests}/plc_operations.rs | 0 .../tranquil-pds/tests}/plc_validation.rs | 0 .../tranquil-pds/tests}/rate_limit.rs | 0 .../tranquil-pds/tests}/record_validation.rs | 0 .../tranquil-pds/tests}/repo_batch.rs | 0 .../tranquil-pds/tests}/repo_blob.rs | 0 .../tranquil-pds/tests}/repo_conformance.rs | 0 .../tranquil-pds/tests}/scope_edge_cases.rs | 0 .../tranquil-pds/tests}/security_fixes.rs | 0 .../tranquil-pds/tests}/server.rs | 0 .../tranquil-pds/tests}/session_management.rs | 0 .../tranquil-pds/tests}/signing_key.rs | 0 .../tranquil-pds/tests}/sync_blob.rs | 0 .../tranquil-pds/tests}/sync_conformance.rs | 0 .../tranquil-pds/tests}/sync_deprecated.rs | 0 .../tranquil-pds/tests}/sync_repo.rs | 0 .../tests}/validation_edge_cases.rs | 0 .../tranquil-pds/tests}/verify_live_commit.rs | 0 crates/tranquil-repo/.sqlx | 1 + crates/tranquil-repo/Cargo.toml | 15 ++ .../mod.rs => crates/tranquil-repo/src/lib.rs | 117 +++++++++++- crates/tranquil-scopes/Cargo.toml | 14 ++ .../tranquil-scopes/src}/definitions.rs | 0 .../tranquil-scopes/src}/error.rs | 0 .../tranquil-scopes/src/lib.rs | 5 +- .../tranquil-scopes/src}/parser.rs | 0 .../tranquil-scopes/src}/permission_set.rs | 0 .../tranquil-scopes/src}/permissions.rs | 0 crates/tranquil-storage/Cargo.toml | 17 ++ .../tranquil-storage/src/lib.rs | 109 ++---------- crates/tranquil-types/Cargo.toml | 14 ++ .../tranquil-types/src/lib.rs | 130 +++----------- src/oauth/mod.rs | 16 -- src/repo/tracking.rs | 113 ------------ 266 files changed, 2188 insertions(+), 686 deletions(-) create mode 100644 .sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json create mode 100644 .sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json create mode 100644 .sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json create mode 100644 .sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json create mode 100644 .sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json create mode 100644 .sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json create mode 100644 .sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json create mode 100644 .sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json create mode 100644 .sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json create mode 100644 .sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json create mode 100644 .sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json create mode 100644 .sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json create mode 100644 .sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json create mode 100644 .sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json create mode 100644 .sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json create mode 100644 .sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json create mode 100644 .sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json create mode 100644 .sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json create mode 100644 .sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json create mode 100644 .sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json create mode 100644 .sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json create mode 100644 .sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json create mode 100644 .sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json rename .sqlx/{query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json => query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json} (89%) create mode 100644 .sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json create mode 100644 .sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json create mode 100644 .sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json create mode 100644 .sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json create mode 100644 .sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json create mode 100644 .sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json create mode 100644 crates/tranquil-auth/Cargo.toml create mode 100644 crates/tranquil-auth/src/lib.rs rename {src/auth => crates/tranquil-auth/src}/token.rs (94%) rename {src/auth => crates/tranquil-auth/src}/totp.rs (86%) create mode 100644 crates/tranquil-auth/src/types.rs rename {src/auth => crates/tranquil-auth/src}/verify.rs (95%) create mode 100644 crates/tranquil-cache/Cargo.toml rename src/cache/mod.rs => crates/tranquil-cache/src/lib.rs (88%) create mode 100644 crates/tranquil-comms/Cargo.toml create mode 100644 crates/tranquil-comms/src/lib.rs rename {src/comms => crates/tranquil-comms/src}/locale.rs (100%) rename {src/comms => crates/tranquil-comms/src}/sender.rs (100%) rename {src/comms => crates/tranquil-comms/src}/types.rs (85%) create mode 100644 crates/tranquil-crypto/Cargo.toml create mode 100644 crates/tranquil-crypto/src/encryption.rs rename src/oauth/jwks.rs => crates/tranquil-crypto/src/jwk.rs (100%) create mode 100644 crates/tranquil-crypto/src/lib.rs create mode 100644 crates/tranquil-crypto/src/signing.rs create mode 100644 crates/tranquil-infra/Cargo.toml create mode 100644 crates/tranquil-infra/src/lib.rs create mode 100644 crates/tranquil-oauth/Cargo.toml rename {src/oauth => crates/tranquil-oauth/src}/client.rs (95%) rename {src/oauth => crates/tranquil-oauth/src}/dpop.rs (99%) rename {src/oauth => crates/tranquil-oauth/src}/error.rs (100%) create mode 100644 crates/tranquil-oauth/src/lib.rs rename {src/oauth => crates/tranquil-oauth/src}/types.rs (100%) create mode 120000 crates/tranquil-pds/.sqlx create mode 100644 crates/tranquil-pds/Cargo.toml create mode 120000 crates/tranquil-pds/migrations rename {src => crates/tranquil-pds/src}/api/actor/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/actor/preferences.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/account/delete.rs (96%) rename {src => crates/tranquil-pds/src}/api/admin/account/email.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/account/info.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/account/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/account/search.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/account/update.rs (96%) rename {src => crates/tranquil-pds/src}/api/admin/config.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/invite.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/server_stats.rs (100%) rename {src => crates/tranquil-pds/src}/api/admin/status.rs (98%) rename {src => crates/tranquil-pds/src}/api/age_assurance.rs (100%) rename {src => crates/tranquil-pds/src}/api/backup.rs (100%) rename {src => crates/tranquil-pds/src}/api/delegation.rs (99%) rename {src => crates/tranquil-pds/src}/api/error.rs (100%) rename {src => crates/tranquil-pds/src}/api/identity/account.rs (99%) rename {src => crates/tranquil-pds/src}/api/identity/did.rs (99%) rename {src => crates/tranquil-pds/src}/api/identity/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/identity/plc/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/identity/plc/request.rs (100%) rename {src => crates/tranquil-pds/src}/api/identity/plc/sign.rs (100%) rename {src => crates/tranquil-pds/src}/api/identity/plc/submit.rs (100%) rename {src => crates/tranquil-pds/src}/api/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/moderation/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/notification_prefs.rs (100%) rename {src => crates/tranquil-pds/src}/api/proxy.rs (99%) rename {src => crates/tranquil-pds/src}/api/proxy_client.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/blob.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/import.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/meta.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/record/batch.rs (99%) rename {src => crates/tranquil-pds/src}/api/repo/record/delete.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/record/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/record/read.rs (95%) rename {src => crates/tranquil-pds/src}/api/repo/record/utils.rs (99%) rename {src => crates/tranquil-pds/src}/api/repo/record/validation.rs (100%) rename {src => crates/tranquil-pds/src}/api/repo/record/write.rs (100%) rename {src => crates/tranquil-pds/src}/api/responses.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/account_status.rs (99%) rename {src => crates/tranquil-pds/src}/api/server/app_password.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/email.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/invite.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/logo.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/meta.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/migration.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/mod.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/passkey_account.rs (99%) rename {src => crates/tranquil-pds/src}/api/server/passkeys.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/password.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/reauth.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/service_auth.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/session.rs (99%) rename {src => crates/tranquil-pds/src}/api/server/signing_key.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/totp.rs (99%) rename {src => crates/tranquil-pds/src}/api/server/trusted_devices.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/verify_email.rs (100%) rename {src => crates/tranquil-pds/src}/api/server/verify_token.rs (100%) rename {src => crates/tranquil-pds/src}/api/temp.rs (100%) rename {src => crates/tranquil-pds/src}/api/validation.rs (100%) rename {src => crates/tranquil-pds/src}/api/verification.rs (100%) rename {src => crates/tranquil-pds/src}/appview/mod.rs (100%) rename {src => crates/tranquil-pds/src}/auth/extractor.rs (100%) rename {src => crates/tranquil-pds/src}/auth/mod.rs (92%) rename {src => crates/tranquil-pds/src}/auth/scope_check.rs (98%) rename {src => crates/tranquil-pds/src}/auth/service.rs (100%) rename {src => crates/tranquil-pds/src}/auth/verification_token.rs (100%) rename {src => crates/tranquil-pds/src}/auth/webauthn.rs (100%) create mode 100644 crates/tranquil-pds/src/cache/mod.rs rename {src => crates/tranquil-pds/src}/circuit_breaker.rs (100%) rename {src => crates/tranquil-pds/src}/comms/mod.rs (52%) rename {src => crates/tranquil-pds/src}/comms/service.rs (95%) rename {src => crates/tranquil-pds/src}/config.rs (100%) rename {src => crates/tranquil-pds/src}/crawlers.rs (100%) rename {src => crates/tranquil-pds/src}/delegation/audit.rs (100%) rename {src => crates/tranquil-pds/src}/delegation/db.rs (100%) rename {src => crates/tranquil-pds/src}/delegation/mod.rs (100%) rename {src => crates/tranquil-pds/src}/delegation/scopes.rs (100%) rename {src => crates/tranquil-pds/src}/handle/mod.rs (100%) rename {src => crates/tranquil-pds/src}/handle/reserved.rs (100%) rename {src => crates/tranquil-pds/src}/image/mod.rs (100%) rename {src => crates/tranquil-pds/src}/lib.rs (100%) rename {src => crates/tranquil-pds/src}/main.rs (100%) rename {src => crates/tranquil-pds/src}/metrics.rs (100%) rename {src => crates/tranquil-pds/src}/moderation/mod.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/client.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/device.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/dpop.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/helpers.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/mod.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/request.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/scope_preference.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/token.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/db/two_factor.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/authorize.rs (99%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/delegation.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/metadata.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/mod.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/par.rs (97%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/token/grants.rs (97%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/token/helpers.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/token/introspect.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/token/mod.rs (100%) rename {src => crates/tranquil-pds/src}/oauth/endpoints/token/types.rs (100%) create mode 100644 crates/tranquil-pds/src/oauth/jwks.rs create mode 100644 crates/tranquil-pds/src/oauth/mod.rs create mode 100644 crates/tranquil-pds/src/oauth/scopes/mod.rs rename {src => crates/tranquil-pds/src}/oauth/verify.rs (99%) rename {src => crates/tranquil-pds/src}/plc/mod.rs (100%) rename {src => crates/tranquil-pds/src}/rate_limit.rs (100%) create mode 100644 crates/tranquil-pds/src/repo/mod.rs rename {src => crates/tranquil-pds/src}/scheduled.rs (99%) rename {src => crates/tranquil-pds/src}/state.rs (100%) create mode 100644 crates/tranquil-pds/src/storage/mod.rs rename {src => crates/tranquil-pds/src}/sync/blob.rs (100%) rename {src => crates/tranquil-pds/src}/sync/car.rs (100%) rename {src => crates/tranquil-pds/src}/sync/commit.rs (100%) rename {src => crates/tranquil-pds/src}/sync/crawl.rs (100%) rename {src => crates/tranquil-pds/src}/sync/deprecated.rs (100%) rename {src => crates/tranquil-pds/src}/sync/firehose.rs (100%) rename {src => crates/tranquil-pds/src}/sync/frame.rs (100%) rename {src => crates/tranquil-pds/src}/sync/import.rs (100%) rename {src => crates/tranquil-pds/src}/sync/listener.rs (100%) rename {src => crates/tranquil-pds/src}/sync/mod.rs (100%) rename {src => crates/tranquil-pds/src}/sync/repo.rs (100%) rename {src => crates/tranquil-pds/src}/sync/subscribe_repos.rs (100%) rename {src => crates/tranquil-pds/src}/sync/util.rs (100%) rename {src => crates/tranquil-pds/src}/sync/verify.rs (100%) rename {src => crates/tranquil-pds/src}/sync/verify_tests.rs (100%) create mode 100644 crates/tranquil-pds/src/types.rs rename {src => crates/tranquil-pds/src}/util.rs (100%) rename {src => crates/tranquil-pds/src}/validation/mod.rs (100%) rename {tests => crates/tranquil-pds/tests}/account_lifecycle.rs (100%) rename {tests => crates/tranquil-pds/tests}/account_notifications.rs (100%) rename {tests => crates/tranquil-pds/tests}/actor.rs (100%) rename {tests => crates/tranquil-pds/tests}/admin_email.rs (100%) rename {tests => crates/tranquil-pds/tests}/admin_invite.rs (100%) rename {tests => crates/tranquil-pds/tests}/admin_moderation.rs (100%) rename {tests => crates/tranquil-pds/tests}/admin_search.rs (100%) rename {tests => crates/tranquil-pds/tests}/admin_stats.rs (100%) rename {tests => crates/tranquil-pds/tests}/backup.rs (100%) rename {tests => crates/tranquil-pds/tests}/banned_words.rs (100%) rename {tests => crates/tranquil-pds/tests}/change_password.rs (100%) rename {tests => crates/tranquil-pds/tests}/commit_signing.rs (100%) rename {tests => crates/tranquil-pds/tests}/common/mod.rs (100%) rename {tests => crates/tranquil-pds/tests}/delete_account.rs (100%) rename {tests => crates/tranquil-pds/tests}/did_web.rs (100%) rename {tests => crates/tranquil-pds/tests}/dpop_unit.rs (99%) rename {tests => crates/tranquil-pds/tests}/email_update.rs (100%) rename {tests => crates/tranquil-pds/tests}/firehose_validation.rs (100%) rename {tests => crates/tranquil-pds/tests}/helpers/mod.rs (100%) rename {tests => crates/tranquil-pds/tests}/identity.rs (100%) rename {tests => crates/tranquil-pds/tests}/image_processing.rs (100%) rename {tests => crates/tranquil-pds/tests}/import_verification.rs (100%) rename {tests => crates/tranquil-pds/tests}/import_with_verification.rs (100%) rename {tests => crates/tranquil-pds/tests}/invite.rs (100%) rename {tests => crates/tranquil-pds/tests}/jwt_security.rs (100%) rename {tests => crates/tranquil-pds/tests}/lifecycle_record.rs (100%) rename {tests => crates/tranquil-pds/tests}/lifecycle_session.rs (100%) rename {tests => crates/tranquil-pds/tests}/lifecycle_social.rs (100%) rename {tests => crates/tranquil-pds/tests}/moderation.rs (100%) rename {tests => crates/tranquil-pds/tests}/notifications.rs (100%) rename {tests => crates/tranquil-pds/tests}/oauth.rs (100%) rename {tests => crates/tranquil-pds/tests}/oauth_client_metadata.rs (100%) rename {tests => crates/tranquil-pds/tests}/oauth_lifecycle.rs (100%) rename {tests => crates/tranquil-pds/tests}/oauth_scopes.rs (100%) rename {tests => crates/tranquil-pds/tests}/oauth_security.rs (99%) rename {tests => crates/tranquil-pds/tests}/password_reset.rs (100%) rename {tests => crates/tranquil-pds/tests}/plc_migration.rs (100%) rename {tests => crates/tranquil-pds/tests}/plc_operations.rs (100%) rename {tests => crates/tranquil-pds/tests}/plc_validation.rs (100%) rename {tests => crates/tranquil-pds/tests}/rate_limit.rs (100%) rename {tests => crates/tranquil-pds/tests}/record_validation.rs (100%) rename {tests => crates/tranquil-pds/tests}/repo_batch.rs (100%) rename {tests => crates/tranquil-pds/tests}/repo_blob.rs (100%) rename {tests => crates/tranquil-pds/tests}/repo_conformance.rs (100%) rename {tests => crates/tranquil-pds/tests}/scope_edge_cases.rs (100%) rename {tests => crates/tranquil-pds/tests}/security_fixes.rs (100%) rename {tests => crates/tranquil-pds/tests}/server.rs (100%) rename {tests => crates/tranquil-pds/tests}/session_management.rs (100%) rename {tests => crates/tranquil-pds/tests}/signing_key.rs (100%) rename {tests => crates/tranquil-pds/tests}/sync_blob.rs (100%) rename {tests => crates/tranquil-pds/tests}/sync_conformance.rs (100%) rename {tests => crates/tranquil-pds/tests}/sync_deprecated.rs (100%) rename {tests => crates/tranquil-pds/tests}/sync_repo.rs (100%) rename {tests => crates/tranquil-pds/tests}/validation_edge_cases.rs (100%) rename {tests => crates/tranquil-pds/tests}/verify_live_commit.rs (100%) create mode 120000 crates/tranquil-repo/.sqlx create mode 100644 crates/tranquil-repo/Cargo.toml rename src/repo/mod.rs => crates/tranquil-repo/src/lib.rs (52%) create mode 100644 crates/tranquil-scopes/Cargo.toml rename {src/oauth/scopes => crates/tranquil-scopes/src}/definitions.rs (100%) rename {src/oauth/scopes => crates/tranquil-scopes/src}/error.rs (100%) rename src/oauth/scopes/mod.rs => crates/tranquil-scopes/src/lib.rs (69%) rename {src/oauth/scopes => crates/tranquil-scopes/src}/parser.rs (100%) rename {src/oauth/scopes => crates/tranquil-scopes/src}/permission_set.rs (100%) rename {src/oauth/scopes => crates/tranquil-scopes/src}/permissions.rs (100%) create mode 100644 crates/tranquil-storage/Cargo.toml rename src/storage/mod.rs => crates/tranquil-storage/src/lib.rs (74%) create mode 100644 crates/tranquil-types/Cargo.toml rename src/types.rs => crates/tranquil-types/src/lib.rs (92%) delete mode 100644 src/oauth/mod.rs delete mode 100644 src/repo/tracking.rs diff --git a/.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json b/.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json new file mode 100644 index 0000000..15ba9ce --- /dev/null +++ b/.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc" +} diff --git a/.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json b/.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json new file mode 100644 index 0000000..734848a --- /dev/null +++ b/.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET deactivated_at = $1 WHERE did = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107" +} diff --git a/.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json b/.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json new file mode 100644 index 0000000..9e5e897 --- /dev/null +++ b/.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "body", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99" +} diff --git a/.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json b/.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json new file mode 100644 index 0000000..cf39684 --- /dev/null +++ b/.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6" +} diff --git a/.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json b/.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json new file mode 100644 index 0000000..8d0d055 --- /dev/null +++ b/.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5" +} diff --git a/.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json b/.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json new file mode 100644 index 0000000..7004d69 --- /dev/null +++ b/.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "subject", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "body", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "comms_type: String", + "type_info": { + "Custom": { + "name": "comms_type", + "kind": { + "Enum": [ + "welcome", + "email_verification", + "password_reset", + "email_update", + "account_deletion", + "admin_email", + "plc_operation", + "two_factor_code", + "channel_verification", + "passkey_recovery", + "legacy_login_alert", + "migration_verification" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true, + false, + false + ] + }, + "hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe" +} diff --git a/.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json b/.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json new file mode 100644 index 0000000..b81fee7 --- /dev/null +++ b/.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM users WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068" +} diff --git a/.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json b/.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json new file mode 100644 index 0000000..5c9872f --- /dev/null +++ b/.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT body, metadata FROM comms_queue WHERE user_id = $1 AND comms_type = 'channel_verification' ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "body", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "metadata", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true + ] + }, + "hash": "4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a" +} diff --git a/.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json b/.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json new file mode 100644 index 0000000..e232907 --- /dev/null +++ b/.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88" +} diff --git a/.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json b/.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json new file mode 100644 index 0000000..873f0f7 --- /dev/null +++ b/.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email_verified FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email_verified", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6" +} diff --git a/.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json b/.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json new file mode 100644 index 0000000..32bd1aa --- /dev/null +++ b/.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT k.key_bytes, k.encryption_version\n FROM user_keys k\n JOIN users u ON k.user_id = u.id\n WHERE u.did = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "key_bytes", + "type_info": "Bytea" + }, + { + "ordinal": 1, + "name": "encryption_version", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true + ] + }, + "hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0" +} diff --git a/.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json b/.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json new file mode 100644 index 0000000..296bb04 --- /dev/null +++ b/.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT body FROM comms_queue WHERE user_id = $1 AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "body", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45" +} diff --git a/.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json b/.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json new file mode 100644 index 0000000..af80ee3 --- /dev/null +++ b/.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "subject", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f" +} diff --git a/.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json b/.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json new file mode 100644 index 0000000..416f681 --- /dev/null +++ b/.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT t.token\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631" +} diff --git a/.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json b/.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json new file mode 100644 index 0000000..8d5a9b7 --- /dev/null +++ b/.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81" +} diff --git a/.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json b/.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json new file mode 100644 index 0000000..ef52899 --- /dev/null +++ b/.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "body", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5" +} diff --git a/.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json b/.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json new file mode 100644 index 0000000..178bc9e --- /dev/null +++ b/.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "did", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "public_key_did_key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + false + ] + }, + "hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002" +} diff --git a/.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json b/.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json new file mode 100644 index 0000000..8bcc4f5 --- /dev/null +++ b/.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json @@ -0,0 +1,108 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id, user_id, recipient, subject, body,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\"\n FROM comms_queue\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "recipient", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "subject", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "body", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "channel: CommsChannel", + "type_info": { + "Custom": { + "name": "comms_channel", + "kind": { + "Enum": [ + "email", + "discord", + "telegram", + "signal" + ] + } + } + } + }, + { + "ordinal": 6, + "name": "comms_type: CommsType", + "type_info": { + "Custom": { + "name": "comms_type", + "kind": { + "Enum": [ + "welcome", + "email_verification", + "password_reset", + "email_update", + "account_deletion", + "admin_email", + "plc_operation", + "two_factor_code", + "channel_verification", + "passkey_recovery", + "legacy_login_alert", + "migration_verification" + ] + } + } + } + }, + { + "ordinal": 7, + "name": "status: CommsStatus", + "type_info": { + "Custom": { + "name": "comms_status", + "kind": { + "Enum": [ + "pending", + "processing", + "sent", + "failed" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe" +} diff --git a/.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json b/.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json new file mode 100644 index 0000000..2640f70 --- /dev/null +++ b/.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "private_key_bytes", + "type_info": "Bytea" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "used_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5" +} diff --git a/.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json b/.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json new file mode 100644 index 0000000..f10b2bc --- /dev/null +++ b/.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT token FROM account_deletion_requests WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5" +} diff --git a/.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json b/.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json new file mode 100644 index 0000000..0297c9a --- /dev/null +++ b/.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json @@ -0,0 +1,60 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n recipient, subject, body,\n comms_type as \"comms_type: CommsType\"\n FROM comms_queue\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "recipient", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "subject", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "body", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "comms_type: CommsType", + "type_info": { + "Custom": { + "name": "comms_type", + "kind": { + "Enum": [ + "welcome", + "email_verification", + "password_reset", + "email_update", + "account_deletion", + "admin_email", + "plc_operation", + "two_factor_code", + "channel_verification", + "passkey_recovery", + "legacy_login_alert", + "migration_verification" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false, + false + ] + }, + "hash": "b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5" +} diff --git a/.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json b/.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json new file mode 100644 index 0000000..6b53208 --- /dev/null +++ b/.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT password_reset_code FROM users WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "password_reset_code", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee" +} diff --git a/.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json b/.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json new file mode 100644 index 0000000..08e8e23 --- /dev/null +++ b/.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3" +} diff --git a/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json b/.sqlx/query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json similarity index 89% rename from .sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json rename to .sqlx/query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json index 6fda80b..07e2470 100644 --- a/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json +++ b/.sqlx/query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE comms_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM comms_queue\n WHERE status = 'pending'\n AND scheduled_for <= $1\n AND attempts < max_attempts\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\n id, user_id,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: super::types::CommsType\",\n status as \"status: CommsStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n ", + "query": "\n UPDATE comms_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM comms_queue\n WHERE status = 'pending'\n AND scheduled_for <= $1\n AND attempts < max_attempts\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\n id, user_id,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n ", "describe": { "columns": [ { @@ -32,7 +32,7 @@ }, { "ordinal": 3, - "name": "comms_type: super::types::CommsType", + "name": "comms_type: CommsType", "type_info": { "Custom": { "name": "comms_type", @@ -153,5 +153,5 @@ true ] }, - "hash": "20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08" + "hash": "d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87" } diff --git a/.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json b/.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json new file mode 100644 index 0000000..be751e8 --- /dev/null +++ b/.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4" +} diff --git a/.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json b/.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json new file mode 100644 index 0000000..0576d91 --- /dev/null +++ b/.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3" +} diff --git a/.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json b/.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json new file mode 100644 index 0000000..edb2f4b --- /dev/null +++ b/.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "used_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9" +} diff --git a/.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json b/.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json new file mode 100644 index 0000000..e8c8434 --- /dev/null +++ b/.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7" +} diff --git a/.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json b/.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json new file mode 100644 index 0000000..db8bde6 --- /dev/null +++ b/.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET is_admin = TRUE WHERE did = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814" +} diff --git a/.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json b/.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json new file mode 100644 index 0000000..32320a7 --- /dev/null +++ b/.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "password_reset_code", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "password_reset_code_expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + true + ] + }, + "hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382" +} diff --git a/Cargo.lock b/Cargo.lock index 68b10d0..ccdce4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6313,6 +6313,110 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tranquil-auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "base32", + "base64 0.22.1", + "bcrypt", + "chrono", + "hmac", + "k256", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.17", + "totp-rs", + "tranquil-crypto", + "urlencoding", + "uuid", +] + +[[package]] +name = "tranquil-cache" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "redis", + "thiserror 2.0.17", + "tracing", + "tranquil-infra", +] + +[[package]] +name = "tranquil-comms" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "reqwest", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", + "tokio", + "urlencoding", + "uuid", +] + +[[package]] +name = "tranquil-crypto" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "base64 0.22.1", + "hkdf", + "hmac", + "p256 0.13.2", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.17", +] + +[[package]] +name = "tranquil-infra" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "futures", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "tranquil-oauth" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64 0.22.1", + "chrono", + "ed25519-dalek", + "p256 0.13.2", + "p384", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "sha2", + "sqlx", + "tokio", + "tracing", + "tranquil-types", + "uuid", +] + [[package]] name = "tranquil-pds" version = "0.1.0" @@ -6380,6 +6484,16 @@ dependencies = [ "tower-layer", "tracing", "tracing-subscriber", + "tranquil-auth", + "tranquil-cache", + "tranquil-comms", + "tranquil-crypto", + "tranquil-infra", + "tranquil-oauth", + "tranquil-repo", + "tranquil-scopes", + "tranquil-storage", + "tranquil-types", "urlencoding", "uuid", "webauthn-rs", @@ -6388,6 +6502,60 @@ dependencies = [ "zip", ] +[[package]] +name = "tranquil-repo" +version = "0.1.0" +dependencies = [ + "bytes", + "cid", + "jacquard-repo", + "multihash", + "sha2", + "sqlx", + "tranquil-types", +] + +[[package]] +name = "tranquil-scopes" +version = "0.1.0" +dependencies = [ + "axum", + "futures", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "tranquil-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-sdk-s3", + "bytes", + "futures", + "sha2", + "thiserror 2.0.17", + "tracing", + "tranquil-infra", +] + +[[package]] +name = "tranquil-types" +version = "0.1.0" +dependencies = [ + "chrono", + "cid", + "jacquard", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", +] + [[package]] name = "triomphe" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index d20935b..1986026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,79 +1,103 @@ -[package] -name = "tranquil-pds" +[workspace] +resolver = "2" +members = [ + "crates/tranquil-types", + "crates/tranquil-infra", + "crates/tranquil-crypto", + "crates/tranquil-storage", + "crates/tranquil-cache", + "crates/tranquil-repo", + "crates/tranquil-scopes", + "crates/tranquil-auth", + "crates/tranquil-oauth", + "crates/tranquil-comms", + "crates/tranquil-pds", +] + +[workspace.package] version = "0.1.0" edition = "2024" license = "AGPL-3.0-or-later" -[dependencies] -anyhow = "1.0.100" -async-trait = "0.1.89" -aws-config = "1.8.12" -aws-sdk-s3 = "1.118.0" -axum = { version = "0.8.8", features = ["ws", "macros"] } + +[workspace.dependencies] +tranquil-types = { path = "crates/tranquil-types" } +tranquil-infra = { path = "crates/tranquil-infra" } +tranquil-crypto = { path = "crates/tranquil-crypto" } +tranquil-storage = { path = "crates/tranquil-storage" } +tranquil-cache = { path = "crates/tranquil-cache" } +tranquil-repo = { path = "crates/tranquil-repo" } +tranquil-scopes = { path = "crates/tranquil-scopes" } +tranquil-auth = { path = "crates/tranquil-auth" } +tranquil-oauth = { path = "crates/tranquil-oauth" } +tranquil-comms = { path = "crates/tranquil-comms" } + +aes-gcm = "0.10" +anyhow = "1.0" +async-trait = "0.1" +aws-config = "1.8" +aws-sdk-s3 = "1.118" +axum = { version = "0.8", features = ["ws", "macros"] } base32 = "0.5" -base64 = "0.22.1" -bcrypt = "0.17.1" -bytes = "1.11.0" -chrono = { version = "0.4.42", features = ["serde"] } -cid = "0.11.1" -dotenvy = "0.15.7" -futures = "0.3.30" +base64 = "0.22" +bcrypt = "0.17" +bs58 = "0.5" +bytes = "1.11" +chrono = { version = "0.4", features = ["serde"] } +cid = "0.11" +dotenvy = "0.15" +ed25519-dalek = { version = "2.1", features = ["pkcs8"] } +futures = "0.3" +futures-util = "0.3" governor = "0.10" hex = "0.4" +hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } hkdf = "0.12" hmac = "0.12" +http = "1.4" +image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } infer = "0.19" -aes-gcm = "0.10" -jacquard = { version = "0.9.5", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] } -jacquard-axum = "0.9.6" -jacquard-repo = "0.9.6" -jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } -k256 = { version = "0.13.3", features = ["ecdsa", "pem", "pkcs8"] } -multibase = "0.9.1" -multihash = "0.19.3" -rand = "0.8.5" -regex = "1" -reqwest = { version = "0.12.28", features = ["json"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_bytes = "0.11.14" -serde_ipld_dagcbor = "0.6.4" -ipld-core = "0.4.2" -serde_json = "1.0.146" -serde_urlencoded = "0.7" -sha2 = "0.10.9" -subtle = "2.5" +ipld-core = "0.4" +iroh-car = "0.5" +jacquard = { version = "0.9", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] } +jacquard-axum = "0.9" +jacquard-repo = "0.9" +jsonwebtoken = { version = "10.2", features = ["rust_crypto"] } +k256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] } +metrics = "0.24" +metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } +multibase = "0.9" +multihash = "0.19" p256 = { version = "0.13", features = ["ecdsa"] } p384 = { version = "0.13", features = ["ecdsa"] } -ed25519-dalek = { version = "2.1", features = ["pkcs8"] } -sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } -thiserror = "2.0.17" -tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time", "signal", "process"] } -tracing = "0.1.43" -tracing-subscriber = "0.3.22" -tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] } -urlencoding = "2.1" -uuid = { version = "1.19.0", features = ["v4", "v5", "fast-rng"] } -iroh-car = "0.5.1" -image = { version = "0.25.9", default-features = false, features = ["jpeg", "png", "gif", "webp"] } -redis = { version = "1.0.1", features = ["tokio-comp", "connection-manager"] } -tower-http = { version = "0.6.8", features = ["fs", "cors"] } -hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } -metrics = "0.24.3" -metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } -bs58 = "0.5.1" +rand = "0.8" +redis = { version = "1.0", features = ["tokio-comp", "connection-manager"] } +regex = "1" +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11" +serde_ipld_dagcbor = "0.6" +serde_json = "1.0" +serde_urlencoded = "0.7" +sha2 = "0.10" +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } +subtle = "2.5" +thiserror = "2.0" +tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "time", "signal", "process"] } +tokio-tungstenite = { version = "0.28", features = ["native-tls"] } totp-rs = { version = "5", features = ["qr"] } -webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] } -webauthn-rs-proto = "0.5.4" -zip = { version = "7.0.0", default-features = false, features = ["deflate"] } -tower = "0.5.2" -tower-layer = "0.3.3" -futures-util = "0.3.31" -http = "1.4.0" -[features] -external-infra = [] -[dev-dependencies] +tower = "0.5" +tower-http = { version = "0.6", features = ["fs", "cors"] } +tower-layer = "0.3" +tracing = "0.1" +tracing-subscriber = "0.3" +urlencoding = "2.1" +uuid = { version = "1.19", features = ["v4", "v5", "fast-rng"] } +webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] } +webauthn-rs-proto = "0.5" +zip = { version = "7.0", default-features = false, features = ["deflate"] } + ciborium = "0.2" -ctor = "0.6.3" -testcontainers = "0.26.2" -testcontainers-modules = { version = "0.14.0", features = ["postgres"] } -wiremock = "0.6.5" -# urlencoding is also in dependencies, but tests use it directly +ctor = "0.6" +testcontainers = "0.26" +testcontainers-modules = { version = "0.14", features = ["postgres"] } +wiremock = "0.6" diff --git a/crates/tranquil-auth/Cargo.toml b/crates/tranquil-auth/Cargo.toml new file mode 100644 index 0000000..a9bce57 --- /dev/null +++ b/crates/tranquil-auth/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tranquil-auth" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-crypto = { workspace = true } + +anyhow = { workspace = true } +base32 = { workspace = true } +base64 = { workspace = true } +bcrypt = { workspace = true } +chrono = { workspace = true } +hmac = { workspace = true } +k256 = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +thiserror = { workspace = true } +totp-rs = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } diff --git a/crates/tranquil-auth/src/lib.rs b/crates/tranquil-auth/src/lib.rs new file mode 100644 index 0000000..cf1e75b --- /dev/null +++ b/crates/tranquil-auth/src/lib.rs @@ -0,0 +1,29 @@ +mod token; +mod totp; +mod types; +mod verify; + +pub use token::{ + SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS, + TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token, create_access_token_hs256, + create_access_token_hs256_with_metadata, create_access_token_with_delegation, + create_access_token_with_metadata, create_access_token_with_scope_metadata, + create_refresh_token, create_refresh_token_hs256, create_refresh_token_hs256_with_metadata, + create_refresh_token_with_metadata, create_service_token, create_service_token_hs256, +}; + +pub use totp::{ + decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, + generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, + verify_backup_code, verify_totp_code, +}; + +pub use types::{ + ActClaim, Claims, Header, TokenData, TokenVerifyError, TokenWithMetadata, UnsafeClaims, +}; + +pub use verify::{ + get_algorithm_from_token, get_did_from_token, get_jti_from_token, verify_access_token, + verify_access_token_hs256, verify_access_token_typed, verify_refresh_token, + verify_refresh_token_hs256, verify_token, +}; diff --git a/src/auth/token.rs b/crates/tranquil-auth/src/token.rs similarity index 94% rename from src/auth/token.rs rename to crates/tranquil-auth/src/token.rs index 08c3767..104ea9a 100644 --- a/src/auth/token.rs +++ b/crates/tranquil-auth/src/token.rs @@ -1,12 +1,11 @@ -use super::{ActClaim, Claims, Header}; +use super::types::{ActClaim, Claims, Header, TokenWithMetadata}; use anyhow::Result; use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use chrono::{DateTime, Duration, Utc}; +use chrono::{Duration, Utc}; use hmac::{Hmac, Mac}; use k256::ecdsa::{Signature, SigningKey, signature::Signer}; use sha2::Sha256; -use uuid; type HmacSha256 = Hmac; @@ -18,12 +17,6 @@ pub const SCOPE_REFRESH: &str = "com.atproto.refresh"; pub const SCOPE_APP_PASS: &str = "com.atproto.appPass"; pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged"; -pub struct TokenWithMetadata { - pub token: String, - pub jti: String, - pub expires_at: DateTime, -} - pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result { Ok(create_access_token_with_metadata(did, key_bytes)?.token) } @@ -33,13 +26,14 @@ pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result { } pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result { - create_access_token_with_scope_metadata(did, key_bytes, None) + create_access_token_with_scope_metadata(did, key_bytes, None, None) } pub fn create_access_token_with_scope_metadata( did: &str, key_bytes: &[u8], scopes: Option<&str>, + hostname: Option<&str>, ) -> Result { let scope = scopes.unwrap_or(SCOPE_ACCESS); create_signed_token_with_metadata( @@ -48,6 +42,7 @@ pub fn create_access_token_with_scope_metadata( TOKEN_TYPE_ACCESS, key_bytes, Duration::minutes(15), + hostname, ) } @@ -56,6 +51,7 @@ pub fn create_access_token_with_delegation( key_bytes: &[u8], scopes: Option<&str>, controller_did: Option<&str>, + hostname: Option<&str>, ) -> Result { let scope = scopes.unwrap_or(SCOPE_ACCESS); let act = controller_did.map(|c| ActClaim { sub: c.to_string() }); @@ -66,6 +62,7 @@ pub fn create_access_token_with_delegation( key_bytes, Duration::minutes(15), act, + hostname, ) } @@ -79,6 +76,7 @@ pub fn create_refresh_token_with_metadata( TOKEN_TYPE_REFRESH, key_bytes, Duration::days(14), + None, ) } @@ -111,8 +109,9 @@ fn create_signed_token_with_metadata( typ: &str, key_bytes: &[u8], duration: Duration, + hostname: Option<&str>, ) -> Result { - create_signed_token_with_act(did, scope, typ, key_bytes, duration, None) + create_signed_token_with_act(did, scope, typ, key_bytes, duration, None, hostname) } fn create_signed_token_with_act( @@ -122,6 +121,7 @@ fn create_signed_token_with_act( key_bytes: &[u8], duration: Duration, act: Option, + hostname: Option<&str>, ) -> Result { let signing_key = SigningKey::from_slice(key_bytes)?; @@ -132,13 +132,14 @@ fn create_signed_token_with_act( let expiration = expires_at.timestamp(); let jti = uuid::Uuid::new_v4().to_string(); + let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| { + std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) + }); + let claims = Claims { iss: did.to_owned(), sub: did.to_owned(), - aud: format!( - "did:web:{}", - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) - ), + aud: format!("did:web:{}", aud_hostname), exp: expiration as usize, iat: Utc::now().timestamp() as usize, scope: Some(scope.to_string()), diff --git a/src/auth/totp.rs b/crates/tranquil-auth/src/totp.rs similarity index 86% rename from src/auth/totp.rs rename to crates/tranquil-auth/src/totp.rs index 58a06d2..6f1e4c6 100644 --- a/src/auth/totp.rs +++ b/crates/tranquil-auth/src/totp.rs @@ -13,12 +13,18 @@ pub fn generate_totp_secret() -> Vec { secret } -pub fn encrypt_totp_secret(secret: &[u8]) -> Result, String> { - crate::config::encrypt_key(secret) +pub fn encrypt_totp_secret( + secret: &[u8], + master_key: &[u8; 32], +) -> Result, tranquil_crypto::CryptoError> { + tranquil_crypto::encrypt_with_key(master_key, secret) } -pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result, String> { - crate::config::decrypt_key(encrypted, Some(version)) +pub fn decrypt_totp_secret( + encrypted: &[u8], + master_key: &[u8; 32], +) -> Result, tranquil_crypto::CryptoError> { + tranquil_crypto::decrypt_with_key(master_key, encrypted) } fn create_totp( @@ -53,16 +59,12 @@ pub fn verify_totp_code(secret: &[u8], code: &str) -> bool { .map(|d| d.as_secs()) .unwrap_or(0); - for offset in [-1i64, 0, 1] { + [-1i64, 0, 1].iter().any(|&offset| { let time = (now as i64 + offset * TOTP_STEP as i64) as u64; let expected = totp.generate(time); let is_valid: bool = code.as_bytes().ct_eq(expected.as_bytes()).into(); - if is_valid { - return true; - } - } - - false + is_valid + }) } pub fn generate_totp_uri(secret: &[u8], account_name: &str, issuer: &str) -> String { @@ -107,14 +109,15 @@ pub fn generate_backup_codes() -> Vec { let mut codes = Vec::with_capacity(BACKUP_CODE_COUNT); let mut rng = rand::thread_rng(); - for _ in 0..BACKUP_CODE_COUNT { - let mut code = String::with_capacity(BACKUP_CODE_LENGTH); - for _ in 0..BACKUP_CODE_LENGTH { - let idx = (rng.next_u32() as usize) % BACKUP_CODE_ALPHABET.len(); - code.push(BACKUP_CODE_ALPHABET[idx] as char); - } + (0..BACKUP_CODE_COUNT).for_each(|_| { + let code: String = (0..BACKUP_CODE_LENGTH) + .map(|_| { + let idx = (rng.next_u32() as usize) % BACKUP_CODE_ALPHABET.len(); + BACKUP_CODE_ALPHABET[idx] as char + }) + .collect(); codes.push(code); - } + }); codes } @@ -167,10 +170,10 @@ mod tests { fn test_backup_codes() { let codes = generate_backup_codes(); assert_eq!(codes.len(), BACKUP_CODE_COUNT); - for code in &codes { + codes.iter().for_each(|code| { assert_eq!(code.len(), BACKUP_CODE_LENGTH); assert!(is_backup_code_format(code)); - } + }); } #[test] diff --git a/crates/tranquil-auth/src/types.rs b/crates/tranquil-auth/src/types.rs new file mode 100644 index 0000000..089b59e --- /dev/null +++ b/crates/tranquil-auth/src/types.rs @@ -0,0 +1,63 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActClaim { + pub sub: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub iss: String, + pub sub: String, + pub aud: String, + pub exp: usize, + pub iat: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lxm: Option, + pub jti: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub act: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Header { + pub alg: String, + pub typ: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UnsafeClaims { + pub iss: String, + pub sub: Option, +} + +pub struct TokenData { + pub claims: T, +} + +pub struct TokenWithMetadata { + pub token: String, + pub jti: String, + pub expires_at: DateTime, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TokenVerifyError { + Expired, + Invalid, +} + +impl fmt::Display for TokenVerifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Expired => write!(f, "Token expired"), + Self::Invalid => write!(f, "Token invalid"), + } + } +} + +impl std::error::Error for TokenVerifyError {} diff --git a/src/auth/verify.rs b/crates/tranquil-auth/src/verify.rs similarity index 95% rename from src/auth/verify.rs rename to crates/tranquil-auth/src/verify.rs index b936e73..775d664 100644 --- a/src/auth/verify.rs +++ b/crates/tranquil-auth/src/verify.rs @@ -2,7 +2,7 @@ use super::token::{ SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, }; -use super::{Claims, Header, TokenData, UnsafeClaims}; +use super::types::{Claims, Header, TokenData, TokenVerifyError, UnsafeClaims}; use anyhow::{Context, Result, anyhow}; use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; @@ -10,28 +10,10 @@ use chrono::Utc; use hmac::{Hmac, Mac}; use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier}; use sha2::Sha256; -use std::fmt; use subtle::ConstantTimeEq; type HmacSha256 = Hmac; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TokenVerifyError { - Expired, - Invalid, -} - -impl fmt::Display for TokenVerifyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Expired => write!(f, "Token expired"), - Self::Invalid => write!(f, "Token invalid"), - } - } -} - -impl std::error::Error for TokenVerifyError {} - pub fn get_did_from_token(token: &str) -> Result { let parts: Vec<&str> = token.split('.').collect(); if parts.len() != 3 { @@ -68,6 +50,22 @@ pub fn get_jti_from_token(token: &str) -> Result { .ok_or_else(|| "No jti claim in token".to_string()) } +pub fn get_algorithm_from_token(token: &str) -> Result { + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return Err("Invalid token format".to_string()); + } + + let header_bytes = URL_SAFE_NO_PAD + .decode(parts[0]) + .map_err(|e| format!("Base64 decode failed: {}", e))?; + + let header: Header = + serde_json::from_slice(&header_bytes).map_err(|e| format!("JSON decode failed: {}", e))?; + + Ok(header.alg) +} + pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result> { verify_token_internal(token, key_bytes, None, None) } @@ -331,19 +329,3 @@ fn verify_token_typed_internal( Ok(TokenData { claims }) } - -pub fn get_algorithm_from_token(token: &str) -> Result { - let parts: Vec<&str> = token.split('.').collect(); - if parts.len() != 3 { - return Err("Invalid token format".to_string()); - } - - let header_bytes = URL_SAFE_NO_PAD - .decode(parts[0]) - .map_err(|e| format!("Base64 decode failed: {}", e))?; - - let header: Header = - serde_json::from_slice(&header_bytes).map_err(|e| format!("JSON decode failed: {}", e))?; - - Ok(header.alg) -} diff --git a/crates/tranquil-cache/Cargo.toml b/crates/tranquil-cache/Cargo.toml new file mode 100644 index 0000000..2badc06 --- /dev/null +++ b/crates/tranquil-cache/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tranquil-cache" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-infra = { workspace = true } + +async-trait = { workspace = true } +base64 = { workspace = true } +redis = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } diff --git a/src/cache/mod.rs b/crates/tranquil-cache/src/lib.rs similarity index 88% rename from src/cache/mod.rs rename to crates/tranquil-cache/src/lib.rs index c1983fa..dbcad63 100644 --- a/src/cache/mod.rs +++ b/crates/tranquil-cache/src/lib.rs @@ -1,30 +1,10 @@ +pub use tranquil_infra::{Cache, CacheError, DistributedRateLimiter}; + use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use std::sync::Arc; use std::time::Duration; -#[derive(Debug, thiserror::Error)] -pub enum CacheError { - #[error("Cache connection error: {0}")] - Connection(String), - #[error("Serialization error: {0}")] - Serialization(String), -} - -#[async_trait] -pub trait Cache: Send + Sync { - async fn get(&self, key: &str) -> Option; - async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError>; - async fn delete(&self, key: &str) -> Result<(), CacheError>; - async fn get_bytes(&self, key: &str) -> Option> { - self.get(key).await.and_then(|s| BASE64.decode(&s).ok()) - } - async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError> { - let encoded = BASE64.encode(value); - self.set(key, &encoded, ttl).await - } -} - #[derive(Clone)] pub struct ValkeyCache { conn: redis::aio::ConnectionManager, @@ -77,6 +57,15 @@ impl Cache for ValkeyCache { .await .map_err(|e| CacheError::Connection(e.to_string())) } + + async fn get_bytes(&self, key: &str) -> Option> { + self.get(key).await.and_then(|s| BASE64.decode(&s).ok()) + } + + async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError> { + let encoded = BASE64.encode(value); + self.set(key, &encoded, ttl).await + } } pub struct NoOpCache; @@ -94,11 +83,14 @@ impl Cache for NoOpCache { async fn delete(&self, _key: &str) -> Result<(), CacheError> { Ok(()) } -} -#[async_trait] -pub trait DistributedRateLimiter: Send + Sync { - async fn check_rate_limit(&self, key: &str, limit: u32, window_ms: u64) -> bool; + async fn get_bytes(&self, _key: &str) -> Option> { + None + } + + async fn set_bytes(&self, _key: &str, _value: &[u8], _ttl: Duration) -> Result<(), CacheError> { + Ok(()) + } } #[derive(Clone)] diff --git a/crates/tranquil-comms/Cargo.toml b/crates/tranquil-comms/Cargo.toml new file mode 100644 index 0000000..87cdc1e --- /dev/null +++ b/crates/tranquil-comms/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tranquil-comms" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } diff --git a/crates/tranquil-comms/src/lib.rs b/crates/tranquil-comms/src/lib.rs new file mode 100644 index 0000000..b06ea35 --- /dev/null +++ b/crates/tranquil-comms/src/lib.rs @@ -0,0 +1,13 @@ +mod locale; +mod sender; +mod types; + +pub use locale::{ + DEFAULT_LOCALE, NotificationStrings, VALID_LOCALES, format_message, get_strings, + validate_locale, +}; +pub use sender::{ + CommsSender, DiscordSender, EmailSender, SendError, SignalSender, TelegramSender, + is_valid_phone_number, mime_encode_header, sanitize_header_value, +}; +pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms}; diff --git a/src/comms/locale.rs b/crates/tranquil-comms/src/locale.rs similarity index 100% rename from src/comms/locale.rs rename to crates/tranquil-comms/src/locale.rs diff --git a/src/comms/sender.rs b/crates/tranquil-comms/src/sender.rs similarity index 100% rename from src/comms/sender.rs rename to crates/tranquil-comms/src/sender.rs diff --git a/src/comms/types.rs b/crates/tranquil-comms/src/types.rs similarity index 85% rename from src/comms/types.rs rename to crates/tranquil-comms/src/types.rs index a21e1b0..cb14ce0 100644 --- a/src/comms/types.rs +++ b/crates/tranquil-comms/src/types.rs @@ -1,9 +1,9 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] #[sqlx(type_name = "comms_channel", rename_all = "lowercase")] pub enum CommsChannel { Email, @@ -12,7 +12,8 @@ pub enum CommsChannel { Signal, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] #[sqlx(type_name = "comms_status", rename_all = "lowercase")] pub enum CommsStatus { Pending, @@ -21,7 +22,8 @@ pub enum CommsStatus { Failed, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] +#[serde(rename_all = "snake_case")] #[sqlx(type_name = "comms_type", rename_all = "snake_case")] pub enum CommsType { Welcome, @@ -37,7 +39,7 @@ pub enum CommsType { MigrationVerification, } -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] pub struct QueuedComms { pub id: Uuid, pub user_id: Uuid, diff --git a/crates/tranquil-crypto/Cargo.toml b/crates/tranquil-crypto/Cargo.toml new file mode 100644 index 0000000..fc6317b --- /dev/null +++ b/crates/tranquil-crypto/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tranquil-crypto" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +aes-gcm = { workspace = true } +base64 = { workspace = true } +hkdf = { workspace = true } +hmac = { workspace = true } +p256 = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/tranquil-crypto/src/encryption.rs b/crates/tranquil-crypto/src/encryption.rs new file mode 100644 index 0000000..cb026f5 --- /dev/null +++ b/crates/tranquil-crypto/src/encryption.rs @@ -0,0 +1,78 @@ +#[allow(deprecated)] +use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::CryptoError; + +pub fn derive_key(master_key: &[u8], context: &[u8]) -> Result<[u8; 32], CryptoError> { + let hk = Hkdf::::new(None, master_key); + let mut output = [0u8; 32]; + hk.expand(context, &mut output) + .map_err(|e| CryptoError::KeyDerivationFailed(format!("{}", e)))?; + Ok(output) +} + +pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result, CryptoError> { + use rand::RngCore; + + let cipher = Aes256Gcm::new_from_slice(key) + .map_err(|e| CryptoError::EncryptionFailed(format!("Failed to create cipher: {}", e)))?; + + let mut nonce_bytes = [0u8; 12]; + rand::thread_rng().fill_bytes(&mut nonce_bytes); + + #[allow(deprecated)] + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|e| CryptoError::EncryptionFailed(format!("{}", e)))?; + + let mut result = Vec::with_capacity(12 + ciphertext.len()); + result.extend_from_slice(&nonce_bytes); + result.extend_from_slice(&ciphertext); + + Ok(result) +} + +pub fn decrypt_with_key(key: &[u8; 32], encrypted: &[u8]) -> Result, CryptoError> { + if encrypted.len() < 12 { + return Err(CryptoError::DecryptionFailed( + "Encrypted data too short".to_string(), + )); + } + + let cipher = Aes256Gcm::new_from_slice(key) + .map_err(|e| CryptoError::DecryptionFailed(format!("Failed to create cipher: {}", e)))?; + + #[allow(deprecated)] + let nonce = Nonce::from_slice(&encrypted[..12]); + let ciphertext = &encrypted[12..]; + + cipher + .decrypt(nonce, ciphertext) + .map_err(|e| CryptoError::DecryptionFailed(format!("{}", e))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt() { + let key = [0u8; 32]; + let plaintext = b"hello world"; + let encrypted = encrypt_with_key(&key, plaintext).unwrap(); + let decrypted = decrypt_with_key(&key, &encrypted).unwrap(); + assert_eq!(plaintext.as_slice(), decrypted.as_slice()); + } + + #[test] + fn test_derive_key() { + let master = b"master-key-for-testing"; + let key1 = derive_key(master, b"context-1").unwrap(); + let key2 = derive_key(master, b"context-2").unwrap(); + assert_ne!(key1, key2); + } +} diff --git a/src/oauth/jwks.rs b/crates/tranquil-crypto/src/jwk.rs similarity index 100% rename from src/oauth/jwks.rs rename to crates/tranquil-crypto/src/jwk.rs diff --git a/crates/tranquil-crypto/src/lib.rs b/crates/tranquil-crypto/src/lib.rs new file mode 100644 index 0000000..747b6f6 --- /dev/null +++ b/crates/tranquil-crypto/src/lib.rs @@ -0,0 +1,19 @@ +mod encryption; +mod jwk; +mod signing; + +pub use encryption::{decrypt_with_key, derive_key, encrypt_with_key}; +pub use jwk::{Jwk, JwkSet, create_jwk_set}; +pub use signing::{DeviceCookieSigner, SigningKeyPair}; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum CryptoError { + #[error("Encryption failed: {0}")] + EncryptionFailed(String), + #[error("Decryption failed: {0}")] + DecryptionFailed(String), + #[error("Invalid key: {0}")] + InvalidKey(String), + #[error("Key derivation failed: {0}")] + KeyDerivationFailed(String), +} diff --git a/crates/tranquil-crypto/src/signing.rs b/crates/tranquil-crypto/src/signing.rs new file mode 100644 index 0000000..578fef4 --- /dev/null +++ b/crates/tranquil-crypto/src/signing.rs @@ -0,0 +1,150 @@ +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use hmac::Mac; +use p256::ecdsa::SigningKey; +use sha2::{Digest, Sha256}; +use subtle::ConstantTimeEq; + +use crate::CryptoError; + +type HmacSha256 = hmac::Hmac; + +pub struct SigningKeyPair { + #[allow(dead_code)] + signing_key: SigningKey, + pub key_id: String, + pub x: String, + pub y: String, +} + +impl SigningKeyPair { + pub fn from_seed(seed: &[u8]) -> Result { + let mut hasher = Sha256::new(); + hasher.update(b"oauth-signing-key-derivation:"); + hasher.update(seed); + let hash = hasher.finalize(); + + let signing_key = SigningKey::from_slice(&hash) + .map_err(|e| CryptoError::InvalidKey(format!("Failed to create signing key: {}", e)))?; + + let verifying_key = signing_key.verifying_key(); + let point = verifying_key.to_encoded_point(false); + + let x = URL_SAFE_NO_PAD.encode( + point + .x() + .ok_or_else(|| CryptoError::InvalidKey("Missing X coordinate".to_string()))?, + ); + let y = URL_SAFE_NO_PAD.encode( + point + .y() + .ok_or_else(|| CryptoError::InvalidKey("Missing Y coordinate".to_string()))?, + ); + + let mut kid_hasher = Sha256::new(); + kid_hasher.update(x.as_bytes()); + kid_hasher.update(y.as_bytes()); + let kid_hash = kid_hasher.finalize(); + let key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]); + + Ok(Self { + signing_key, + key_id, + x, + y, + }) + } +} + +pub struct DeviceCookieSigner { + key: [u8; 32], +} + +impl DeviceCookieSigner { + pub fn new(key: [u8; 32]) -> Self { + Self { key } + } + + pub fn sign(&self, device_id: &str) -> String { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let message = format!("{}:{}", device_id, timestamp); + let mut mac = + ::new_from_slice(&self.key).expect("HMAC key size is valid"); + mac.update(message.as_bytes()); + let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); + + format!("{}.{}.{}", device_id, timestamp, signature) + } + + pub fn verify(&self, cookie_value: &str, max_age_days: u64) -> Option { + let parts: Vec<&str> = cookie_value.splitn(3, '.').collect(); + if parts.len() != 3 { + return None; + } + + let device_id = parts[0]; + let timestamp_str = parts[1]; + let provided_signature = parts[2]; + + let timestamp: u64 = timestamp_str.parse().ok()?; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if now.saturating_sub(timestamp) > max_age_days * 24 * 60 * 60 { + return None; + } + + let message = format!("{}:{}", device_id, timestamp); + let mut mac = + ::new_from_slice(&self.key).expect("HMAC key size is valid"); + mac.update(message.as_bytes()); + let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); + + if provided_signature + .as_bytes() + .ct_eq(expected_signature.as_bytes()) + .into() + { + Some(device_id.to_string()) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_signing_key_pair() { + let seed = b"test-seed-for-signing-key"; + let kp = SigningKeyPair::from_seed(seed).unwrap(); + assert!(!kp.key_id.is_empty()); + assert!(!kp.x.is_empty()); + assert!(!kp.y.is_empty()); + } + + #[test] + fn test_device_cookie_signer() { + let key = [0u8; 32]; + let signer = DeviceCookieSigner::new(key); + let signed = signer.sign("device-123"); + let verified = signer.verify(&signed, 400); + assert_eq!(verified, Some("device-123".to_string())); + } + + #[test] + fn test_device_cookie_invalid() { + let key = [0u8; 32]; + let signer = DeviceCookieSigner::new(key); + assert!(signer.verify("invalid", 400).is_none()); + assert!(signer.verify("a.b.c", 400).is_none()); + } +} diff --git a/crates/tranquil-infra/Cargo.toml b/crates/tranquil-infra/Cargo.toml new file mode 100644 index 0000000..0789a32 --- /dev/null +++ b/crates/tranquil-infra/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tranquil-infra" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/tranquil-infra/src/lib.rs b/crates/tranquil-infra/src/lib.rs new file mode 100644 index 0000000..9307cc5 --- /dev/null +++ b/crates/tranquil-infra/src/lib.rs @@ -0,0 +1,58 @@ +use async_trait::async_trait; +use bytes::Bytes; +use futures::Stream; +use std::pin::Pin; +use std::time::Duration; + +#[derive(Debug, thiserror::Error)] +pub enum StorageError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("S3 error: {0}")] + S3(String), + #[error("Other: {0}")] + Other(String), +} + +pub struct StreamUploadResult { + pub sha256_hash: [u8; 32], + pub size: u64, +} + +#[async_trait] +pub trait BlobStorage: Send + Sync { + async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError>; + async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError>; + async fn get(&self, key: &str) -> Result, StorageError>; + async fn get_bytes(&self, key: &str) -> Result; + async fn get_head(&self, key: &str, size: usize) -> Result; + async fn delete(&self, key: &str) -> Result<(), StorageError>; + async fn put_stream( + &self, + key: &str, + stream: Pin> + Send>>, + ) -> Result; + async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError>; +} + +#[derive(Debug, thiserror::Error)] +pub enum CacheError { + #[error("Cache connection error: {0}")] + Connection(String), + #[error("Serialization error: {0}")] + Serialization(String), +} + +#[async_trait] +pub trait Cache: Send + Sync { + async fn get(&self, key: &str) -> Option; + async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError>; + async fn delete(&self, key: &str) -> Result<(), CacheError>; + async fn get_bytes(&self, key: &str) -> Option>; + async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError>; +} + +#[async_trait] +pub trait DistributedRateLimiter: Send + Sync { + async fn check_rate_limit(&self, key: &str, limit: u32, window_ms: u64) -> bool; +} diff --git a/crates/tranquil-oauth/Cargo.toml b/crates/tranquil-oauth/Cargo.toml new file mode 100644 index 0000000..cb12b9e --- /dev/null +++ b/crates/tranquil-oauth/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tranquil-oauth" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-types = { workspace = true } + +anyhow = { workspace = true } +sqlx = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true } +ed25519-dalek = { workspace = true } +p256 = { workspace = true } +p384 = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } diff --git a/src/oauth/client.rs b/crates/tranquil-oauth/src/client.rs similarity index 95% rename from src/oauth/client.rs rename to crates/tranquil-oauth/src/client.rs index 796a6f1..cb7f5a9 100644 --- a/src/oauth/client.rs +++ b/crates/tranquil-oauth/src/client.rs @@ -4,7 +4,8 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use super::OAuthError; +use crate::OAuthError; +use crate::types::ClientAuth; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientMetadata { @@ -96,7 +97,6 @@ impl ClientMetadataCache { url.scheme() == "http" && url.host_str() == Some("localhost") && url.port().is_none() - // empty path && url.path() == "/" } else { false @@ -108,16 +108,14 @@ impl ClientMetadataCache { .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?; let mut redirect_uris = Vec::::new(); let mut scope: Option = None; - for (key, value) in url.query_pairs() { - if key == "redirect_uri" { + url.query_pairs().for_each(|(key, value)| { + if key == "redirect_uri" && redirect_uris.is_empty() { redirect_uris.push(value.to_string()); - break; } - if key == "scope" { + if key == "scope" && scope.is_none() { scope = Some(value.into()); - break; } - } + }); if redirect_uris.is_empty() { redirect_uris.push("http://127.0.0.1/".into()); redirect_uris.push("http://[::1]/".into()); @@ -289,9 +287,10 @@ impl ClientMetadataCache { "redirect_uris is required".to_string(), )); } - for uri in &metadata.redirect_uris { - self.validate_redirect_uri_format(uri)?; - } + metadata + .redirect_uris + .iter() + .try_for_each(|uri| self.validate_redirect_uri_format(uri))?; if !metadata.grant_types.is_empty() && !metadata .grant_types @@ -357,8 +356,7 @@ impl ClientMetadataCache { if !scheme .chars() .next() - .map(|c| c.is_ascii_lowercase()) - .unwrap_or(false) + .is_some_and(|c| c.is_ascii_lowercase()) { return Err(OAuthError::InvalidClient(format!( "Invalid redirect_uri scheme: {}", @@ -388,30 +386,26 @@ impl ClientMetadata { pub async fn verify_client_auth( cache: &ClientMetadataCache, metadata: &ClientMetadata, - client_auth: &super::ClientAuth, + client_auth: &ClientAuth, ) -> Result<(), OAuthError> { let expected_method = metadata.auth_method(); match (expected_method, client_auth) { - ("none", super::ClientAuth::None) => Ok(()), + ("none", ClientAuth::None) => Ok(()), ("none", _) => Err(OAuthError::InvalidClient( "Client is configured for no authentication, but credentials were provided".to_string(), )), - ("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => { + ("private_key_jwt", ClientAuth::PrivateKeyJwt { client_assertion }) => { verify_private_key_jwt_async(cache, metadata, client_assertion).await } ("private_key_jwt", _) => Err(OAuthError::InvalidClient( "Client requires private_key_jwt authentication".to_string(), )), - ("client_secret_post", super::ClientAuth::SecretPost { .. }) => { - Err(OAuthError::InvalidClient( - "client_secret_post is not supported for ATProto OAuth".to_string(), - )) - } - ("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => { - Err(OAuthError::InvalidClient( - "client_secret_basic is not supported for ATProto OAuth".to_string(), - )) - } + ("client_secret_post", ClientAuth::SecretPost { .. }) => Err(OAuthError::InvalidClient( + "client_secret_post is not supported for ATProto OAuth".to_string(), + )), + ("client_secret_basic", ClientAuth::SecretBasic { .. }) => Err(OAuthError::InvalidClient( + "client_secret_basic is not supported for ATProto OAuth".to_string(), + )), (method, _) => Err(OAuthError::InvalidClient(format!( "Unsupported or mismatched authentication method: {}", method @@ -519,12 +513,12 @@ async fn verify_private_key_jwt_async( .get("keys") .and_then(|k| k.as_array()) .ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?; - let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid { - keys.iter() + let matching_keys: Vec<&serde_json::Value> = match kid { + Some(kid) => keys + .iter() .filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid)) - .collect() - } else { - keys.iter().collect() + .collect(), + None => keys.iter().collect(), }; if matching_keys.is_empty() { return Err(OAuthError::InvalidClient( diff --git a/src/oauth/dpop.rs b/crates/tranquil-oauth/src/dpop.rs similarity index 99% rename from src/oauth/dpop.rs rename to crates/tranquil-oauth/src/dpop.rs index 8b99d0a..a547ddb 100644 --- a/src/oauth/dpop.rs +++ b/crates/tranquil-oauth/src/dpop.rs @@ -4,8 +4,8 @@ use chrono::Utc; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use super::OAuthError; -use crate::types::{DPoPProofId, JwkThumbprint}; +use crate::OAuthError; +use tranquil_types::{DPoPProofId, JwkThumbprint}; const DPOP_NONCE_VALIDITY_SECS: i64 = 300; const DPOP_MAX_AGE_SECS: i64 = 300; diff --git a/src/oauth/error.rs b/crates/tranquil-oauth/src/error.rs similarity index 100% rename from src/oauth/error.rs rename to crates/tranquil-oauth/src/error.rs index 8093366..090e4d0 100644 --- a/src/oauth/error.rs +++ b/crates/tranquil-oauth/src/error.rs @@ -82,16 +82,16 @@ impl IntoResponse for OAuthError { } } -impl From for OAuthError { - fn from(err: sqlx::Error) -> Self { - tracing::error!("Database error in OAuth flow: {}", err); - OAuthError::ServerError("An internal error occurred".to_string()) - } -} - impl From for OAuthError { fn from(err: anyhow::Error) -> Self { tracing::error!("Internal error in OAuth flow: {}", err); OAuthError::ServerError("An internal error occurred".to_string()) } } + +impl From for OAuthError { + fn from(err: sqlx::Error) -> Self { + tracing::error!("Database error in OAuth flow: {}", err); + OAuthError::ServerError("An internal error occurred".to_string()) + } +} diff --git a/crates/tranquil-oauth/src/lib.rs b/crates/tranquil-oauth/src/lib.rs new file mode 100644 index 0000000..493252c --- /dev/null +++ b/crates/tranquil-oauth/src/lib.rs @@ -0,0 +1,17 @@ +mod client; +mod dpop; +mod error; +mod types; + +pub use client::{ClientMetadata, ClientMetadataCache, verify_client_auth}; +pub use dpop::{ + DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult, + compute_access_token_hash, compute_jwk_thumbprint, +}; +pub use error::OAuthError; +pub use types::{ + AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata, + AuthorizedClientData, ClientAuth, Code, DPoPClaims, DeviceData, DeviceId, JwkPublicKey, Jwks, + OAuthClientMetadata, ParResponse, ProtectedResourceMetadata, RefreshToken, RefreshTokenState, + RequestData, RequestId, SessionId, TokenData, TokenId, TokenRequest, TokenResponse, +}; diff --git a/src/oauth/types.rs b/crates/tranquil-oauth/src/types.rs similarity index 100% rename from src/oauth/types.rs rename to crates/tranquil-oauth/src/types.rs diff --git a/crates/tranquil-pds/.sqlx b/crates/tranquil-pds/.sqlx new file mode 120000 index 0000000..7575826 --- /dev/null +++ b/crates/tranquil-pds/.sqlx @@ -0,0 +1 @@ +../../.sqlx \ No newline at end of file diff --git a/crates/tranquil-pds/Cargo.toml b/crates/tranquil-pds/Cargo.toml new file mode 100644 index 0000000..8200755 --- /dev/null +++ b/crates/tranquil-pds/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "tranquil-pds" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-types = { workspace = true } +tranquil-infra = { workspace = true } +tranquil-crypto = { workspace = true } +tranquil-storage = { workspace = true } +tranquil-cache = { workspace = true } +tranquil-repo = { workspace = true } +tranquil-scopes = { workspace = true } +tranquil-auth = { workspace = true } +tranquil-oauth = { workspace = true } +tranquil-comms = { workspace = true } + +aes-gcm = { workspace = true } +anyhow = { workspace = true } +async-trait = { workspace = true } +aws-config = { workspace = true } +aws-sdk-s3 = { workspace = true } +axum = { workspace = true } +base32 = { workspace = true } +base64 = { workspace = true } +bcrypt = { workspace = true } +bs58 = { workspace = true } +bytes = { workspace = true } +chrono = { workspace = true } +cid = { workspace = true } +dotenvy = { workspace = true } +ed25519-dalek = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } +governor = { workspace = true } +hex = { workspace = true } +hickory-resolver = { workspace = true } +hkdf = { workspace = true } +hmac = { workspace = true } +http = { workspace = true } +image = { workspace = true } +infer = { workspace = true } +ipld-core = { workspace = true } +iroh-car = { workspace = true } +jacquard = { workspace = true } +jacquard-axum = { workspace = true } +jacquard-repo = { workspace = true } +jsonwebtoken = { workspace = true } +k256 = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } +multibase = { workspace = true } +multihash = { workspace = true } +p256 = { workspace = true } +p384 = { workspace = true } +rand = { workspace = true } +redis = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +serde_ipld_dagcbor = { workspace = true } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +sha2 = { workspace = true } +sqlx = { workspace = true } +subtle = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tokio-tungstenite = { workspace = true } +totp-rs = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +tower-layer = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } +webauthn-rs = { workspace = true } +webauthn-rs-proto = { workspace = true } +zip = { workspace = true } + +[features] +external-infra = [] + +[dev-dependencies] +ciborium = { workspace = true } +ctor = { workspace = true } +testcontainers = { workspace = true } +testcontainers-modules = { workspace = true } +wiremock = { workspace = true } diff --git a/crates/tranquil-pds/migrations b/crates/tranquil-pds/migrations new file mode 120000 index 0000000..c9cf9da --- /dev/null +++ b/crates/tranquil-pds/migrations @@ -0,0 +1 @@ +../../migrations \ No newline at end of file diff --git a/src/api/actor/mod.rs b/crates/tranquil-pds/src/api/actor/mod.rs similarity index 100% rename from src/api/actor/mod.rs rename to crates/tranquil-pds/src/api/actor/mod.rs diff --git a/src/api/actor/preferences.rs b/crates/tranquil-pds/src/api/actor/preferences.rs similarity index 100% rename from src/api/actor/preferences.rs rename to crates/tranquil-pds/src/api/actor/preferences.rs diff --git a/src/api/admin/account/delete.rs b/crates/tranquil-pds/src/api/admin/account/delete.rs similarity index 96% rename from src/api/admin/account/delete.rs rename to crates/tranquil-pds/src/api/admin/account/delete.rs index b305fac..412cf78 100644 --- a/src/api/admin/account/delete.rs +++ b/crates/tranquil-pds/src/api/admin/account/delete.rs @@ -130,13 +130,8 @@ pub async fn delete_account( error!("Failed to commit account deletion transaction: {:?}", e); return ApiError::InternalError(Some("Failed to commit deletion".into())).into_response(); } - if let Err(e) = crate::api::repo::record::sequence_account_event( - &state, - did, - false, - Some("deleted"), - ) - .await + if let Err(e) = + crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await { warn!( "Failed to sequence account deletion event for {}: {}", diff --git a/src/api/admin/account/email.rs b/crates/tranquil-pds/src/api/admin/account/email.rs similarity index 100% rename from src/api/admin/account/email.rs rename to crates/tranquil-pds/src/api/admin/account/email.rs diff --git a/src/api/admin/account/info.rs b/crates/tranquil-pds/src/api/admin/account/info.rs similarity index 100% rename from src/api/admin/account/info.rs rename to crates/tranquil-pds/src/api/admin/account/info.rs diff --git a/src/api/admin/account/mod.rs b/crates/tranquil-pds/src/api/admin/account/mod.rs similarity index 100% rename from src/api/admin/account/mod.rs rename to crates/tranquil-pds/src/api/admin/account/mod.rs diff --git a/src/api/admin/account/search.rs b/crates/tranquil-pds/src/api/admin/account/search.rs similarity index 100% rename from src/api/admin/account/search.rs rename to crates/tranquil-pds/src/api/admin/account/search.rs diff --git a/src/api/admin/account/update.rs b/crates/tranquil-pds/src/api/admin/account/update.rs similarity index 96% rename from src/api/admin/account/update.rs rename to crates/tranquil-pds/src/api/admin/account/update.rs index 0c0ad1e..9dec226 100644 --- a/src/api/admin/account/update.rs +++ b/crates/tranquil-pds/src/api/admin/account/update.rs @@ -104,12 +104,9 @@ pub async fn update_account_handle( } let _ = state.cache.delete(&format!("handle:{}", handle)).await; let handle_typed = Handle::new_unchecked(&handle); - if let Err(e) = crate::api::repo::record::sequence_identity_event( - &state, - did, - Some(&handle_typed), - ) - .await + if let Err(e) = + crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle_typed)) + .await { warn!( "Failed to sequence identity event for admin handle update: {}", diff --git a/src/api/admin/config.rs b/crates/tranquil-pds/src/api/admin/config.rs similarity index 100% rename from src/api/admin/config.rs rename to crates/tranquil-pds/src/api/admin/config.rs diff --git a/src/api/admin/invite.rs b/crates/tranquil-pds/src/api/admin/invite.rs similarity index 100% rename from src/api/admin/invite.rs rename to crates/tranquil-pds/src/api/admin/invite.rs diff --git a/src/api/admin/mod.rs b/crates/tranquil-pds/src/api/admin/mod.rs similarity index 100% rename from src/api/admin/mod.rs rename to crates/tranquil-pds/src/api/admin/mod.rs diff --git a/src/api/admin/server_stats.rs b/crates/tranquil-pds/src/api/admin/server_stats.rs similarity index 100% rename from src/api/admin/server_stats.rs rename to crates/tranquil-pds/src/api/admin/server_stats.rs diff --git a/src/api/admin/status.rs b/crates/tranquil-pds/src/api/admin/status.rs similarity index 98% rename from src/api/admin/status.rs rename to crates/tranquil-pds/src/api/admin/status.rs index b298dd1..e8391e1 100644 --- a/src/api/admin/status.rs +++ b/crates/tranquil-pds/src/api/admin/status.rs @@ -224,9 +224,12 @@ pub async fn update_subject_status( .execute(&mut *tx) .await } else { - sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did.as_str()) - .execute(&mut *tx) - .await + sqlx::query!( + "UPDATE users SET deactivated_at = NULL WHERE did = $1", + did.as_str() + ) + .execute(&mut *tx) + .await }; if let Err(e) = result { error!( diff --git a/src/api/age_assurance.rs b/crates/tranquil-pds/src/api/age_assurance.rs similarity index 100% rename from src/api/age_assurance.rs rename to crates/tranquil-pds/src/api/age_assurance.rs diff --git a/src/api/backup.rs b/crates/tranquil-pds/src/api/backup.rs similarity index 100% rename from src/api/backup.rs rename to crates/tranquil-pds/src/api/backup.rs diff --git a/src/api/delegation.rs b/crates/tranquil-pds/src/api/delegation.rs similarity index 99% rename from src/api/delegation.rs rename to crates/tranquil-pds/src/api/delegation.rs index 734cb95..2dd7cd7 100644 --- a/src/api/delegation.rs +++ b/crates/tranquil-pds/src/api/delegation.rs @@ -768,9 +768,5 @@ pub async fn create_delegated_account( info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); - Json(CreateDelegatedAccountResponse { - did: did.into(), - handle: handle.into(), - }) - .into_response() + Json(CreateDelegatedAccountResponse { did, handle }).into_response() } diff --git a/src/api/error.rs b/crates/tranquil-pds/src/api/error.rs similarity index 100% rename from src/api/error.rs rename to crates/tranquil-pds/src/api/error.rs diff --git a/src/api/identity/account.rs b/crates/tranquil-pds/src/api/identity/account.rs similarity index 99% rename from src/api/identity/account.rs rename to crates/tranquil-pds/src/api/identity/account.rs index aebabeb..c8ed282 100644 --- a/src/api/identity/account.rs +++ b/crates/tranquil-pds/src/api/identity/account.rs @@ -796,8 +796,12 @@ pub async fn create_account( if !is_migration && !is_did_web_byod { let did_typed = Did::new_unchecked(&did); let handle_typed = Handle::new_unchecked(&handle); - if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed)).await + if let Err(e) = crate::api::repo::record::sequence_identity_event( + &state, + &did_typed, + Some(&handle_typed), + ) + .await { warn!("Failed to sequence identity event for {}: {}", did, e); } diff --git a/src/api/identity/did.rs b/crates/tranquil-pds/src/api/identity/did.rs similarity index 99% rename from src/api/identity/did.rs rename to crates/tranquil-pds/src/api/identity/did.rs index ed3b608..391e791 100644 --- a/src/api/identity/did.rs +++ b/crates/tranquil-pds/src/api/identity/did.rs @@ -754,7 +754,8 @@ pub async fn update_handle( let _ = state.cache.delete(&format!("handle:{}", handle)).await; let handle_typed = Handle::new_unchecked(&handle); if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) + .await { warn!("Failed to sequence identity event for handle update: {}", e); } diff --git a/src/api/identity/mod.rs b/crates/tranquil-pds/src/api/identity/mod.rs similarity index 100% rename from src/api/identity/mod.rs rename to crates/tranquil-pds/src/api/identity/mod.rs diff --git a/src/api/identity/plc/mod.rs b/crates/tranquil-pds/src/api/identity/plc/mod.rs similarity index 100% rename from src/api/identity/plc/mod.rs rename to crates/tranquil-pds/src/api/identity/plc/mod.rs diff --git a/src/api/identity/plc/request.rs b/crates/tranquil-pds/src/api/identity/plc/request.rs similarity index 100% rename from src/api/identity/plc/request.rs rename to crates/tranquil-pds/src/api/identity/plc/request.rs diff --git a/src/api/identity/plc/sign.rs b/crates/tranquil-pds/src/api/identity/plc/sign.rs similarity index 100% rename from src/api/identity/plc/sign.rs rename to crates/tranquil-pds/src/api/identity/plc/sign.rs diff --git a/src/api/identity/plc/submit.rs b/crates/tranquil-pds/src/api/identity/plc/submit.rs similarity index 100% rename from src/api/identity/plc/submit.rs rename to crates/tranquil-pds/src/api/identity/plc/submit.rs diff --git a/src/api/mod.rs b/crates/tranquil-pds/src/api/mod.rs similarity index 100% rename from src/api/mod.rs rename to crates/tranquil-pds/src/api/mod.rs diff --git a/src/api/moderation/mod.rs b/crates/tranquil-pds/src/api/moderation/mod.rs similarity index 100% rename from src/api/moderation/mod.rs rename to crates/tranquil-pds/src/api/moderation/mod.rs diff --git a/src/api/notification_prefs.rs b/crates/tranquil-pds/src/api/notification_prefs.rs similarity index 100% rename from src/api/notification_prefs.rs rename to crates/tranquil-pds/src/api/notification_prefs.rs diff --git a/src/api/proxy.rs b/crates/tranquil-pds/src/api/proxy.rs similarity index 99% rename from src/api/proxy.rs rename to crates/tranquil-pds/src/api/proxy.rs index 4c5e4d0..b6b323d 100644 --- a/src/api/proxy.rs +++ b/crates/tranquil-pds/src/api/proxy.rs @@ -268,8 +268,7 @@ async fn proxy_handler( } Err(e) => { warn!("Token validation failed: {:?}", e); - if matches!(e, crate::auth::TokenValidationError::TokenExpired) - && extracted.is_dpop + if matches!(e, crate::auth::TokenValidationError::TokenExpired) && extracted.is_dpop { let www_auth = "DPoP error=\"invalid_token\", error_description=\"Token has expired\""; diff --git a/src/api/proxy_client.rs b/crates/tranquil-pds/src/api/proxy_client.rs similarity index 100% rename from src/api/proxy_client.rs rename to crates/tranquil-pds/src/api/proxy_client.rs diff --git a/src/api/repo/blob.rs b/crates/tranquil-pds/src/api/repo/blob.rs similarity index 100% rename from src/api/repo/blob.rs rename to crates/tranquil-pds/src/api/repo/blob.rs diff --git a/src/api/repo/import.rs b/crates/tranquil-pds/src/api/repo/import.rs similarity index 100% rename from src/api/repo/import.rs rename to crates/tranquil-pds/src/api/repo/import.rs diff --git a/src/api/repo/meta.rs b/crates/tranquil-pds/src/api/repo/meta.rs similarity index 100% rename from src/api/repo/meta.rs rename to crates/tranquil-pds/src/api/repo/meta.rs diff --git a/src/api/repo/mod.rs b/crates/tranquil-pds/src/api/repo/mod.rs similarity index 100% rename from src/api/repo/mod.rs rename to crates/tranquil-pds/src/api/repo/mod.rs diff --git a/src/api/repo/record/batch.rs b/crates/tranquil-pds/src/api/repo/record/batch.rs similarity index 99% rename from src/api/repo/record/batch.rs rename to crates/tranquil-pds/src/api/repo/record/batch.rs index d752749..05960ec 100644 --- a/src/api/repo/record/batch.rs +++ b/crates/tranquil-pds/src/api/repo/record/batch.rs @@ -421,7 +421,14 @@ pub async fn apply_writes( ops, modified_keys, all_blob_cids, - } = match process_writes(&input.writes, initial_mst, &did, input.validate, &tracking_store).await + } = match process_writes( + &input.writes, + initial_mst, + &did, + input.validate, + &tracking_store, + ) + .await { Ok(acc) => acc, Err(response) => return response, diff --git a/src/api/repo/record/delete.rs b/crates/tranquil-pds/src/api/repo/record/delete.rs similarity index 100% rename from src/api/repo/record/delete.rs rename to crates/tranquil-pds/src/api/repo/record/delete.rs diff --git a/src/api/repo/record/mod.rs b/crates/tranquil-pds/src/api/repo/record/mod.rs similarity index 100% rename from src/api/repo/record/mod.rs rename to crates/tranquil-pds/src/api/repo/record/mod.rs diff --git a/src/api/repo/record/read.rs b/crates/tranquil-pds/src/api/repo/record/read.rs similarity index 95% rename from src/api/repo/record/read.rs rename to crates/tranquil-pds/src/api/repo/record/read.rs index ae9a40d..6fcc37a 100644 --- a/src/api/repo/record/read.rs +++ b/crates/tranquil-pds/src/api/repo/record/read.rs @@ -257,13 +257,15 @@ pub async fn list_records( .zip(blocks.into_iter()) .filter_map(|((_, rkey, cid_str), block_opt)| { block_opt.and_then(|block| { - serde_ipld_dagcbor::from_slice::(&block).ok().map(|ipld| { - json!({ - "uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey), - "cid": cid_str, - "value": ipld_to_json(ipld) + serde_ipld_dagcbor::from_slice::(&block) + .ok() + .map(|ipld| { + json!({ + "uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey), + "cid": cid_str, + "value": ipld_to_json(ipld) + }) }) - }) }) }) .collect(); diff --git a/src/api/repo/record/utils.rs b/crates/tranquil-pds/src/api/repo/record/utils.rs similarity index 99% rename from src/api/repo/record/utils.rs rename to crates/tranquil-pds/src/api/repo/record/utils.rs index dd46c58..7b2f927 100644 --- a/src/api/repo/record/utils.rs +++ b/crates/tranquil-pds/src/api/repo/record/utils.rs @@ -219,9 +219,9 @@ pub async fn commit_and_log( .await .map_err(|e| format!("DB Error (user_blocks delete obsolete): {}", e))?; } - let (upserts, deletes): (Vec<_>, Vec<_>) = ops.iter().partition(|op| { - matches!(op, RecordOp::Create { .. } | RecordOp::Update { .. }) - }); + let (upserts, deletes): (Vec<_>, Vec<_>) = ops + .iter() + .partition(|op| matches!(op, RecordOp::Create { .. } | RecordOp::Update { .. })); let (upsert_collections, upsert_rkeys, upsert_cids): (Vec, Vec, Vec) = upserts .into_iter() diff --git a/src/api/repo/record/validation.rs b/crates/tranquil-pds/src/api/repo/record/validation.rs similarity index 100% rename from src/api/repo/record/validation.rs rename to crates/tranquil-pds/src/api/repo/record/validation.rs diff --git a/src/api/repo/record/write.rs b/crates/tranquil-pds/src/api/repo/record/write.rs similarity index 100% rename from src/api/repo/record/write.rs rename to crates/tranquil-pds/src/api/repo/record/write.rs diff --git a/src/api/responses.rs b/crates/tranquil-pds/src/api/responses.rs similarity index 100% rename from src/api/responses.rs rename to crates/tranquil-pds/src/api/responses.rs diff --git a/src/api/server/account_status.rs b/crates/tranquil-pds/src/api/server/account_status.rs similarity index 99% rename from src/api/server/account_status.rs rename to crates/tranquil-pds/src/api/server/account_status.rs index 5c628e8..3b5ee92 100644 --- a/src/api/server/account_status.rs +++ b/crates/tranquil-pds/src/api/server/account_status.rs @@ -449,8 +449,7 @@ pub async fn activate_account( did ); if let Err(e) = - crate::api::repo::record::sequence_account_event(&state, &did, true, None) - .await + crate::api::repo::record::sequence_account_event(&state, &did, true, None).await { warn!( "[MIGRATION] activateAccount: Failed to sequence account activation event: {}", @@ -463,7 +462,7 @@ pub async fn activate_account( "[MIGRATION] activateAccount: Sequencing identity event for did={} handle={:?}", did, handle ); - let handle_typed = handle.as_ref().map(|h| Handle::new_unchecked(h)); + let handle_typed = handle.as_ref().map(Handle::new_unchecked); if let Err(e) = crate::api::repo::record::sequence_identity_event( &state, &did, diff --git a/src/api/server/app_password.rs b/crates/tranquil-pds/src/api/server/app_password.rs similarity index 100% rename from src/api/server/app_password.rs rename to crates/tranquil-pds/src/api/server/app_password.rs diff --git a/src/api/server/email.rs b/crates/tranquil-pds/src/api/server/email.rs similarity index 100% rename from src/api/server/email.rs rename to crates/tranquil-pds/src/api/server/email.rs diff --git a/src/api/server/invite.rs b/crates/tranquil-pds/src/api/server/invite.rs similarity index 100% rename from src/api/server/invite.rs rename to crates/tranquil-pds/src/api/server/invite.rs diff --git a/src/api/server/logo.rs b/crates/tranquil-pds/src/api/server/logo.rs similarity index 100% rename from src/api/server/logo.rs rename to crates/tranquil-pds/src/api/server/logo.rs diff --git a/src/api/server/meta.rs b/crates/tranquil-pds/src/api/server/meta.rs similarity index 100% rename from src/api/server/meta.rs rename to crates/tranquil-pds/src/api/server/meta.rs diff --git a/src/api/server/migration.rs b/crates/tranquil-pds/src/api/server/migration.rs similarity index 100% rename from src/api/server/migration.rs rename to crates/tranquil-pds/src/api/server/migration.rs diff --git a/src/api/server/mod.rs b/crates/tranquil-pds/src/api/server/mod.rs similarity index 100% rename from src/api/server/mod.rs rename to crates/tranquil-pds/src/api/server/mod.rs diff --git a/src/api/server/passkey_account.rs b/crates/tranquil-pds/src/api/server/passkey_account.rs similarity index 99% rename from src/api/server/passkey_account.rs rename to crates/tranquil-pds/src/api/server/passkey_account.rs index cf7b2d1..c4d1676 100644 --- a/src/api/server/passkey_account.rs +++ b/crates/tranquil-pds/src/api/server/passkey_account.rs @@ -602,8 +602,12 @@ pub async fn create_passkey_account( if !is_byod_did_web { let handle_typed = Handle::new_unchecked(&handle); - if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed)).await + if let Err(e) = crate::api::repo::record::sequence_identity_event( + &state, + &did_typed, + Some(&handle_typed), + ) + .await { warn!("Failed to sequence identity event for {}: {}", did, e); } @@ -654,7 +658,7 @@ pub async fn create_passkey_account( info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion"); let access_jwt = if byod_auth.is_some() { - match crate::auth::token::create_access_token_with_metadata(&did, &secret_key_bytes) { + match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { Ok(token_meta) => { let refresh_jti = uuid::Uuid::new_v4().to_string(); let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24); diff --git a/src/api/server/passkeys.rs b/crates/tranquil-pds/src/api/server/passkeys.rs similarity index 100% rename from src/api/server/passkeys.rs rename to crates/tranquil-pds/src/api/server/passkeys.rs diff --git a/src/api/server/password.rs b/crates/tranquil-pds/src/api/server/password.rs similarity index 100% rename from src/api/server/password.rs rename to crates/tranquil-pds/src/api/server/password.rs diff --git a/src/api/server/reauth.rs b/crates/tranquil-pds/src/api/server/reauth.rs similarity index 100% rename from src/api/server/reauth.rs rename to crates/tranquil-pds/src/api/server/reauth.rs diff --git a/src/api/server/service_auth.rs b/crates/tranquil-pds/src/api/server/service_auth.rs similarity index 100% rename from src/api/server/service_auth.rs rename to crates/tranquil-pds/src/api/server/service_auth.rs diff --git a/src/api/server/session.rs b/crates/tranquil-pds/src/api/server/session.rs similarity index 99% rename from src/api/server/session.rs rename to crates/tranquil-pds/src/api/server/session.rs index 1a056aa..e4ad9da 100644 --- a/src/api/server/session.rs +++ b/crates/tranquil-pds/src/api/server/session.rs @@ -212,6 +212,7 @@ pub async fn create_session( &key_bytes, app_password_scopes.as_deref(), app_password_controller.as_deref(), + None, ) { Ok(m) => m, Err(e) => { @@ -489,6 +490,7 @@ pub async fn refresh_session( &key_bytes, session_row.scope.as_deref(), session_row.controller_did.as_deref(), + None, ) { Ok(m) => m, Err(e) => { @@ -1186,7 +1188,7 @@ pub async fn update_legacy_login_preference( } } -use crate::comms::locale::VALID_LOCALES; +use crate::comms::VALID_LOCALES; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/api/server/signing_key.rs b/crates/tranquil-pds/src/api/server/signing_key.rs similarity index 100% rename from src/api/server/signing_key.rs rename to crates/tranquil-pds/src/api/server/signing_key.rs diff --git a/src/api/server/totp.rs b/crates/tranquil-pds/src/api/server/totp.rs similarity index 99% rename from src/api/server/totp.rs rename to crates/tranquil-pds/src/api/server/totp.rs index 773240b..80b3120 100644 --- a/src/api/server/totp.rs +++ b/crates/tranquil-pds/src/api/server/totp.rs @@ -1,7 +1,7 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; use crate::auth::BearerAuth; -use crate::auth::totp::{ +use crate::auth::{ decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, verify_backup_code, verify_totp_code, diff --git a/src/api/server/trusted_devices.rs b/crates/tranquil-pds/src/api/server/trusted_devices.rs similarity index 100% rename from src/api/server/trusted_devices.rs rename to crates/tranquil-pds/src/api/server/trusted_devices.rs diff --git a/src/api/server/verify_email.rs b/crates/tranquil-pds/src/api/server/verify_email.rs similarity index 100% rename from src/api/server/verify_email.rs rename to crates/tranquil-pds/src/api/server/verify_email.rs diff --git a/src/api/server/verify_token.rs b/crates/tranquil-pds/src/api/server/verify_token.rs similarity index 100% rename from src/api/server/verify_token.rs rename to crates/tranquil-pds/src/api/server/verify_token.rs diff --git a/src/api/temp.rs b/crates/tranquil-pds/src/api/temp.rs similarity index 100% rename from src/api/temp.rs rename to crates/tranquil-pds/src/api/temp.rs diff --git a/src/api/validation.rs b/crates/tranquil-pds/src/api/validation.rs similarity index 100% rename from src/api/validation.rs rename to crates/tranquil-pds/src/api/validation.rs diff --git a/src/api/verification.rs b/crates/tranquil-pds/src/api/verification.rs similarity index 100% rename from src/api/verification.rs rename to crates/tranquil-pds/src/api/verification.rs diff --git a/src/appview/mod.rs b/crates/tranquil-pds/src/appview/mod.rs similarity index 100% rename from src/appview/mod.rs rename to crates/tranquil-pds/src/appview/mod.rs diff --git a/src/auth/extractor.rs b/crates/tranquil-pds/src/auth/extractor.rs similarity index 100% rename from src/auth/extractor.rs rename to crates/tranquil-pds/src/auth/extractor.rs diff --git a/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs similarity index 92% rename from src/auth/mod.rs rename to crates/tranquil-pds/src/auth/mod.rs index 1c50eb8..e6637be 100644 --- a/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -11,10 +11,7 @@ use crate::types::Did; pub mod extractor; pub mod scope_check; pub mod service; -pub mod token; -pub mod totp; pub mod verification_token; -pub mod verify; pub mod webauthn; pub use extractor::{ @@ -22,18 +19,30 @@ pub use extractor::{ extract_auth_token_from_header, extract_bearer_token_from_header, }; pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; -pub use token::{ - SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS, - TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenWithMetadata, create_access_token, + +pub use tranquil_auth::{ + ActClaim, Claims, Header, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, + SCOPE_REFRESH, TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenData, + TokenVerifyError, TokenWithMetadata, UnsafeClaims, create_access_token, + create_access_token_hs256, create_access_token_hs256_with_metadata, create_access_token_with_delegation, create_access_token_with_metadata, - create_access_token_with_scope_metadata, create_refresh_token, - create_refresh_token_with_metadata, create_service_token, -}; -pub use verify::{ - TokenVerifyError, get_did_from_token, get_jti_from_token, verify_access_token, - verify_access_token_typed, verify_refresh_token, verify_token, + create_access_token_with_scope_metadata, create_refresh_token, create_refresh_token_hs256, + create_refresh_token_hs256_with_metadata, create_refresh_token_with_metadata, + create_service_token, create_service_token_hs256, generate_backup_codes, + generate_qr_png_base64, generate_totp_secret, generate_totp_uri, get_algorithm_from_token, + get_did_from_token, get_jti_from_token, hash_backup_code, is_backup_code_format, + verify_access_token, verify_access_token_hs256, verify_access_token_typed, verify_backup_code, + verify_refresh_token, verify_refresh_token_hs256, verify_token, verify_totp_code, }; +pub fn encrypt_totp_secret(secret: &[u8]) -> Result, String> { + crate::config::encrypt_key(secret) +} + +pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result, String> { + crate::config::decrypt_key(encrypted, Some(version)) +} + const KEY_CACHE_TTL_SECS: u64 = 300; const SESSION_CACHE_TTL_SECS: u64 = 60; const USER_STATUS_CACHE_TTL_SECS: u64 = 60; @@ -347,10 +356,10 @@ async fn validate_bearer_token_with_options_internal( }); } } - Err(verify::TokenVerifyError::Expired) => { + Err(TokenVerifyError::Expired) => { return Err(TokenValidationError::TokenExpired); } - Err(verify::TokenVerifyError::Invalid) => {} + Err(TokenVerifyError::Invalid) => {} } } } @@ -492,40 +501,3 @@ pub async fn validate_token_with_dpop( Err(_) => Err(TokenValidationError::AuthenticationFailed), } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ActClaim { - pub sub: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - pub iss: String, - pub sub: String, - pub aud: String, - pub exp: usize, - pub iat: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub lxm: Option, - pub jti: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub act: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Header { - pub alg: String, - pub typ: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UnsafeClaims { - pub iss: String, - pub sub: Option, -} - -pub struct TokenData { - pub claims: T, -} diff --git a/src/auth/scope_check.rs b/crates/tranquil-pds/src/auth/scope_check.rs similarity index 98% rename from src/auth/scope_check.rs rename to crates/tranquil-pds/src/auth/scope_check.rs index c560a84..bf560a5 100644 --- a/src/auth/scope_check.rs +++ b/crates/tranquil-pds/src/auth/scope_check.rs @@ -7,7 +7,7 @@ use crate::oauth::scopes::{ AccountAction, AccountAttr, IdentityAttr, RepoAction, ScopePermissions, }; -use super::token::SCOPE_ACCESS; +use super::SCOPE_ACCESS; fn has_custom_scope(scope: Option<&str>) -> bool { match scope { diff --git a/src/auth/service.rs b/crates/tranquil-pds/src/auth/service.rs similarity index 100% rename from src/auth/service.rs rename to crates/tranquil-pds/src/auth/service.rs diff --git a/src/auth/verification_token.rs b/crates/tranquil-pds/src/auth/verification_token.rs similarity index 100% rename from src/auth/verification_token.rs rename to crates/tranquil-pds/src/auth/verification_token.rs diff --git a/src/auth/webauthn.rs b/crates/tranquil-pds/src/auth/webauthn.rs similarity index 100% rename from src/auth/webauthn.rs rename to crates/tranquil-pds/src/auth/webauthn.rs diff --git a/crates/tranquil-pds/src/cache/mod.rs b/crates/tranquil-pds/src/cache/mod.rs new file mode 100644 index 0000000..a15dbf2 --- /dev/null +++ b/crates/tranquil-pds/src/cache/mod.rs @@ -0,0 +1,4 @@ +pub use tranquil_cache::{ + Cache, CacheError, DistributedRateLimiter, NoOpCache, NoOpRateLimiter, RedisRateLimiter, + ValkeyCache, create_cache, +}; diff --git a/src/circuit_breaker.rs b/crates/tranquil-pds/src/circuit_breaker.rs similarity index 100% rename from src/circuit_breaker.rs rename to crates/tranquil-pds/src/circuit_breaker.rs diff --git a/src/comms/mod.rs b/crates/tranquil-pds/src/comms/mod.rs similarity index 52% rename from src/comms/mod.rs rename to crates/tranquil-pds/src/comms/mod.rs index bd26464..8632fea 100644 --- a/src/comms/mod.rs +++ b/crates/tranquil-pds/src/comms/mod.rs @@ -1,11 +1,10 @@ -pub mod locale; -mod sender; mod service; -mod types; -pub use sender::{ - CommsSender, DiscordSender, EmailSender, SendError, SignalSender, TelegramSender, - is_valid_phone_number, sanitize_header_value, +pub use tranquil_comms::{ + CommsChannel, CommsSender, CommsStatus, CommsType, DEFAULT_LOCALE, DiscordSender, EmailSender, + NewComms, NotificationStrings, QueuedComms, SendError, SignalSender, TelegramSender, + VALID_LOCALES, format_message, get_strings, is_valid_phone_number, mime_encode_header, + sanitize_header_value, validate_locale, }; pub use service::{ @@ -14,5 +13,3 @@ pub use service::{ enqueue_passkey_recovery, enqueue_password_reset, enqueue_plc_operation, enqueue_signup_verification, enqueue_welcome, queue_legacy_login_notification, }; - -pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms}; diff --git a/src/comms/service.rs b/crates/tranquil-pds/src/comms/service.rs similarity index 95% rename from src/comms/service.rs rename to crates/tranquil-pds/src/comms/service.rs index e75987f..57bf4d5 100644 --- a/src/comms/service.rs +++ b/crates/tranquil-pds/src/comms/service.rs @@ -7,12 +7,12 @@ use sqlx::PgPool; use tokio::sync::watch; use tokio::time::interval; use tracing::{debug, error, info, warn}; +use tranquil_comms::{ + CommsChannel, CommsSender, CommsStatus, CommsType, NewComms, QueuedComms, SendError, + format_message, get_strings, +}; use uuid::Uuid; -use super::locale::{format_message, get_strings}; -use super::sender::{CommsSender, SendError}; -use super::types::{CommsChannel, CommsStatus, NewComms, QueuedComms}; - pub struct CommsService { db: PgPool, senders: HashMap>, @@ -63,7 +63,7 @@ impl CommsService { "#, item.user_id, item.channel as CommsChannel, - item.comms_type as super::types::CommsType, + item.comms_type as CommsType, item.recipient, item.subject, item.body, @@ -140,7 +140,7 @@ impl CommsService { RETURNING id, user_id, channel as "channel: CommsChannel", - comms_type as "comms_type: super::types::CommsType", + comms_type as "comms_type: CommsType", status as "status: CommsStatus", recipient, subject, body, metadata, attempts, max_attempts, last_error, @@ -244,7 +244,7 @@ pub async fn enqueue_comms(db: &PgPool, item: NewComms) -> Result Result fn validate_scope( requested_scope: &Option, - client_metadata: &crate::oauth::client::ClientMetadata, + client_metadata: &crate::oauth::ClientMetadata, ) -> Result, OAuthError> { let scope_str = match requested_scope { Some(s) if !s.is_empty() => s, diff --git a/src/oauth/endpoints/token/grants.rs b/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs similarity index 97% rename from src/oauth/endpoints/token/grants.rs rename to crates/tranquil-pds/src/oauth/endpoints/token/grants.rs index 8be2b20..ba876ca 100644 --- a/src/oauth/endpoints/token/grants.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs @@ -3,11 +3,11 @@ use super::types::{TokenGrant, TokenResponse, ValidatedTokenRequest}; use crate::config::AuthConfig; use crate::delegation; use crate::oauth::{ - AuthFlowState, ClientAuth, OAuthError, RefreshToken, TokenData, TokenId, - client::{ClientMetadataCache, verify_client_auth}, + AuthFlowState, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, + TokenData, TokenId, db::{self, RefreshTokenLookup}, - dpop::DPoPVerifier, scopes::expand_include_scopes, + verify_client_auth, }; use crate::state::AppState; use axum::Json; @@ -110,8 +110,7 @@ pub async fn handle_authorization_code_grant( Some(result.jkt.as_str().to_string()) } else if auth_request.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() { return Err(OAuthError::UseDpopNonce( - crate::oauth::dpop::DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()) - .generate_nonce(), + DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()).generate_nonce(), )); } else { None diff --git a/src/oauth/endpoints/token/helpers.rs b/crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs similarity index 100% rename from src/oauth/endpoints/token/helpers.rs rename to crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs diff --git a/src/oauth/endpoints/token/introspect.rs b/crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs similarity index 100% rename from src/oauth/endpoints/token/introspect.rs rename to crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs diff --git a/src/oauth/endpoints/token/mod.rs b/crates/tranquil-pds/src/oauth/endpoints/token/mod.rs similarity index 100% rename from src/oauth/endpoints/token/mod.rs rename to crates/tranquil-pds/src/oauth/endpoints/token/mod.rs diff --git a/src/oauth/endpoints/token/types.rs b/crates/tranquil-pds/src/oauth/endpoints/token/types.rs similarity index 100% rename from src/oauth/endpoints/token/types.rs rename to crates/tranquil-pds/src/oauth/endpoints/token/types.rs diff --git a/crates/tranquil-pds/src/oauth/jwks.rs b/crates/tranquil-pds/src/oauth/jwks.rs new file mode 100644 index 0000000..d0f72ab --- /dev/null +++ b/crates/tranquil-pds/src/oauth/jwks.rs @@ -0,0 +1 @@ +pub use tranquil_crypto::{Jwk, JwkSet, create_jwk_set}; diff --git a/crates/tranquil-pds/src/oauth/mod.rs b/crates/tranquil-pds/src/oauth/mod.rs new file mode 100644 index 0000000..1f5f162 --- /dev/null +++ b/crates/tranquil-pds/src/oauth/mod.rs @@ -0,0 +1,20 @@ +pub mod db; +pub mod endpoints; +pub mod jwks; +pub mod scopes; +pub mod verify; + +pub use tranquil_oauth::{ + AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata, + AuthorizedClientData, ClientAuth, ClientMetadata, ClientMetadataCache, Code, DPoPClaims, + DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult, DeviceData, + DeviceId, JwkPublicKey, Jwks, OAuthClientMetadata, OAuthError, ParResponse, + ProtectedResourceMetadata, RefreshToken, RefreshTokenState, RequestData, RequestId, SessionId, + TokenData, TokenId, TokenRequest, TokenResponse, compute_access_token_hash, + compute_jwk_thumbprint, verify_client_auth, +}; + +pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions}; +pub use verify::{ + OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token, +}; diff --git a/crates/tranquil-pds/src/oauth/scopes/mod.rs b/crates/tranquil-pds/src/oauth/scopes/mod.rs new file mode 100644 index 0000000..5c1c24b --- /dev/null +++ b/crates/tranquil-pds/src/oauth/scopes/mod.rs @@ -0,0 +1,6 @@ +pub use tranquil_scopes::{ + AccountAction, AccountAttr, AccountScope, BlobScope, IdentityAttr, IdentityScope, IncludeScope, + ParsedScope, RepoAction, RepoScope, RpcScope, SCOPE_DEFINITIONS, ScopeCategory, + ScopeDefinition, ScopeError, ScopePermissions, expand_include_scopes, format_scope_for_display, + get_required_scopes, get_scope_definition, is_valid_scope, parse_scope, parse_scope_string, +}; diff --git a/src/oauth/verify.rs b/crates/tranquil-pds/src/oauth/verify.rs similarity index 99% rename from src/oauth/verify.rs rename to crates/tranquil-pds/src/oauth/verify.rs index 858fcb1..7f0aa00 100644 --- a/src/oauth/verify.rs +++ b/crates/tranquil-pds/src/oauth/verify.rs @@ -11,10 +11,9 @@ use sha2::Sha256; use sqlx::PgPool; use subtle::ConstantTimeEq; -use super::OAuthError; use super::db; -use super::dpop::DPoPVerifier; use super::scopes::ScopePermissions; +use super::{DPoPVerifier, OAuthError}; use crate::config::AuthConfig; use crate::state::AppState; diff --git a/src/plc/mod.rs b/crates/tranquil-pds/src/plc/mod.rs similarity index 100% rename from src/plc/mod.rs rename to crates/tranquil-pds/src/plc/mod.rs diff --git a/src/rate_limit.rs b/crates/tranquil-pds/src/rate_limit.rs similarity index 100% rename from src/rate_limit.rs rename to crates/tranquil-pds/src/rate_limit.rs diff --git a/crates/tranquil-pds/src/repo/mod.rs b/crates/tranquil-pds/src/repo/mod.rs new file mode 100644 index 0000000..32e1fe9 --- /dev/null +++ b/crates/tranquil-pds/src/repo/mod.rs @@ -0,0 +1,5 @@ +pub use tranquil_repo::{PostgresBlockStore, TrackingBlockStore}; + +pub mod tracking { + pub use tranquil_repo::TrackingBlockStore; +} diff --git a/src/scheduled.rs b/crates/tranquil-pds/src/scheduled.rs similarity index 99% rename from src/scheduled.rs rename to crates/tranquil-pds/src/scheduled.rs index f9903f5..4de0e16 100644 --- a/src/scheduled.rs +++ b/crates/tranquil-pds/src/scheduled.rs @@ -816,8 +816,8 @@ pub async fn generate_repo_car_from_user_blocks( .map_err(|e| format!("Failed to fetch repo: {}", e))? .ok_or_else(|| "Repository not found".to_string())?; - let actual_head_cid = Cid::from_str(&repo_root_cid_str) - .map_err(|e| format!("Invalid repo_root_cid: {}", e))?; + let actual_head_cid = + Cid::from_str(&repo_root_cid_str).map_err(|e| format!("Invalid repo_root_cid: {}", e))?; generate_repo_car(block_store, &actual_head_cid).await } diff --git a/src/state.rs b/crates/tranquil-pds/src/state.rs similarity index 100% rename from src/state.rs rename to crates/tranquil-pds/src/state.rs diff --git a/crates/tranquil-pds/src/storage/mod.rs b/crates/tranquil-pds/src/storage/mod.rs new file mode 100644 index 0000000..7b47dda --- /dev/null +++ b/crates/tranquil-pds/src/storage/mod.rs @@ -0,0 +1,3 @@ +pub use tranquil_storage::{ + BackupStorage, BlobStorage, S3BlobStorage, StorageError, StreamUploadResult, +}; diff --git a/src/sync/blob.rs b/crates/tranquil-pds/src/sync/blob.rs similarity index 100% rename from src/sync/blob.rs rename to crates/tranquil-pds/src/sync/blob.rs diff --git a/src/sync/car.rs b/crates/tranquil-pds/src/sync/car.rs similarity index 100% rename from src/sync/car.rs rename to crates/tranquil-pds/src/sync/car.rs diff --git a/src/sync/commit.rs b/crates/tranquil-pds/src/sync/commit.rs similarity index 100% rename from src/sync/commit.rs rename to crates/tranquil-pds/src/sync/commit.rs diff --git a/src/sync/crawl.rs b/crates/tranquil-pds/src/sync/crawl.rs similarity index 100% rename from src/sync/crawl.rs rename to crates/tranquil-pds/src/sync/crawl.rs diff --git a/src/sync/deprecated.rs b/crates/tranquil-pds/src/sync/deprecated.rs similarity index 100% rename from src/sync/deprecated.rs rename to crates/tranquil-pds/src/sync/deprecated.rs diff --git a/src/sync/firehose.rs b/crates/tranquil-pds/src/sync/firehose.rs similarity index 100% rename from src/sync/firehose.rs rename to crates/tranquil-pds/src/sync/firehose.rs diff --git a/src/sync/frame.rs b/crates/tranquil-pds/src/sync/frame.rs similarity index 100% rename from src/sync/frame.rs rename to crates/tranquil-pds/src/sync/frame.rs diff --git a/src/sync/import.rs b/crates/tranquil-pds/src/sync/import.rs similarity index 100% rename from src/sync/import.rs rename to crates/tranquil-pds/src/sync/import.rs diff --git a/src/sync/listener.rs b/crates/tranquil-pds/src/sync/listener.rs similarity index 100% rename from src/sync/listener.rs rename to crates/tranquil-pds/src/sync/listener.rs diff --git a/src/sync/mod.rs b/crates/tranquil-pds/src/sync/mod.rs similarity index 100% rename from src/sync/mod.rs rename to crates/tranquil-pds/src/sync/mod.rs diff --git a/src/sync/repo.rs b/crates/tranquil-pds/src/sync/repo.rs similarity index 100% rename from src/sync/repo.rs rename to crates/tranquil-pds/src/sync/repo.rs diff --git a/src/sync/subscribe_repos.rs b/crates/tranquil-pds/src/sync/subscribe_repos.rs similarity index 100% rename from src/sync/subscribe_repos.rs rename to crates/tranquil-pds/src/sync/subscribe_repos.rs diff --git a/src/sync/util.rs b/crates/tranquil-pds/src/sync/util.rs similarity index 100% rename from src/sync/util.rs rename to crates/tranquil-pds/src/sync/util.rs diff --git a/src/sync/verify.rs b/crates/tranquil-pds/src/sync/verify.rs similarity index 100% rename from src/sync/verify.rs rename to crates/tranquil-pds/src/sync/verify.rs diff --git a/src/sync/verify_tests.rs b/crates/tranquil-pds/src/sync/verify_tests.rs similarity index 100% rename from src/sync/verify_tests.rs rename to crates/tranquil-pds/src/sync/verify_tests.rs diff --git a/crates/tranquil-pds/src/types.rs b/crates/tranquil-pds/src/types.rs new file mode 100644 index 0000000..41fa026 --- /dev/null +++ b/crates/tranquil-pds/src/types.rs @@ -0,0 +1 @@ +pub use tranquil_types::*; diff --git a/src/util.rs b/crates/tranquil-pds/src/util.rs similarity index 100% rename from src/util.rs rename to crates/tranquil-pds/src/util.rs diff --git a/src/validation/mod.rs b/crates/tranquil-pds/src/validation/mod.rs similarity index 100% rename from src/validation/mod.rs rename to crates/tranquil-pds/src/validation/mod.rs diff --git a/tests/account_lifecycle.rs b/crates/tranquil-pds/tests/account_lifecycle.rs similarity index 100% rename from tests/account_lifecycle.rs rename to crates/tranquil-pds/tests/account_lifecycle.rs diff --git a/tests/account_notifications.rs b/crates/tranquil-pds/tests/account_notifications.rs similarity index 100% rename from tests/account_notifications.rs rename to crates/tranquil-pds/tests/account_notifications.rs diff --git a/tests/actor.rs b/crates/tranquil-pds/tests/actor.rs similarity index 100% rename from tests/actor.rs rename to crates/tranquil-pds/tests/actor.rs diff --git a/tests/admin_email.rs b/crates/tranquil-pds/tests/admin_email.rs similarity index 100% rename from tests/admin_email.rs rename to crates/tranquil-pds/tests/admin_email.rs diff --git a/tests/admin_invite.rs b/crates/tranquil-pds/tests/admin_invite.rs similarity index 100% rename from tests/admin_invite.rs rename to crates/tranquil-pds/tests/admin_invite.rs diff --git a/tests/admin_moderation.rs b/crates/tranquil-pds/tests/admin_moderation.rs similarity index 100% rename from tests/admin_moderation.rs rename to crates/tranquil-pds/tests/admin_moderation.rs diff --git a/tests/admin_search.rs b/crates/tranquil-pds/tests/admin_search.rs similarity index 100% rename from tests/admin_search.rs rename to crates/tranquil-pds/tests/admin_search.rs diff --git a/tests/admin_stats.rs b/crates/tranquil-pds/tests/admin_stats.rs similarity index 100% rename from tests/admin_stats.rs rename to crates/tranquil-pds/tests/admin_stats.rs diff --git a/tests/backup.rs b/crates/tranquil-pds/tests/backup.rs similarity index 100% rename from tests/backup.rs rename to crates/tranquil-pds/tests/backup.rs diff --git a/tests/banned_words.rs b/crates/tranquil-pds/tests/banned_words.rs similarity index 100% rename from tests/banned_words.rs rename to crates/tranquil-pds/tests/banned_words.rs diff --git a/tests/change_password.rs b/crates/tranquil-pds/tests/change_password.rs similarity index 100% rename from tests/change_password.rs rename to crates/tranquil-pds/tests/change_password.rs diff --git a/tests/commit_signing.rs b/crates/tranquil-pds/tests/commit_signing.rs similarity index 100% rename from tests/commit_signing.rs rename to crates/tranquil-pds/tests/commit_signing.rs diff --git a/tests/common/mod.rs b/crates/tranquil-pds/tests/common/mod.rs similarity index 100% rename from tests/common/mod.rs rename to crates/tranquil-pds/tests/common/mod.rs diff --git a/tests/delete_account.rs b/crates/tranquil-pds/tests/delete_account.rs similarity index 100% rename from tests/delete_account.rs rename to crates/tranquil-pds/tests/delete_account.rs diff --git a/tests/did_web.rs b/crates/tranquil-pds/tests/did_web.rs similarity index 100% rename from tests/did_web.rs rename to crates/tranquil-pds/tests/did_web.rs diff --git a/tests/dpop_unit.rs b/crates/tranquil-pds/tests/dpop_unit.rs similarity index 99% rename from tests/dpop_unit.rs rename to crates/tranquil-pds/tests/dpop_unit.rs index 4c76240..3aa9e56 100644 --- a/tests/dpop_unit.rs +++ b/crates/tranquil-pds/tests/dpop_unit.rs @@ -4,7 +4,7 @@ use chrono::Utc; use p256::ecdsa::{SigningKey, signature::Signer}; use serde_json::json; -use tranquil_pds::oauth::dpop::{ +use tranquil_pds::oauth::{ DPoPJwk, DPoPVerifier, compute_access_token_hash, compute_jwk_thumbprint, }; diff --git a/tests/email_update.rs b/crates/tranquil-pds/tests/email_update.rs similarity index 100% rename from tests/email_update.rs rename to crates/tranquil-pds/tests/email_update.rs diff --git a/tests/firehose_validation.rs b/crates/tranquil-pds/tests/firehose_validation.rs similarity index 100% rename from tests/firehose_validation.rs rename to crates/tranquil-pds/tests/firehose_validation.rs diff --git a/tests/helpers/mod.rs b/crates/tranquil-pds/tests/helpers/mod.rs similarity index 100% rename from tests/helpers/mod.rs rename to crates/tranquil-pds/tests/helpers/mod.rs diff --git a/tests/identity.rs b/crates/tranquil-pds/tests/identity.rs similarity index 100% rename from tests/identity.rs rename to crates/tranquil-pds/tests/identity.rs diff --git a/tests/image_processing.rs b/crates/tranquil-pds/tests/image_processing.rs similarity index 100% rename from tests/image_processing.rs rename to crates/tranquil-pds/tests/image_processing.rs diff --git a/tests/import_verification.rs b/crates/tranquil-pds/tests/import_verification.rs similarity index 100% rename from tests/import_verification.rs rename to crates/tranquil-pds/tests/import_verification.rs diff --git a/tests/import_with_verification.rs b/crates/tranquil-pds/tests/import_with_verification.rs similarity index 100% rename from tests/import_with_verification.rs rename to crates/tranquil-pds/tests/import_with_verification.rs diff --git a/tests/invite.rs b/crates/tranquil-pds/tests/invite.rs similarity index 100% rename from tests/invite.rs rename to crates/tranquil-pds/tests/invite.rs diff --git a/tests/jwt_security.rs b/crates/tranquil-pds/tests/jwt_security.rs similarity index 100% rename from tests/jwt_security.rs rename to crates/tranquil-pds/tests/jwt_security.rs diff --git a/tests/lifecycle_record.rs b/crates/tranquil-pds/tests/lifecycle_record.rs similarity index 100% rename from tests/lifecycle_record.rs rename to crates/tranquil-pds/tests/lifecycle_record.rs diff --git a/tests/lifecycle_session.rs b/crates/tranquil-pds/tests/lifecycle_session.rs similarity index 100% rename from tests/lifecycle_session.rs rename to crates/tranquil-pds/tests/lifecycle_session.rs diff --git a/tests/lifecycle_social.rs b/crates/tranquil-pds/tests/lifecycle_social.rs similarity index 100% rename from tests/lifecycle_social.rs rename to crates/tranquil-pds/tests/lifecycle_social.rs diff --git a/tests/moderation.rs b/crates/tranquil-pds/tests/moderation.rs similarity index 100% rename from tests/moderation.rs rename to crates/tranquil-pds/tests/moderation.rs diff --git a/tests/notifications.rs b/crates/tranquil-pds/tests/notifications.rs similarity index 100% rename from tests/notifications.rs rename to crates/tranquil-pds/tests/notifications.rs diff --git a/tests/oauth.rs b/crates/tranquil-pds/tests/oauth.rs similarity index 100% rename from tests/oauth.rs rename to crates/tranquil-pds/tests/oauth.rs diff --git a/tests/oauth_client_metadata.rs b/crates/tranquil-pds/tests/oauth_client_metadata.rs similarity index 100% rename from tests/oauth_client_metadata.rs rename to crates/tranquil-pds/tests/oauth_client_metadata.rs diff --git a/tests/oauth_lifecycle.rs b/crates/tranquil-pds/tests/oauth_lifecycle.rs similarity index 100% rename from tests/oauth_lifecycle.rs rename to crates/tranquil-pds/tests/oauth_lifecycle.rs diff --git a/tests/oauth_scopes.rs b/crates/tranquil-pds/tests/oauth_scopes.rs similarity index 100% rename from tests/oauth_scopes.rs rename to crates/tranquil-pds/tests/oauth_scopes.rs diff --git a/tests/oauth_security.rs b/crates/tranquil-pds/tests/oauth_security.rs similarity index 99% rename from tests/oauth_security.rs rename to crates/tranquil-pds/tests/oauth_security.rs index ea2f68a..a1349da 100644 --- a/tests/oauth_security.rs +++ b/crates/tranquil-pds/tests/oauth_security.rs @@ -8,7 +8,7 @@ use helpers::verify_new_account; use reqwest::StatusCode; use serde_json::{Value, json}; use sha2::{Digest, Sha256}; -use tranquil_pds::oauth::dpop::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint}; +use tranquil_pds::oauth::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; diff --git a/tests/password_reset.rs b/crates/tranquil-pds/tests/password_reset.rs similarity index 100% rename from tests/password_reset.rs rename to crates/tranquil-pds/tests/password_reset.rs diff --git a/tests/plc_migration.rs b/crates/tranquil-pds/tests/plc_migration.rs similarity index 100% rename from tests/plc_migration.rs rename to crates/tranquil-pds/tests/plc_migration.rs diff --git a/tests/plc_operations.rs b/crates/tranquil-pds/tests/plc_operations.rs similarity index 100% rename from tests/plc_operations.rs rename to crates/tranquil-pds/tests/plc_operations.rs diff --git a/tests/plc_validation.rs b/crates/tranquil-pds/tests/plc_validation.rs similarity index 100% rename from tests/plc_validation.rs rename to crates/tranquil-pds/tests/plc_validation.rs diff --git a/tests/rate_limit.rs b/crates/tranquil-pds/tests/rate_limit.rs similarity index 100% rename from tests/rate_limit.rs rename to crates/tranquil-pds/tests/rate_limit.rs diff --git a/tests/record_validation.rs b/crates/tranquil-pds/tests/record_validation.rs similarity index 100% rename from tests/record_validation.rs rename to crates/tranquil-pds/tests/record_validation.rs diff --git a/tests/repo_batch.rs b/crates/tranquil-pds/tests/repo_batch.rs similarity index 100% rename from tests/repo_batch.rs rename to crates/tranquil-pds/tests/repo_batch.rs diff --git a/tests/repo_blob.rs b/crates/tranquil-pds/tests/repo_blob.rs similarity index 100% rename from tests/repo_blob.rs rename to crates/tranquil-pds/tests/repo_blob.rs diff --git a/tests/repo_conformance.rs b/crates/tranquil-pds/tests/repo_conformance.rs similarity index 100% rename from tests/repo_conformance.rs rename to crates/tranquil-pds/tests/repo_conformance.rs diff --git a/tests/scope_edge_cases.rs b/crates/tranquil-pds/tests/scope_edge_cases.rs similarity index 100% rename from tests/scope_edge_cases.rs rename to crates/tranquil-pds/tests/scope_edge_cases.rs diff --git a/tests/security_fixes.rs b/crates/tranquil-pds/tests/security_fixes.rs similarity index 100% rename from tests/security_fixes.rs rename to crates/tranquil-pds/tests/security_fixes.rs diff --git a/tests/server.rs b/crates/tranquil-pds/tests/server.rs similarity index 100% rename from tests/server.rs rename to crates/tranquil-pds/tests/server.rs diff --git a/tests/session_management.rs b/crates/tranquil-pds/tests/session_management.rs similarity index 100% rename from tests/session_management.rs rename to crates/tranquil-pds/tests/session_management.rs diff --git a/tests/signing_key.rs b/crates/tranquil-pds/tests/signing_key.rs similarity index 100% rename from tests/signing_key.rs rename to crates/tranquil-pds/tests/signing_key.rs diff --git a/tests/sync_blob.rs b/crates/tranquil-pds/tests/sync_blob.rs similarity index 100% rename from tests/sync_blob.rs rename to crates/tranquil-pds/tests/sync_blob.rs diff --git a/tests/sync_conformance.rs b/crates/tranquil-pds/tests/sync_conformance.rs similarity index 100% rename from tests/sync_conformance.rs rename to crates/tranquil-pds/tests/sync_conformance.rs diff --git a/tests/sync_deprecated.rs b/crates/tranquil-pds/tests/sync_deprecated.rs similarity index 100% rename from tests/sync_deprecated.rs rename to crates/tranquil-pds/tests/sync_deprecated.rs diff --git a/tests/sync_repo.rs b/crates/tranquil-pds/tests/sync_repo.rs similarity index 100% rename from tests/sync_repo.rs rename to crates/tranquil-pds/tests/sync_repo.rs diff --git a/tests/validation_edge_cases.rs b/crates/tranquil-pds/tests/validation_edge_cases.rs similarity index 100% rename from tests/validation_edge_cases.rs rename to crates/tranquil-pds/tests/validation_edge_cases.rs diff --git a/tests/verify_live_commit.rs b/crates/tranquil-pds/tests/verify_live_commit.rs similarity index 100% rename from tests/verify_live_commit.rs rename to crates/tranquil-pds/tests/verify_live_commit.rs diff --git a/crates/tranquil-repo/.sqlx b/crates/tranquil-repo/.sqlx new file mode 120000 index 0000000..7575826 --- /dev/null +++ b/crates/tranquil-repo/.sqlx @@ -0,0 +1 @@ +../../.sqlx \ No newline at end of file diff --git a/crates/tranquil-repo/Cargo.toml b/crates/tranquil-repo/Cargo.toml new file mode 100644 index 0000000..77e2a16 --- /dev/null +++ b/crates/tranquil-repo/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tranquil-repo" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-types = { workspace = true } + +bytes = { workspace = true } +cid = { workspace = true } +jacquard-repo = { workspace = true } +multihash = { workspace = true } +sha2 = { workspace = true } +sqlx = { workspace = true } diff --git a/src/repo/mod.rs b/crates/tranquil-repo/src/lib.rs similarity index 52% rename from src/repo/mod.rs rename to crates/tranquil-repo/src/lib.rs index ec0cea4..6e44e10 100644 --- a/src/repo/mod.rs +++ b/crates/tranquil-repo/src/lib.rs @@ -6,8 +6,8 @@ use jacquard_repo::storage::BlockStore; use multihash::Multihash; use sha2::{Digest, Sha256}; use sqlx::PgPool; - -pub mod tracking; +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct PostgresBlockStore { @@ -18,11 +18,14 @@ impl PostgresBlockStore { pub fn new(pool: PgPool) -> Self { Self { pool } } + + pub fn pool(&self) -> &PgPool { + &self.pool + } } impl BlockStore for PostgresBlockStore { async fn get(&self, cid: &Cid) -> Result, RepoError> { - crate::metrics::record_block_operation("get"); let cid_bytes = cid.to_bytes(); let row = sqlx::query!("SELECT data FROM blocks WHERE cid = $1", &cid_bytes) .fetch_optional(&self.pool) @@ -35,7 +38,6 @@ impl BlockStore for PostgresBlockStore { } async fn put(&self, data: &[u8]) -> Result { - crate::metrics::record_block_operation("put"); let mut hasher = Sha256::new(); hasher.update(data); let hash = hasher.finalize(); @@ -59,7 +61,6 @@ impl BlockStore for PostgresBlockStore { } async fn has(&self, cid: &Cid) -> Result { - crate::metrics::record_block_operation("has"); let cid_bytes = cid.to_bytes(); let row = sqlx::query!("SELECT 1 as one FROM blocks WHERE cid = $1", &cid_bytes) .fetch_optional(&self.pool) @@ -76,7 +77,6 @@ impl BlockStore for PostgresBlockStore { if blocks.is_empty() { return Ok(()); } - crate::metrics::record_block_operation("put_many"); let cids: Vec> = blocks.iter().map(|(cid, _)| cid.to_bytes()).collect(); let data: Vec<&[u8]> = blocks.iter().map(|(_, d)| d.as_ref()).collect(); sqlx::query!( @@ -98,7 +98,6 @@ impl BlockStore for PostgresBlockStore { if cids.is_empty() { return Ok(Vec::new()); } - crate::metrics::record_block_operation("get_many"); let cid_bytes: Vec> = cids.iter().map(|c| c.to_bytes()).collect(); let rows = sqlx::query!( "SELECT cid, data FROM blocks WHERE cid = ANY($1)", @@ -123,3 +122,107 @@ impl BlockStore for PostgresBlockStore { Ok(()) } } + +#[derive(Clone)] +pub struct TrackingBlockStore { + inner: PostgresBlockStore, + written_cids: Arc>>, + read_cids: Arc>>, +} + +impl TrackingBlockStore { + pub fn new(store: PostgresBlockStore) -> Self { + Self { + inner: store, + written_cids: Arc::new(Mutex::new(Vec::new())), + read_cids: Arc::new(Mutex::new(HashSet::new())), + } + } + + pub fn get_written_cids(&self) -> Vec { + match self.written_cids.lock() { + Ok(guard) => guard.clone(), + Err(poisoned) => poisoned.into_inner().clone(), + } + } + + pub fn get_read_cids(&self) -> Vec { + match self.read_cids.lock() { + Ok(guard) => guard.iter().cloned().collect(), + Err(poisoned) => poisoned.into_inner().iter().cloned().collect(), + } + } + + pub fn get_all_relevant_cids(&self) -> Vec { + let written = self.get_written_cids(); + let read = self.get_read_cids(); + let mut all: HashSet = written.into_iter().collect(); + all.extend(read); + all.into_iter().collect() + } +} + +impl BlockStore for TrackingBlockStore { + async fn get(&self, cid: &Cid) -> Result, RepoError> { + let result = self.inner.get(cid).await?; + if result.is_some() { + match self.read_cids.lock() { + Ok(mut guard) => { + guard.insert(*cid); + } + Err(poisoned) => { + poisoned.into_inner().insert(*cid); + } + } + } + Ok(result) + } + + async fn put(&self, data: &[u8]) -> Result { + let cid = self.inner.put(data).await?; + match self.written_cids.lock() { + Ok(mut guard) => guard.push(cid), + Err(poisoned) => poisoned.into_inner().push(cid), + } + Ok(cid) + } + + async fn has(&self, cid: &Cid) -> Result { + self.inner.has(cid).await + } + + async fn put_many( + &self, + blocks: impl IntoIterator + Send, + ) -> Result<(), RepoError> { + let blocks: Vec<_> = blocks.into_iter().collect(); + let cids: Vec = blocks.iter().map(|(cid, _)| *cid).collect(); + self.inner.put_many(blocks).await?; + match self.written_cids.lock() { + Ok(mut guard) => guard.extend(cids), + Err(poisoned) => poisoned.into_inner().extend(cids), + } + Ok(()) + } + + async fn get_many(&self, cids: &[Cid]) -> Result>, RepoError> { + let results = self.inner.get_many(cids).await?; + cids.iter() + .zip(results.iter()) + .filter(|(_, result)| result.is_some()) + .for_each(|(cid, _)| match self.read_cids.lock() { + Ok(mut guard) => { + guard.insert(*cid); + } + Err(poisoned) => { + poisoned.into_inner().insert(*cid); + } + }); + Ok(results) + } + + async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> { + self.put_many(commit.blocks).await?; + Ok(()) + } +} diff --git a/crates/tranquil-scopes/Cargo.toml b/crates/tranquil-scopes/Cargo.toml new file mode 100644 index 0000000..985d823 --- /dev/null +++ b/crates/tranquil-scopes/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tranquil-scopes" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axum = { workspace = true } +futures = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/src/oauth/scopes/definitions.rs b/crates/tranquil-scopes/src/definitions.rs similarity index 100% rename from src/oauth/scopes/definitions.rs rename to crates/tranquil-scopes/src/definitions.rs diff --git a/src/oauth/scopes/error.rs b/crates/tranquil-scopes/src/error.rs similarity index 100% rename from src/oauth/scopes/error.rs rename to crates/tranquil-scopes/src/error.rs diff --git a/src/oauth/scopes/mod.rs b/crates/tranquil-scopes/src/lib.rs similarity index 69% rename from src/oauth/scopes/mod.rs rename to crates/tranquil-scopes/src/lib.rs index c6906e7..463daf2 100644 --- a/src/oauth/scopes/mod.rs +++ b/crates/tranquil-scopes/src/lib.rs @@ -4,7 +4,10 @@ mod parser; mod permission_set; mod permissions; -pub use definitions::{SCOPE_DEFINITIONS, ScopeCategory, ScopeDefinition}; +pub use definitions::{ + SCOPE_DEFINITIONS, ScopeCategory, ScopeDefinition, format_scope_for_display, + get_required_scopes, get_scope_definition, is_valid_scope, +}; pub use error::ScopeError; pub use parser::{ AccountAction, AccountAttr, AccountScope, BlobScope, IdentityAttr, IdentityScope, IncludeScope, diff --git a/src/oauth/scopes/parser.rs b/crates/tranquil-scopes/src/parser.rs similarity index 100% rename from src/oauth/scopes/parser.rs rename to crates/tranquil-scopes/src/parser.rs diff --git a/src/oauth/scopes/permission_set.rs b/crates/tranquil-scopes/src/permission_set.rs similarity index 100% rename from src/oauth/scopes/permission_set.rs rename to crates/tranquil-scopes/src/permission_set.rs diff --git a/src/oauth/scopes/permissions.rs b/crates/tranquil-scopes/src/permissions.rs similarity index 100% rename from src/oauth/scopes/permissions.rs rename to crates/tranquil-scopes/src/permissions.rs diff --git a/crates/tranquil-storage/Cargo.toml b/crates/tranquil-storage/Cargo.toml new file mode 100644 index 0000000..d4a7cb2 --- /dev/null +++ b/crates/tranquil-storage/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tranquil-storage" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-infra = { workspace = true } + +async-trait = { workspace = true } +aws-config = { workspace = true } +aws-sdk-s3 = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } diff --git a/src/storage/mod.rs b/crates/tranquil-storage/src/lib.rs similarity index 74% rename from src/storage/mod.rs rename to crates/tranquil-storage/src/lib.rs index 4374295..30f856c 100644 --- a/src/storage/mod.rs +++ b/crates/tranquil-storage/src/lib.rs @@ -1,3 +1,5 @@ +pub use tranquil_infra::{BlobStorage, StorageError, StreamUploadResult}; + use async_trait::async_trait; use aws_config::BehaviorVersion; use aws_config::meta::region::RegionProviderChain; @@ -9,41 +11,9 @@ use bytes::Bytes; use futures::Stream; use sha2::{Digest, Sha256}; use std::pin::Pin; -use thiserror::Error; const MIN_PART_SIZE: usize = 5 * 1024 * 1024; -#[derive(Error, Debug)] -pub enum StorageError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("S3 error: {0}")] - S3(String), - #[error("Other: {0}")] - Other(String), -} - -pub struct StreamUploadResult { - pub sha256_hash: [u8; 32], - pub size: u64, -} - -#[async_trait] -pub trait BlobStorage: Send + Sync { - async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError>; - async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError>; - async fn get(&self, key: &str) -> Result, StorageError>; - async fn get_bytes(&self, key: &str) -> Result; - async fn get_head(&self, key: &str, size: usize) -> Result; - async fn delete(&self, key: &str) -> Result<(), StorageError>; - async fn put_stream( - &self, - key: &str, - stream: Pin> + Send>>, - ) -> Result; - async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError>; -} - pub struct S3BlobStorage { client: Client, bucket: String, @@ -55,6 +25,11 @@ impl S3BlobStorage { let client = create_s3_client().await; Self { client, bucket } } + + pub async fn with_bucket(bucket: String) -> Self { + let client = create_s3_client().await; + Self { client, bucket } + } } async fn create_s3_client() -> Client { @@ -124,12 +99,8 @@ impl BackupStorage { .body(ByteStream::from(Bytes::copy_from_slice(data))) .send() .await - .map_err(|e| { - crate::metrics::record_s3_operation("backup_put", "error"); - StorageError::S3(e.to_string()) - })?; + .map_err(|e| StorageError::S3(e.to_string()))?; - crate::metrics::record_s3_operation("backup_put", "success"); Ok(key) } @@ -141,22 +112,15 @@ impl BackupStorage { .key(storage_key) .send() .await - .map_err(|e| { - crate::metrics::record_s3_operation("backup_get", "error"); - StorageError::S3(e.to_string()) - })?; + .map_err(|e| StorageError::S3(e.to_string()))?; let data = resp .body .collect() .await - .map_err(|e| { - crate::metrics::record_s3_operation("backup_get", "error"); - StorageError::S3(e.to_string()) - })? + .map_err(|e| StorageError::S3(e.to_string()))? .into_bytes(); - crate::metrics::record_s3_operation("backup_get", "success"); Ok(data) } @@ -167,12 +131,8 @@ impl BackupStorage { .key(storage_key) .send() .await - .map_err(|e| { - crate::metrics::record_s3_operation("backup_delete", "error"); - StorageError::S3(e.to_string()) - })?; + .map_err(|e| StorageError::S3(e.to_string()))?; - crate::metrics::record_s3_operation("backup_delete", "success"); Ok(()) } } @@ -184,22 +144,15 @@ impl BlobStorage for S3BlobStorage { } async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError> { - let result = self - .client + self.client .put_object() .bucket(&self.bucket) .key(key) .body(ByteStream::from(data)) .send() .await - .map_err(|e| StorageError::S3(e.to_string())); + .map_err(|e| StorageError::S3(e.to_string()))?; - match &result { - Ok(_) => crate::metrics::record_s3_operation("put", "success"), - Err(_) => crate::metrics::record_s3_operation("put", "error"), - } - - result?; Ok(()) } @@ -215,22 +168,15 @@ impl BlobStorage for S3BlobStorage { .key(key) .send() .await - .map_err(|e| { - crate::metrics::record_s3_operation("get", "error"); - StorageError::S3(e.to_string()) - })?; + .map_err(|e| StorageError::S3(e.to_string()))?; let data = resp .body .collect() .await - .map_err(|e| { - crate::metrics::record_s3_operation("get", "error"); - StorageError::S3(e.to_string()) - })? + .map_err(|e| StorageError::S3(e.to_string()))? .into_bytes(); - crate::metrics::record_s3_operation("get", "success"); Ok(data) } @@ -244,41 +190,27 @@ impl BlobStorage for S3BlobStorage { .range(range) .send() .await - .map_err(|e| { - crate::metrics::record_s3_operation("get_head", "error"); - StorageError::S3(e.to_string()) - })?; + .map_err(|e| StorageError::S3(e.to_string()))?; let data = resp .body .collect() .await - .map_err(|e| { - crate::metrics::record_s3_operation("get_head", "error"); - StorageError::S3(e.to_string()) - })? + .map_err(|e| StorageError::S3(e.to_string()))? .into_bytes(); - crate::metrics::record_s3_operation("get_head", "success"); Ok(data) } async fn delete(&self, key: &str) -> Result<(), StorageError> { - let result = self - .client + self.client .delete_object() .bucket(&self.bucket) .key(key) .send() .await - .map_err(|e| StorageError::S3(e.to_string())); + .map_err(|e| StorageError::S3(e.to_string()))?; - match &result { - Ok(_) => crate::metrics::record_s3_operation("delete", "success"), - Err(_) => crate::metrics::record_s3_operation("delete", "error"), - } - - result?; Ok(()) } @@ -423,8 +355,6 @@ impl BlobStorage for S3BlobStorage { .await .map_err(|e| StorageError::S3(format!("Failed to complete multipart upload: {}", e)))?; - crate::metrics::record_s3_operation("put_stream", "success"); - let hash: [u8; 32] = hasher.finalize().into(); Ok(StreamUploadResult { sha256_hash: hash, @@ -444,7 +374,6 @@ impl BlobStorage for S3BlobStorage { .await .map_err(|e| StorageError::S3(format!("Failed to copy object: {}", e)))?; - crate::metrics::record_s3_operation("copy", "success"); Ok(()) } } diff --git a/crates/tranquil-types/Cargo.toml b/crates/tranquil-types/Cargo.toml new file mode 100644 index 0000000..eae451a --- /dev/null +++ b/crates/tranquil-types/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tranquil-types" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +chrono = { workspace = true } +cid = { workspace = true } +jacquard = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +thiserror = { workspace = true } diff --git a/src/types.rs b/crates/tranquil-types/src/lib.rs similarity index 92% rename from src/types.rs rename to crates/tranquil-types/src/lib.rs index eb88a2c..67991ca 100644 --- a/src/types.rs +++ b/crates/tranquil-types/src/lib.rs @@ -123,21 +123,12 @@ impl FromStr for Did { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum DidError { + #[error("invalid DID: {0}")] Invalid(String), } -impl fmt::Display for DidError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid DID: {}", s), - } - } -} - -impl std::error::Error for DidError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] @@ -239,21 +230,12 @@ impl FromStr for Handle { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum HandleError { + #[error("invalid handle: {0}")] Invalid(String), } -impl fmt::Display for HandleError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid handle: {}", s), - } - } -} - -impl std::error::Error for HandleError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum AtIdentifier { Did(Did), @@ -370,21 +352,12 @@ impl<'de> Deserialize<'de> for AtIdentifier { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum AtIdentifierError { + #[error("invalid AT identifier: {0}")] Invalid(String), } -impl fmt::Display for AtIdentifierError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid AT identifier: {}", s), - } - } -} - -impl std::error::Error for AtIdentifierError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(type_name = "rkey")] @@ -495,21 +468,12 @@ impl FromStr for Rkey { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum RkeyError { + #[error("invalid rkey: {0}")] Invalid(String), } -impl fmt::Display for RkeyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid rkey: {}", s), - } - } -} - -impl std::error::Error for RkeyError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(type_name = "nsid")] @@ -624,21 +588,12 @@ impl FromStr for Nsid { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum NsidError { + #[error("invalid NSID: {0}")] Invalid(String), } -impl fmt::Display for NsidError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid NSID: {}", s), - } - } -} - -impl std::error::Error for NsidError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(type_name = "at_uri")] @@ -744,21 +699,12 @@ impl FromStr for AtUri { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum AtUriError { + #[error("invalid AT URI: {0}")] Invalid(String), } -impl fmt::Display for AtUriError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid AT URI: {}", s), - } - } -} - -impl std::error::Error for AtUriError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] @@ -865,21 +811,12 @@ impl FromStr for Tid { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum TidError { + #[error("invalid TID: {0}")] Invalid(String), } -impl fmt::Display for TidError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid TID: {}", s), - } - } -} - -impl std::error::Error for TidError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] @@ -990,21 +927,12 @@ impl FromStr for Datetime { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum DatetimeError { + #[error("invalid datetime: {0}")] Invalid(String), } -impl fmt::Display for DatetimeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid datetime: {}", s), - } - } -} - -impl std::error::Error for DatetimeError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] @@ -1107,21 +1035,12 @@ impl FromStr for Language { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum LanguageError { + #[error("invalid language tag: {0}")] Invalid(String), } -impl fmt::Display for LanguageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid language tag: {}", s), - } - } -} - -impl std::error::Error for LanguageError {} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] @@ -1227,21 +1146,12 @@ impl FromStr for CidLink { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum CidLinkError { + #[error("invalid CID: {0}")] Invalid(String), } -impl fmt::Display for CidLinkError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(s) => write!(f, "invalid CID: {}", s), - } - } -} - -impl std::error::Error for CidLinkError {} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccountState { Active, diff --git a/src/oauth/mod.rs b/src/oauth/mod.rs deleted file mode 100644 index 8b20c0a..0000000 --- a/src/oauth/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub mod client; -pub mod db; -pub mod dpop; -pub mod endpoints; -pub mod error; -pub mod jwks; -pub mod scopes; -pub mod types; -pub mod verify; - -pub use error::OAuthError; -pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions}; -pub use types::*; -pub use verify::{ - OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token, -}; diff --git a/src/repo/tracking.rs b/src/repo/tracking.rs deleted file mode 100644 index 872c450..0000000 --- a/src/repo/tracking.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::repo::PostgresBlockStore; -use bytes::Bytes; -use cid::Cid; -use jacquard_repo::error::RepoError; -use jacquard_repo::repo::CommitData; -use jacquard_repo::storage::BlockStore; -use std::collections::HashSet; -use std::sync::{Arc, Mutex}; - -#[derive(Clone)] -pub struct TrackingBlockStore { - inner: PostgresBlockStore, - written_cids: Arc>>, - read_cids: Arc>>, -} - -impl TrackingBlockStore { - pub fn new(store: PostgresBlockStore) -> Self { - Self { - inner: store, - written_cids: Arc::new(Mutex::new(Vec::new())), - read_cids: Arc::new(Mutex::new(HashSet::new())), - } - } - - pub fn get_written_cids(&self) -> Vec { - match self.written_cids.lock() { - Ok(guard) => guard.clone(), - Err(poisoned) => poisoned.into_inner().clone(), - } - } - - pub fn get_read_cids(&self) -> Vec { - match self.read_cids.lock() { - Ok(guard) => guard.iter().cloned().collect(), - Err(poisoned) => poisoned.into_inner().iter().cloned().collect(), - } - } - - pub fn get_all_relevant_cids(&self) -> Vec { - let written = self.get_written_cids(); - let read = self.get_read_cids(); - let mut all: HashSet = written.into_iter().collect(); - all.extend(read); - all.into_iter().collect() - } -} - -impl BlockStore for TrackingBlockStore { - async fn get(&self, cid: &Cid) -> Result, RepoError> { - let result = self.inner.get(cid).await?; - if result.is_some() { - match self.read_cids.lock() { - Ok(mut guard) => { - guard.insert(*cid); - } - Err(poisoned) => { - poisoned.into_inner().insert(*cid); - } - } - } - Ok(result) - } - - async fn put(&self, data: &[u8]) -> Result { - let cid = self.inner.put(data).await?; - match self.written_cids.lock() { - Ok(mut guard) => guard.push(cid), - Err(poisoned) => poisoned.into_inner().push(cid), - } - Ok(cid) - } - - async fn has(&self, cid: &Cid) -> Result { - self.inner.has(cid).await - } - - async fn put_many( - &self, - blocks: impl IntoIterator + Send, - ) -> Result<(), RepoError> { - let blocks: Vec<_> = blocks.into_iter().collect(); - let cids: Vec = blocks.iter().map(|(cid, _)| *cid).collect(); - self.inner.put_many(blocks).await?; - match self.written_cids.lock() { - Ok(mut guard) => guard.extend(cids), - Err(poisoned) => poisoned.into_inner().extend(cids), - } - Ok(()) - } - - async fn get_many(&self, cids: &[Cid]) -> Result>, RepoError> { - let results = self.inner.get_many(cids).await?; - for (cid, result) in cids.iter().zip(results.iter()) { - if result.is_some() { - match self.read_cids.lock() { - Ok(mut guard) => { - guard.insert(*cid); - } - Err(poisoned) => { - poisoned.into_inner().insert(*cid); - } - } - } - } - Ok(results) - } - - async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> { - self.put_many(commit.blocks).await?; - Ok(()) - } -}