mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-24 18:26:06 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72fa88d79a | ||
|
|
596b9b15fd | ||
|
|
59934cc184 | ||
|
|
34a47e6e5a | ||
|
|
aca78bb8d3 |
@@ -4,6 +4,7 @@
|
||||
|
||||
In order of importance:
|
||||
|
||||
- If your change involves how Tranquil implements atproto make sure its correct! See more below.
|
||||
- **You must run your change! Every contribution that says "here's xyz. untested." does not help the project.**
|
||||
- Relevant tests to your PR must pass. The whole suite doesn't have to be proven to have run, because there are a *ton* of tests and they're quite heavy, but hopefully there are existing tests for whatever you're PRing, and if there aren't, please add those too.
|
||||
- Run cargo fmt :P
|
||||
@@ -16,6 +17,60 @@ Things that would also be nice but aren't like, a pain in our side:
|
||||
|
||||
- Big changes should be stacked PRs that are broken up into digestible pieces. Those stacked PRs should hopefully be able to be merged individually if necessary.
|
||||
|
||||
### How we define a "correct" PDS implementation
|
||||
|
||||
The atproto specs are notoriously imprecise, ambiguious,
|
||||
lacks specifications for large parts of the protocol and network (even including what implementing a PDS entails!)
|
||||
and is generally none specific.
|
||||
This is bad.
|
||||
We won't waste time here describing all the ways in which that is problematic,
|
||||
the important thing for Tranquil is that this means that "follows spec" is not sufficient to describe a "correct" PDS implementation.
|
||||
Thus we need to come up with a description of "correct".
|
||||
In order of importance the following rules describe what "correct" means for Tranquil:
|
||||
|
||||
- The specs take precedence.
|
||||
If the spec *is* specific enough then follow it.
|
||||
Even if the reference implementation doesn't.
|
||||
- If the specs aren't sufficiently specific
|
||||
rely on the reference implementation, potential supporting documents or discussions,
|
||||
and/or community sentiment or common sense.
|
||||
If the matter is still debated and/or PBCs opinion differs from community sentiment we generally side with the community.
|
||||
- Examples here include what features and APIs to implement,
|
||||
here we look at what the reference implementation implements
|
||||
as well as https://github.com/bluesky-social/atproto/discussions/2350 as a supporting document.
|
||||
Another example is whether `include` scopes are allowed to use a `*` `aud` parameter.
|
||||
Discussion here has happened in https://github.com/bluesky-social/atproto/issues/4490.
|
||||
PBC has voiced an opinion that this should be disallowed,
|
||||
community sentiment seems to strongly lean to allowing it. Tranquil allows it.
|
||||
- Please mark locations like this with a `// SPECAMB: ...` comment explaining the ambiguity
|
||||
and what parts of the reference implementation and/or supporting documents have been used as reference.
|
||||
- If the reference implementation has behaviour that is only ever relevant for the Bluesky application.
|
||||
Implementions of such behaviour **must** be gated behind a `bsky-support` cargo feature of the implementing crate.
|
||||
- Examples here include bluesky feedgen specific service proxying behaviour,
|
||||
the `app.bsky.actor.getPreferences` and `app.bsky.actor.putPreferences` APIs,
|
||||
and special handling of the `X-BSKY-TOPICS` HTTP header during service proxying.
|
||||
- Please add a comment next to these implementations with an explanation of the behaviour.
|
||||
- Most of these behaviours are required for proper functioning of the official Bluesky client, though not all.
|
||||
If the behaviour isn't required for the official client consider not implementing it.
|
||||
- One such behaviour that we have a *hard rule* to never implement is default proxying to a configured Bluesky appview
|
||||
for `app.bsky.*` APIs and as fallback for `com.atproto.repo.getRecord`.
|
||||
Many third-party Bluesky clients rely on this behaviour, the official client used to do the same but does not anymore.
|
||||
Third-party clients breaking because they don't specify an `atproto-proxy` header is thus *not* a Tranquil bug but a bug in said clients.
|
||||
- Bluesky is the only application that will ever recieve application specific behaviour like this.
|
||||
It does so only because such a big section of atproto usage is Bluesky
|
||||
and because Bluesky is the only application that can practically rely on application specific behaviour.
|
||||
Application specific behaviour for other applications may still be added to Tranquil if such behaviour is a Tranquil feature,
|
||||
for example for Tranquils rudimentary banned content moderation feature,
|
||||
and not something said application relies on for proper functioning.
|
||||
|
||||
There is bound to be edge cases that these rules don't fully cover.
|
||||
Here common sense, community sentiment, furthering the goals of atproto itself, and ultimately maintainer opinion take precedence over support for any individual applicaion.
|
||||
Even Bluesky.
|
||||
|
||||
The rules above are meant to capture Tranquils goals of being correct while being community oriented and avoiding as much "Bluesky-defaultism" as possible.
|
||||
Tranquil is a community atproto PDS, *not* a company-led Bluesky (or other atproto app) PDS.
|
||||
See also "Tranquil & the world" in docs/1_WELCOME_TO_TRANQUIL_PDS.md.
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Generated
+22
-75
@@ -2486,7 +2486,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7630,9 +7629,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-api"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"backon",
|
||||
"base32",
|
||||
@@ -7666,22 +7664,19 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tranquil-config",
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-lexicon",
|
||||
"tranquil-pds",
|
||||
"tranquil-scopes",
|
||||
"tranquil-signal",
|
||||
"tranquil-types",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-auth"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base32",
|
||||
@@ -7705,7 +7700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-cache"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -7720,7 +7715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-comms"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -7734,7 +7729,6 @@ dependencies = [
|
||||
"rsa",
|
||||
"secrecy",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -7746,15 +7740,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-config"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"confique",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-crypto"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
@@ -7770,7 +7763,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -7787,7 +7780,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db-traits"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -7803,18 +7796,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-infra"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures",
|
||||
"thiserror 2.0.18",
|
||||
"tranquil-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-lexicon"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -7834,7 +7826,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -7857,7 +7849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth-server"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7892,7 +7884,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-pds"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -7904,7 +7896,6 @@ dependencies = [
|
||||
"base32",
|
||||
"base64 0.22.1",
|
||||
"bcrypt",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ciborium",
|
||||
@@ -7920,7 +7911,6 @@ dependencies = [
|
||||
"hmac",
|
||||
"http 1.4.0",
|
||||
"image",
|
||||
"infer",
|
||||
"ipld-core",
|
||||
"iroh-car",
|
||||
"jacquard-common",
|
||||
@@ -7962,7 +7952,6 @@ dependencies = [
|
||||
"tranquil-cache",
|
||||
"tranquil-comms",
|
||||
"tranquil-config",
|
||||
"tranquil-crypto",
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-lexicon",
|
||||
@@ -7981,12 +7970,11 @@ dependencies = [
|
||||
"webauthn-rs",
|
||||
"webauthn-rs-proto",
|
||||
"wiremock",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-repo"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cid",
|
||||
@@ -7998,7 +7986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-ripple"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
@@ -8027,7 +8015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-scopes"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
@@ -8044,7 +8032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-server"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"axum",
|
||||
@@ -8081,7 +8069,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-signal"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8089,7 +8077,6 @@ dependencies = [
|
||||
"futures",
|
||||
"presage",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
@@ -8097,14 +8084,13 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tranquil-signal",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-storage"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
@@ -8121,7 +8107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-store"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -8161,7 +8147,6 @@ dependencies = [
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-oauth",
|
||||
"tranquil-repo",
|
||||
"tranquil-store",
|
||||
"tranquil-types",
|
||||
"uuid",
|
||||
@@ -8170,7 +8155,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-sync"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -8192,7 +8177,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-types"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -8255,12 +8240,6 @@ version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
@@ -9462,20 +9441,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "7.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
"typed-path",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zkcredential"
|
||||
version = "0.1.0"
|
||||
@@ -9524,30 +9489,12 @@ dependencies = [
|
||||
"zkcredential",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
|
||||
+1
-3
@@ -26,7 +26,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -47,7 +47,6 @@ tranquil-db = { path = "crates/tranquil-db" }
|
||||
tranquil-ripple = { path = "crates/tranquil-ripple" }
|
||||
tranquil-lexicon = { path = "crates/tranquil-lexicon" }
|
||||
tranquil-pds = { path = "crates/tranquil-pds" }
|
||||
tranquil-server = { path = "crates/tranquil-server" }
|
||||
tranquil-sync = { path = "crates/tranquil-sync" }
|
||||
tranquil-oauth-server = { path = "crates/tranquil-oauth-server" }
|
||||
tranquil-api = { path = "crates/tranquil-api" }
|
||||
@@ -142,7 +141,6 @@ uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng", "serde"] }
|
||||
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys", "conditional-ui"] }
|
||||
webauthn-rs-proto = "0.5"
|
||||
x509-parser = "0.18"
|
||||
zip = { version = "7.0", default-features = false, features = ["deflate"] }
|
||||
|
||||
ciborium = "0.2"
|
||||
ctor = "0.6"
|
||||
|
||||
@@ -8,13 +8,10 @@ license.workspace = true
|
||||
tranquil-pds = { workspace = true }
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-db = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
tranquil-lexicon = { workspace = true, features = ["resolve"] }
|
||||
tranquil-scopes = { workspace = true }
|
||||
tranquil-signal = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
@@ -50,4 +47,7 @@ tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[features]
|
||||
bsky = ["bsky-support"]
|
||||
bsky-support = []
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod preferences;
|
||||
|
||||
pub use preferences::{get_preferences, put_preferences};
|
||||
@@ -205,20 +205,24 @@ pub async fn sequence_new_account(
|
||||
{
|
||||
tracing::warn!("Failed to sequence sync event for {}: {}", did, e);
|
||||
}
|
||||
let profile_record = serde_json::json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": display_name
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
state,
|
||||
did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
// TODO: make this configurable and also deduplicate with tranquil-oauth-server/src/sso_endpoints.rs:1210
|
||||
#[cfg(feature = "bsky")]
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
let profile_record = serde_json::json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": display_name
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
state,
|
||||
did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
pub mod actor;
|
||||
// BSKY: Bluesky requires PDSs to implement its private preferences API
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub mod actor {
|
||||
mod preferences;
|
||||
|
||||
pub use preferences::{get_preferences, put_preferences};
|
||||
}
|
||||
pub mod admin;
|
||||
#[cfg(feature = "bsky")]
|
||||
pub mod age_assurance;
|
||||
pub mod common;
|
||||
pub mod delegation;
|
||||
@@ -21,7 +28,7 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
let blob_body_limit =
|
||||
DefaultBodyLimit::max(tranquil_config::get().server.max_blob_size as usize);
|
||||
|
||||
axum::Router::new()
|
||||
let router = axum::Router::new()
|
||||
.route("/_health", get(server::health))
|
||||
.route(
|
||||
"/com.atproto.server.describeServer",
|
||||
@@ -373,14 +380,6 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
post(admin::update_subject_status),
|
||||
)
|
||||
.route("/com.atproto.admin.sendEmail", post(admin::send_email))
|
||||
.route(
|
||||
"/app.bsky.actor.getPreferences",
|
||||
get(actor::get_preferences),
|
||||
)
|
||||
.route(
|
||||
"/app.bsky.actor.putPreferences",
|
||||
post(actor::put_preferences),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.temp.checkSignupQueue",
|
||||
get(temp::check_signup_queue),
|
||||
@@ -438,7 +437,21 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
.route(
|
||||
"/_delegation.resolveController",
|
||||
get(delegation::resolve_controller),
|
||||
);
|
||||
|
||||
#[cfg(feature = "bsky-support")]
|
||||
let router = router
|
||||
.route(
|
||||
"/app.bsky.actor.getPreferences",
|
||||
get(actor::get_preferences),
|
||||
)
|
||||
.route(
|
||||
"/app.bsky.actor.putPreferences",
|
||||
post(actor::put_preferences),
|
||||
);
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
let router = router
|
||||
.route(
|
||||
"/app.bsky.ageassurance.getState",
|
||||
get(age_assurance::get_state),
|
||||
@@ -446,7 +459,9 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
.route(
|
||||
"/app.bsky.unspecced.getAgeAssuranceState",
|
||||
get(age_assurance::get_age_assurance_state),
|
||||
)
|
||||
);
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
pub fn well_known_api_routes() -> axum::Router<AppState> {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub use tranquil_pds::repo_ops::*;
|
||||
@@ -1,31 +0,0 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tranquil_pds::api::SuccessResponse;
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfirmChannelVerificationInput {
|
||||
pub channel: tranquil_db_traits::CommsChannel,
|
||||
pub identifier: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
pub async fn confirm_channel_verification(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<ConfirmChannelVerificationInput>,
|
||||
) -> Response {
|
||||
let token_input = crate::server::VerifyTokenInput {
|
||||
token: input.code,
|
||||
identifier: input.identifier,
|
||||
};
|
||||
|
||||
match crate::server::verify_token_internal(&state, token_input).await {
|
||||
Ok(_output) => SuccessResponse::ok().into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
valkey = ["dep:redis"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -19,7 +19,6 @@ reqwest = { workspace = true }
|
||||
rsa = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -6,4 +6,3 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
confique = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -5,9 +5,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["postgres"]
|
||||
postgres = []
|
||||
sqlite = []
|
||||
|
||||
[dependencies]
|
||||
tranquil-db-traits = { workspace = true }
|
||||
|
||||
@@ -5,8 +5,6 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
resolve = ["dep:reqwest", "dep:hickory-resolver", "dep:tokio", "dep:parking_lot", "dep:tracing", "dep:urlencoding"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -37,3 +37,6 @@ webauthn-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[features]
|
||||
bsky = []
|
||||
|
||||
@@ -1209,20 +1209,23 @@ pub async fn complete_registration(
|
||||
tracing::warn!("Failed to sequence account event for {}: {}", did, e);
|
||||
}
|
||||
|
||||
let profile_record = json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle.as_str()
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
#[cfg(feature = "bsky")]
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
let profile_record = json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle.as_str()
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to create default profile for {}: {}", did, e);
|
||||
};
|
||||
}
|
||||
|
||||
let app_password = generate_app_password();
|
||||
|
||||
@@ -7,7 +7,6 @@ license.workspace = true
|
||||
[dependencies]
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-crypto = { workspace = true }
|
||||
tranquil-storage = { workspace = true }
|
||||
tranquil-cache = { workspace = true }
|
||||
tranquil-repo = { workspace = true }
|
||||
@@ -29,13 +28,11 @@ axum = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
governor = { workspace = true }
|
||||
hickory-resolver = { workspace = true }
|
||||
@@ -43,7 +40,6 @@ hkdf = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
http = { workspace = true }
|
||||
image = { workspace = true }
|
||||
infer = { workspace = true }
|
||||
ipld-core = { workspace = true }
|
||||
iroh-car = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
@@ -57,7 +53,6 @@ multihash = { workspace = true }
|
||||
p256 = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true, optional = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -70,7 +65,6 @@ sqlx = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
@@ -80,16 +74,14 @@ urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-s3 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["frontend", "s3", "valkey"]
|
||||
bsky = ["bsky-support"]
|
||||
bsky-support = []
|
||||
external-infra = []
|
||||
s3-storage = ["tranquil-storage/s3", "dep:aws-config", "dep:aws-sdk-s3"]
|
||||
s3 = ["s3-storage"]
|
||||
valkey = ["tranquil-cache/valkey", "dep:redis"]
|
||||
postgres = ["tranquil-db/postgres"]
|
||||
s3 = ["tranquil-storage/s3"]
|
||||
valkey = ["tranquil-cache/valkey"]
|
||||
frontend = []
|
||||
native-tls-roots = ["tranquil-oauth/native-tls-roots"]
|
||||
|
||||
@@ -105,3 +97,8 @@ tranquil-api = { workspace = true }
|
||||
tranquil-oauth-server = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
wiremock = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
aws-config = { workspace = true }
|
||||
aws-sdk-s3 = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
|
||||
@@ -19,9 +19,7 @@ use tower::{Service, util::BoxCloneSyncService};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
static PROTECTED_METHODS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
[
|
||||
"app.bsky.actor.getPreferences",
|
||||
"app.bsky.actor.putPreferences",
|
||||
let mut methods: HashSet<&str> = [
|
||||
"com.atproto.admin.deleteAccount",
|
||||
"com.atproto.admin.disableAccountInvites",
|
||||
"com.atproto.admin.disableInviteCodes",
|
||||
@@ -103,7 +101,13 @@ static PROTECTED_METHODS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"com.atproto.temp.dereferenceScope",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
.collect();
|
||||
// BSKY: the Bluesky preferences API must be implemented by PDSs
|
||||
if cfg!(feature = "bsky-support") {
|
||||
methods.insert("app.bsky.actor.getPreferences");
|
||||
methods.insert("app.bsky.actor.putPreferences");
|
||||
};
|
||||
methods
|
||||
});
|
||||
|
||||
fn is_protected_method(method: &str) -> bool {
|
||||
@@ -111,6 +115,7 @@ fn is_protected_method(method: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Fetch the `feed` generator record from the AppView and return its `did`.
|
||||
#[cfg(feature = "bsky-support")]
|
||||
async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<Did> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetFeedQuery {
|
||||
@@ -201,6 +206,7 @@ impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Reque
|
||||
}
|
||||
|
||||
// If the age assurance override is set and this is an age assurance call then we dont want to proxy even if the client requests it
|
||||
#[cfg(feature = "bsky")]
|
||||
if tranquil_config::get().server.age_assurance_override
|
||||
&& (path.ends_with("app.bsky.ageassurance.getState")
|
||||
|| path.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
|
||||
@@ -328,27 +334,28 @@ async fn proxy_handler(
|
||||
},
|
||||
};
|
||||
|
||||
// getFeed must be audienced to the feed generator, not the AppView.
|
||||
let (token_aud, token_lxm) = if method == "app.bsky.feed.getFeed" {
|
||||
match resolve_feed_generator_did(&resolved.url, query.as_deref()).await {
|
||||
Some(feed_did) => (
|
||||
feed_did,
|
||||
"app.bsky.feed.getFeedSkeleton"
|
||||
.parse::<Nsid>()
|
||||
.expect("getFeedSkeleton is a valid NSID"),
|
||||
),
|
||||
None => {
|
||||
warn!(
|
||||
"getFeed proxy: could not resolve feed generator DID; refusing \
|
||||
// BSKY: getFeed must be audienced to the feed generator, not the AppView.
|
||||
let (token_aud, token_lxm) =
|
||||
if cfg!(feature = "bsky-support") && method == "app.bsky.feed.getFeed" {
|
||||
match resolve_feed_generator_did(&resolved.url, query.as_deref()).await {
|
||||
Some(feed_did) => (
|
||||
feed_did,
|
||||
"app.bsky.feed.getFeedSkeleton"
|
||||
.parse::<Nsid>()
|
||||
.expect("getFeedSkeleton is a valid NSID"),
|
||||
),
|
||||
None => {
|
||||
warn!(
|
||||
"getFeed proxy: could not resolve feed generator DID; refusing \
|
||||
to mint an AppView-audienced token"
|
||||
);
|
||||
return ApiError::InvalidRequest("Could not resolve feed".into())
|
||||
.into_response();
|
||||
);
|
||||
return ApiError::InvalidRequest("Could not resolve feed".into())
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(resolved.did.clone(), method_nsid.clone())
|
||||
};
|
||||
} else {
|
||||
(resolved.did.clone(), method_nsid.clone())
|
||||
};
|
||||
|
||||
match crate::auth::create_service_token(
|
||||
&auth_user.did,
|
||||
|
||||
@@ -147,7 +147,9 @@ impl std::fmt::Display for SsrfError {
|
||||
}
|
||||
|
||||
impl std::error::Error for SsrfError {}
|
||||
|
||||
// TODO: update to match https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/pipethrough.ts
|
||||
// Currently spec says nothing at all!! about forwarding headers during proxying and https://github.com/bluesky-social/atproto/discussions/2350
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 4]> = LazyLock::new(|| {
|
||||
[
|
||||
HeaderName::from_static("accept-language"),
|
||||
@@ -156,6 +158,14 @@ pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 4]> = LazyLock::new(|| {
|
||||
http::header::CONTENT_TYPE,
|
||||
]
|
||||
});
|
||||
#[cfg(not(feature = "bsky-support"))]
|
||||
pub static HEADERS_TO_FORWARD: LazyLock<[HeaderName; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
HeaderName::from_static("accept-language"),
|
||||
crate::util::HEADER_ATPROTO_ACCEPT_LABELERS,
|
||||
http::header::CONTENT_TYPE,
|
||||
]
|
||||
});
|
||||
pub static RESPONSE_HEADERS_TO_FORWARD: LazyLock<[HeaderName; 6]> = LazyLock::new(|| {
|
||||
[
|
||||
crate::util::HEADER_ATPROTO_REPO_REV,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// TODO: make all of this configurable.
|
||||
// PDS implementation should not impose any reserved domains imo.
|
||||
// some of these are even bad to have as defaults let alone non-configurables
|
||||
const ATP_SPECIFIC: &[&str] = &[
|
||||
"at", "atp", "plc", "pds", "did", "repo", "tid", "nsid", "xrpc", "lex", "lexicon", "bsky",
|
||||
"bluesky", "handle",
|
||||
|
||||
@@ -115,6 +115,10 @@ pub fn extract_blob_cids(record: &Value) -> Vec<crate::types::CidLink> {
|
||||
use crate::types::AtUri;
|
||||
use tranquil_db_traits::{Backlink, BacklinkPath};
|
||||
|
||||
// TODO: it really really should not be necessary to extract backlinks and store those.
|
||||
// especially not in a way that isnt generic.
|
||||
// figure out what the fuck is going on here
|
||||
// (lewis do you remember why you did this???)
|
||||
pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec<Backlink> {
|
||||
let record_type = record
|
||||
.get("$type")
|
||||
|
||||
@@ -2,5 +2,5 @@ pub use tranquil_storage::{
|
||||
BlobStorage, FilesystemBlobStorage, StorageError, StreamUploadResult, create_blob_storage,
|
||||
};
|
||||
|
||||
#[cfg(feature = "s3-storage")]
|
||||
#[cfg(feature = "s3")]
|
||||
pub use tranquil_storage::S3BlobStorage;
|
||||
|
||||
@@ -2,6 +2,8 @@ pub use tranquil_types::*;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
pub static PROFILE_COLLECTION: LazyLock<Nsid> =
|
||||
LazyLock::new(|| "app.bsky.actor.profile".parse().unwrap());
|
||||
#[cfg(feature = "bsky")]
|
||||
pub static PROFILE_RKEY: LazyLock<Rkey> = LazyLock::new(|| "self".parse().unwrap());
|
||||
|
||||
@@ -87,6 +87,7 @@ pub const HEADER_ATPROTO_ACCEPT_LABELERS: HeaderName =
|
||||
pub const HEADER_ATPROTO_REPO_REV: HeaderName = HeaderName::from_static("atproto-repo-rev");
|
||||
pub const HEADER_ATPROTO_CONTENT_LABELERS: HeaderName =
|
||||
HeaderName::from_static("atproto-content-labelers");
|
||||
#[cfg(feature = "bsky-support")]
|
||||
pub const HEADER_X_BSKY_TOPICS: HeaderName = HeaderName::from_static("x-bsky-topics");
|
||||
|
||||
pub fn get_header_str(
|
||||
@@ -247,7 +248,13 @@ pub fn build_full_url(path: &str) -> String {
|
||||
let cfg = tranquil_config::get();
|
||||
let normalized_path = if !path.starts_with("/xrpc/")
|
||||
&& (path.starts_with("/com.atproto.")
|
||||
|| path.starts_with("/app.bsky.")
|
||||
// BSKY: Bluesky requires that the PDS implement some app.bsky.* endpoints so we need to deal with those here too.
|
||||
// TODO: surely we can figure out a way to do this more generically?
|
||||
|| (if cfg!(feature = "bsky-support") {
|
||||
path.starts_with("/app.bsky.")
|
||||
} else {
|
||||
true
|
||||
})
|
||||
|| path.starts_with("/_"))
|
||||
{
|
||||
format!("/xrpc{path}")
|
||||
|
||||
@@ -138,20 +138,25 @@ fn check_banned_content(
|
||||
rkey: Option<&Rkey>,
|
||||
) -> Result<(), ValidationError> {
|
||||
match record_type {
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.feed.post" => {
|
||||
check_post_banned_content(obj)?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.actor.profile" => {
|
||||
check_string_field(obj, "displayName")?;
|
||||
check_string_field(obj, "description")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.graph.list" => {
|
||||
check_string_field(obj, "name")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.graph.starterpack" => {
|
||||
check_string_field(obj, "name")?;
|
||||
check_string_field(obj, "description")?;
|
||||
}
|
||||
#[cfg(feature = "bsky")]
|
||||
"app.bsky.feed.generator" => {
|
||||
if let Some(rkey) = rkey
|
||||
&& crate::moderation::has_explicit_slur(rkey)
|
||||
@@ -167,6 +172,7 @@ fn check_banned_content(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bsky")]
|
||||
fn check_post_banned_content(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if let Some(tags) = obj.get("tags").and_then(|v| v.as_array()) {
|
||||
tags.iter().enumerate().try_for_each(|(i, tag)| {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
use aws_config::BehaviorVersion;
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
use aws_sdk_s3::Client as S3Client;
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
use aws_sdk_s3::config::Credentials;
|
||||
use chrono::Utc;
|
||||
use reqwest::{Client, StatusCode, header};
|
||||
@@ -56,9 +56,9 @@ pub struct ServerInstance {
|
||||
pub distributed_rate_limiter: Option<Arc<dyn DistributedRateLimiter>>,
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
use testcontainers::GenericImage;
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
use testcontainers::core::ContainerPort;
|
||||
#[cfg(not(feature = "external-infra"))]
|
||||
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
|
||||
@@ -66,7 +66,7 @@ use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
|
||||
use testcontainers_modules::postgres::Postgres;
|
||||
#[cfg(not(feature = "external-infra"))]
|
||||
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
static S3_CONTAINER: OnceLock<ContainerAsync<GenericImage>> = OnceLock::new();
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -195,7 +195,7 @@ async fn setup_with_external_infra() -> String {
|
||||
spawn_app(database_url).await
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "external-infra"), not(feature = "s3-storage")))]
|
||||
#[cfg(all(not(feature = "external-infra"), not(feature = "s3")))]
|
||||
async fn setup_with_testcontainers() -> String {
|
||||
let temp_dir = std::env::temp_dir().join(format!("tranquil-pds-test-{}", uuid::Uuid::new_v4()));
|
||||
let blob_path = temp_dir.join("blobs");
|
||||
@@ -227,7 +227,7 @@ async fn setup_with_testcontainers() -> String {
|
||||
spawn_app(connection_string).await
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3-storage"))]
|
||||
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
|
||||
async fn setup_with_testcontainers() -> String {
|
||||
let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest")
|
||||
.with_exposed_port(ContainerPort::Tcp(9000))
|
||||
|
||||
@@ -177,7 +177,10 @@ async fn test_external_did_web_no_local_doc() {
|
||||
|
||||
async fn reserve_signing_key(client: &reqwest::Client, base: &str, did: &str) -> String {
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.reserveSigningKey", base))
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.reserveSigningKey",
|
||||
base
|
||||
))
|
||||
.json(&json!({ "did": did }))
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -605,6 +605,7 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
("redirect_uri", "https://client1.example.com/callback"),
|
||||
("code_challenge", &challenge1),
|
||||
("code_challenge_method", "S256"),
|
||||
("scope", "atproto repo:app.bsky.feed.post?action=create"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
@@ -631,7 +632,7 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
let consent_res = http_client
|
||||
.post(format!("{}/oauth/authorize/consent", url))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&json!({"request_uri": request_uri1, "approved_scopes": ["atproto", "transition:generic"], "remember": false}))
|
||||
.json(&json!({"request_uri": request_uri1, "approved_scopes": ["atproto", "repo:app.bsky.feed.post?action=create"], "remember": false}))
|
||||
.send().await.unwrap();
|
||||
let consent_body: Value = consent_res.json().await.unwrap();
|
||||
location1 = consent_body["redirect_uri"].as_str().unwrap().to_string();
|
||||
@@ -666,6 +667,7 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
("redirect_uri", "https://client2.example.com/callback"),
|
||||
("code_challenge", &challenge2),
|
||||
("code_challenge_method", "S256"),
|
||||
("scope", "atproto repo:app.bsky.feed.post?action=create"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
@@ -692,7 +694,7 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
let consent_res = http_client
|
||||
.post(format!("{}/oauth/authorize/consent", url))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&json!({"request_uri": request_uri2, "approved_scopes": ["atproto", "transition:generic"], "remember": false}))
|
||||
.json(&json!({"request_uri": request_uri2, "approved_scopes": ["atproto", "repo:app.bsky.feed.post?action=create"], "remember": false}))
|
||||
.send().await.unwrap();
|
||||
let consent_body: Value = consent_res.json().await.unwrap();
|
||||
location2 = consent_body["redirect_uri"].as_str().unwrap().to_string();
|
||||
@@ -763,6 +765,33 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
StatusCode::OK,
|
||||
"Client 2 token should work"
|
||||
);
|
||||
let ungranted_collection = "app.bsky.graph.follow";
|
||||
let denied_res = http_client
|
||||
.post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
|
||||
.bearer_auth(token1)
|
||||
.json(&json!({
|
||||
"repo": user_did,
|
||||
"collection": ungranted_collection,
|
||||
"record": {
|
||||
"$type": ungranted_collection,
|
||||
"subject": user_did,
|
||||
"createdAt": Utc::now().to_rfc3339()
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
denied_res.status(),
|
||||
StatusCode::FORBIDDEN,
|
||||
"Client 1 token shouldn't write outside its granted collection"
|
||||
);
|
||||
let denied_body: Value = denied_res.json().await.unwrap();
|
||||
assert_eq!(
|
||||
denied_body["error"].as_str(),
|
||||
Some("InsufficientScope"),
|
||||
"Write outside the granted collection should fail on scope"
|
||||
);
|
||||
let list_res = http_client
|
||||
.get(format!("{}/xrpc/com.atproto.repo.listRecords", url))
|
||||
.bearer_auth(token1)
|
||||
@@ -770,6 +799,7 @@ async fn test_oauth_multiple_clients_same_user() {
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_res.status(), StatusCode::OK, "listRecords should work");
|
||||
let list_body: Value = list_res.json().await.unwrap();
|
||||
let records = list_body["records"].as_array().unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -41,8 +41,11 @@ tracing-subscriber = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["frontend", "s3", "valkey"]
|
||||
default = ["bsky", "frontend", "postgres", "s3", "valkey"]
|
||||
bsky = ["bsky-support", "tranquil-pds/bsky", "tranquil-api/bsky"]
|
||||
bsky-support = ["tranquil-pds/bsky-support", "tranquil-api/bsky-support"]
|
||||
frontend = ["tranquil-pds/frontend"]
|
||||
postgres = ["tranquil-pds/postgres"]
|
||||
s3 = ["tranquil-pds/s3"]
|
||||
valkey = ["tranquil-pds/valkey"]
|
||||
native-tls-roots = ["tranquil-pds/native-tls-roots"]
|
||||
|
||||
@@ -17,13 +17,11 @@ tracing = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = "2.5"
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tranquil-signal = { path = ".", features = ["fjall-store"] }
|
||||
rand = "0.9"
|
||||
tempfile = "3"
|
||||
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -59,8 +59,7 @@ tempfile = "3"
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "rt-multi-thread", "macros", "time"] }
|
||||
jacquard-common = { workspace = true }
|
||||
tranquil-repo = { workspace = true }
|
||||
tranquil-db = { workspace = true }
|
||||
tranquil-db = { workspace = true, features = [ "postgres" ] }
|
||||
sqlx = { workspace = true }
|
||||
k256 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -4,10 +4,6 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["sqlx"]
|
||||
sqlx = ["dep:sqlx"]
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -16,6 +12,6 @@ jacquard-common = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true, optional = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -92,9 +92,9 @@ macro_rules! simple_string_newtype {
|
||||
$vis:vis struct $name:ident;
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
#[serde(transparent)]
|
||||
#[sqlx(transparent)]
|
||||
$vis struct $name(String);
|
||||
|
||||
impl $name {
|
||||
@@ -148,9 +148,9 @@ macro_rules! validated_string_newtype {
|
||||
validator = $validator:expr;
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, sqlx::Type)]
|
||||
#[serde(transparent)]
|
||||
#[sqlx(transparent)]
|
||||
$vis struct $name(String);
|
||||
|
||||
impl<'de> Deserialize<'de> for $name {
|
||||
@@ -223,9 +223,9 @@ impl Did {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, sqlx::Type)]
|
||||
#[serde(transparent)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct Handle(String);
|
||||
|
||||
impl<'de> Deserialize<'de> for Handle {
|
||||
@@ -523,9 +523,9 @@ validated_string_newtype! {
|
||||
validator = |s| jacquard_common::types::string::Language::from_str(s).map(|v| v.as_str().to_owned()).map_err(|_| ());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, sqlx::Type)]
|
||||
#[serde(transparent)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct CidLink(String);
|
||||
|
||||
impl<'de> Deserialize<'de> for CidLink {
|
||||
@@ -718,9 +718,9 @@ impl Deref for PlainPassword {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[serde(transparent)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct PasswordHash(String);
|
||||
|
||||
impl PasswordHash {
|
||||
@@ -843,13 +843,9 @@ simple_string_newtype! {
|
||||
pub struct InviteCode;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
feature = "sqlx",
|
||||
derive(sqlx::Type),
|
||||
sqlx(type_name = "comms_channel", rename_all = "snake_case")
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "comms_channel", rename_all = "snake_case")]
|
||||
#[derive(Copy)]
|
||||
pub enum CommsChannel {
|
||||
Email,
|
||||
@@ -885,13 +881,9 @@ impl fmt::Display for CommsChannel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
feature = "sqlx",
|
||||
derive(sqlx::Type),
|
||||
sqlx(type_name = "comms_type", rename_all = "snake_case")
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "comms_type", rename_all = "snake_case")]
|
||||
pub enum CommsType {
|
||||
Verification,
|
||||
PasswordReset,
|
||||
|
||||
@@ -609,7 +609,8 @@
|
||||
"malformed": "This bundle's name is not a valid identifier, so it cannot be granted.",
|
||||
"not_a_permission_set": "This identifier does not refer to a permission bundle, so it cannot be granted.",
|
||||
"malformed_lexicon": "This bundle is published incorrectly and cannot be granted.",
|
||||
"empty_permissions": "This bundle does not currently grant any permissions."
|
||||
"empty_permissions": "This bundle does not currently grant any permissions.",
|
||||
"unknown": "This bundle cannot be granted."
|
||||
},
|
||||
"permTable": {
|
||||
"data": "Data",
|
||||
|
||||
@@ -603,6 +603,29 @@
|
||||
"permissionsRequested": "Pyydetyt oikeudet",
|
||||
"required": "Vaaditaan",
|
||||
"rememberChoiceLabel": "Muista valintani tälle sovellukselle",
|
||||
"permissionSets": "Oikeuspaketit",
|
||||
"showIncludedScopes": "Näytä sisältyvät oikeudet ({count})",
|
||||
"unavailablePermissions": "Ei käytettävissä olevat oikeudet",
|
||||
"unavailableLimited": "Delegoinnin rajoittama",
|
||||
"unavailableFailed": "Lataus epäonnistui",
|
||||
"setPartiallyLimited": "Delegointisi rajoittaa osaa tämän paketin oikeuksista. Kohta \"Ei käytettävissä olevat oikeudet\" kertoo, mitä ei sallittu.",
|
||||
"setFailureReason": {
|
||||
"not_found": "Pakettia ei löytynyt, joten sitä ei voi myöntää.",
|
||||
"unreachable": "Paketin julkaisijaan ei saatu yhteyttä, joten sitä ei voi nyt myöntää.",
|
||||
"malformed": "Paketin nimi ei ole kelvollinen tunniste, joten sitä ei voi myöntää.",
|
||||
"not_a_permission_set": "Tunniste ei viittaa oikeuspakettiin, joten sitä ei voi myöntää.",
|
||||
"malformed_lexicon": "Paketti on julkaistu virheellisesti, joten sitä ei voi myöntää.",
|
||||
"empty_permissions": "Paketti ei myönnä tällä hetkellä yhtään oikeutta.",
|
||||
"unknown": "Pakettia ei voi myöntää."
|
||||
},
|
||||
"permTable": {
|
||||
"data": "Tiedot",
|
||||
"create": "Luonti",
|
||||
"update": "Päivitys",
|
||||
"delete": "Poisto",
|
||||
"allData": "Kaikki kokoelmat",
|
||||
"apiAccess": "API-käyttö"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "AT Protocol -käyttöoikeus",
|
||||
|
||||
@@ -597,6 +597,29 @@
|
||||
"permissionsRequested": "Permissions demandées",
|
||||
"required": "Requis",
|
||||
"rememberChoiceLabel": "Se souvenir de mon choix pour cette application",
|
||||
"permissionSets": "Ensembles de permissions",
|
||||
"showIncludedScopes": "Afficher les permissions incluses ({count})",
|
||||
"unavailablePermissions": "Permissions indisponibles",
|
||||
"unavailableLimited": "Limité par la délégation",
|
||||
"unavailableFailed": "Échec du chargement",
|
||||
"setPartiallyLimited": "Votre délégation limite certaines permissions de cet ensemble. La section « Permissions indisponibles » indique lesquelles ont été refusées.",
|
||||
"setFailureReason": {
|
||||
"not_found": "Cet ensemble est introuvable et ne peut pas être accordé.",
|
||||
"unreachable": "L'éditeur de cet ensemble est injoignable, il ne peut donc pas être accordé pour le moment.",
|
||||
"malformed": "Le nom de cet ensemble n'est pas un identifiant valide, il ne peut donc pas être accordé.",
|
||||
"not_a_permission_set": "Cet identifiant ne désigne pas un ensemble de permissions, il ne peut donc pas être accordé.",
|
||||
"malformed_lexicon": "Cet ensemble est publié de façon incorrecte et ne peut pas être accordé.",
|
||||
"empty_permissions": "Cet ensemble n'accorde actuellement aucune permission.",
|
||||
"unknown": "Cet ensemble ne peut pas être accordé."
|
||||
},
|
||||
"permTable": {
|
||||
"data": "Données",
|
||||
"create": "Créer",
|
||||
"update": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"allData": "Toutes les collections",
|
||||
"apiAccess": "Accès API"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "Accès AT Protocol",
|
||||
|
||||
@@ -603,6 +603,29 @@
|
||||
"permissionsRequested": "リクエストされた権限",
|
||||
"required": "必須",
|
||||
"rememberChoiceLabel": "このアプリに対する選択を記憶する",
|
||||
"permissionSets": "権限バンドル",
|
||||
"showIncludedScopes": "含まれる権限を表示 ({count})",
|
||||
"unavailablePermissions": "利用できない権限",
|
||||
"unavailableLimited": "委任による制限",
|
||||
"unavailableFailed": "読み込み失敗",
|
||||
"setPartiallyLimited": "このバンドルの一部の権限は委任設定により制限されています。許可されなかった権限は「利用できない権限」で確認できます。",
|
||||
"setFailureReason": {
|
||||
"not_found": "このバンドルは見つからないため、付与できません。",
|
||||
"unreachable": "バンドルの公開元に接続できないため、現在は付与できません。",
|
||||
"malformed": "このバンドルの名前が有効な識別子ではないため、付与できません。",
|
||||
"not_a_permission_set": "この識別子は権限バンドルを指していないため、付与できません。",
|
||||
"malformed_lexicon": "このバンドルは正しく公開されていないため、付与できません。",
|
||||
"empty_permissions": "このバンドルは現在、権限を付与しません。",
|
||||
"unknown": "このバンドルは付与できません。"
|
||||
},
|
||||
"permTable": {
|
||||
"data": "データ",
|
||||
"create": "作成",
|
||||
"update": "更新",
|
||||
"delete": "削除",
|
||||
"allData": "すべてのコレクション",
|
||||
"apiAccess": "API アクセス"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "AT Protocol アクセス",
|
||||
|
||||
@@ -603,6 +603,29 @@
|
||||
"permissionsRequested": "요청된 권한",
|
||||
"required": "필수",
|
||||
"rememberChoiceLabel": "이 앱에 대한 선택 기억하기",
|
||||
"permissionSets": "권한 번들",
|
||||
"showIncludedScopes": "포함된 권한 보기 ({count})",
|
||||
"unavailablePermissions": "사용할 수 없는 권한",
|
||||
"unavailableLimited": "위임으로 제한됨",
|
||||
"unavailableFailed": "불러오기 실패",
|
||||
"setPartiallyLimited": "위임 설정이 이 번들의 일부 권한을 제한합니다. 허용되지 않은 권한은 \"사용할 수 없는 권한\" 항목에서 확인하세요.",
|
||||
"setFailureReason": {
|
||||
"not_found": "이 번들을 찾을 수 없어 부여할 수 없습니다.",
|
||||
"unreachable": "번들 게시자에 연결할 수 없어 지금은 부여할 수 없습니다.",
|
||||
"malformed": "이 번들의 이름이 유효한 식별자가 아니어서 부여할 수 없습니다.",
|
||||
"not_a_permission_set": "이 식별자는 권한 번들을 가리키지 않아 부여할 수 없습니다.",
|
||||
"malformed_lexicon": "이 번들이 잘못 게시되어 부여할 수 없습니다.",
|
||||
"empty_permissions": "이 번들은 현재 어떤 권한도 부여하지 않습니다.",
|
||||
"unknown": "이 번들은 부여할 수 없습니다."
|
||||
},
|
||||
"permTable": {
|
||||
"data": "데이터",
|
||||
"create": "생성",
|
||||
"update": "수정",
|
||||
"delete": "삭제",
|
||||
"allData": "모든 컬렉션",
|
||||
"apiAccess": "API 접근"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "AT Protocol 액세스",
|
||||
|
||||
@@ -603,6 +603,29 @@
|
||||
"permissionsRequested": "Begärda behörigheter",
|
||||
"required": "Krävs",
|
||||
"rememberChoiceLabel": "Kom ihåg mitt val för denna applikation",
|
||||
"permissionSets": "Behörighetspaket",
|
||||
"showIncludedScopes": "Visa inkluderade behörigheter ({count})",
|
||||
"unavailablePermissions": "Otillgängliga behörigheter",
|
||||
"unavailableLimited": "Begränsad av delegering",
|
||||
"unavailableFailed": "Kunde inte laddas",
|
||||
"setPartiallyLimited": "Din delegering begränsar vissa behörigheter i paketet. Avsnittet \"Otillgängliga behörigheter\" visar vilka som nekades.",
|
||||
"setFailureReason": {
|
||||
"not_found": "Paketet hittades inte och kan inte beviljas.",
|
||||
"unreachable": "Paketets utgivare kunde inte nås, så paketet kan inte beviljas just nu.",
|
||||
"malformed": "Paketets namn är inte en giltig identifierare, så det kan inte beviljas.",
|
||||
"not_a_permission_set": "Identifieraren avser inte ett behörighetspaket, så det kan inte beviljas.",
|
||||
"malformed_lexicon": "Paketet är felaktigt publicerat och kan inte beviljas.",
|
||||
"empty_permissions": "Paketet beviljar för närvarande inga behörigheter.",
|
||||
"unknown": "Paketet kan inte beviljas."
|
||||
},
|
||||
"permTable": {
|
||||
"data": "Data",
|
||||
"create": "Skapa",
|
||||
"update": "Uppdatera",
|
||||
"delete": "Radera",
|
||||
"allData": "Alla samlingar",
|
||||
"apiAccess": "API-åtkomst"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "AT Protocol-åtkomst",
|
||||
|
||||
@@ -603,6 +603,29 @@
|
||||
"permissionsRequested": "请求的权限",
|
||||
"required": "必需",
|
||||
"rememberChoiceLabel": "记住对此应用的授权选择",
|
||||
"permissionSets": "权限包",
|
||||
"showIncludedScopes": "显示包含的权限 ({count})",
|
||||
"unavailablePermissions": "不可用权限",
|
||||
"unavailableLimited": "受委托限制",
|
||||
"unavailableFailed": "加载失败",
|
||||
"setPartiallyLimited": "您的委托设置限制了此权限包中的部分权限。“不可用权限”一节列出了未被允许的权限。",
|
||||
"setFailureReason": {
|
||||
"not_found": "找不到此权限包,无法授予。",
|
||||
"unreachable": "无法连接此权限包的发布方,目前无法授予。",
|
||||
"malformed": "此权限包的名称不是有效标识符,无法授予。",
|
||||
"not_a_permission_set": "此标识符并非指向权限包,无法授予。",
|
||||
"malformed_lexicon": "此权限包发布有误,无法授予。",
|
||||
"empty_permissions": "此权限包目前不授予任何权限。",
|
||||
"unknown": "此权限包无法授予。"
|
||||
},
|
||||
"permTable": {
|
||||
"data": "数据",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"delete": "删除",
|
||||
"allData": "所有集合",
|
||||
"apiAccess": "API 访问"
|
||||
},
|
||||
"scopes": {
|
||||
"atproto": {
|
||||
"name": "AT Protocol 访问",
|
||||
|
||||
@@ -44,6 +44,28 @@
|
||||
restricted?: boolean
|
||||
}
|
||||
|
||||
type SetFailureReason =
|
||||
| 'unreachable'
|
||||
| 'not_found'
|
||||
| 'malformed'
|
||||
| 'not_a_permission_set'
|
||||
| 'malformed_lexicon'
|
||||
| 'empty_permissions'
|
||||
|
||||
const SET_FAILURE_LOCALE_KEYS: Record<SetFailureReason, string> = {
|
||||
unreachable: 'oauth.consent.setFailureReason.unreachable',
|
||||
not_found: 'oauth.consent.setFailureReason.not_found',
|
||||
malformed: 'oauth.consent.setFailureReason.malformed',
|
||||
not_a_permission_set: 'oauth.consent.setFailureReason.not_a_permission_set',
|
||||
malformed_lexicon: 'oauth.consent.setFailureReason.malformed_lexicon',
|
||||
empty_permissions: 'oauth.consent.setFailureReason.empty_permissions',
|
||||
}
|
||||
|
||||
function setFailureLocaleKey(reason: string): string {
|
||||
const known: Partial<Record<string, string>> = SET_FAILURE_LOCALE_KEYS
|
||||
return known[reason] ?? 'oauth.consent.setFailureReason.unknown'
|
||||
}
|
||||
|
||||
interface FailedSetInfo {
|
||||
nsid: string
|
||||
aud?: string
|
||||
@@ -572,7 +594,7 @@
|
||||
<div class="scope-item failed">
|
||||
<div class="scope-info">
|
||||
<span class="scope-name">{f.nsid}{#if f.aud} ({f.aud}){/if}</span>
|
||||
<span class="scope-description">{$_(`oauth.consent.setFailureReason.${f.reason}`)}</span>
|
||||
<span class="scope-description">{$_(setFailureLocaleKey(f.reason))}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user