Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

Merge branch 'pocket'

lakdsjflkj

+1 -1
.github/workflows/checks.yml
···
- name: get nightly toolchain for jetstream fmt
run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- name: fmt
-
run: cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot -- --check
+
run: cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot --package pocket -- --check
- name: fmt jetstream (nightly)
run: cargo +nightly fmt --package jetstream -- --check
- name: clippy
+277 -77
Cargo.lock
···
"nom",
"num-traits",
"rusticata-macros",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"time",
]
···
]
[[package]]
+
name = "atrium-crypto"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "73a3da430c71dd9006d61072c20771f264e5c498420a49c32305ceab8bd71955"
+
dependencies = [
+
"ecdsa",
+
"k256",
+
"multibase",
+
"p256",
+
"thiserror 1.0.69",
+
]
+
+
[[package]]
name = "atrium-identity"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"axum",
"handlebars",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
]
[[package]]
···
]
[[package]]
+
name = "bitcoin-io"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
+
+
[[package]]
+
name = "bitcoin_hashes"
+
version = "0.14.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
+
dependencies = [
+
"bitcoin-io",
+
"hex-conservative",
+
]
+
+
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
]
[[package]]
+
name = "ciborium"
+
version = "0.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+
dependencies = [
+
"ciborium-io",
+
"ciborium-ll",
+
"serde",
+
]
+
+
[[package]]
+
name = "ciborium-io"
+
version = "0.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+
[[package]]
+
name = "ciborium-ll"
+
version = "0.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+
dependencies = [
+
"ciborium-io",
+
"half",
+
]
+
+
[[package]]
name = "cid"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
name = "clap"
-
version = "4.5.46"
+
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
+
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
dependencies = [
"clap_builder",
"clap_derive",
···
[[package]]
name = "clap_builder"
-
version = "4.5.46"
+
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
+
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstyle",
···
[[package]]
name = "clap_derive"
-
version = "4.5.45"
+
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
+
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
···
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+
[[package]]
+
name = "crunchy"
+
version = "0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
···
"slog-bunyan",
"slog-json",
"slog-term",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tokio-rustls 0.25.0",
"toml",
···
[[package]]
+
name = "fallible-iterator"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+
[[package]]
+
name = "fallible-streaming-iterator"
+
version = "0.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"mixtrics",
"pin-project",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
···
"parking_lot",
"pin-project",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"twox-hash",
···
"parking_lot",
"pin-project",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
···
"pin-project",
"rand 0.9.1",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
"twox-hash",
···
[[package]]
+
name = "half"
+
version = "2.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+
dependencies = [
+
"cfg-if",
+
"crunchy",
+
]
+
+
[[package]]
name = "handlebars"
version = "6.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"pest_derive",
"serde",
"serde_json",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"walkdir",
···
"allocator-api2",
"equivalent",
"foldhash",
+
]
+
+
[[package]]
+
name = "hashlink"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+
dependencies = [
+
"hashbrown 0.15.2",
[[package]]
···
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
+
name = "hex-conservative"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd"
+
dependencies = [
+
"arrayvec",
+
]
+
+
[[package]]
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"once_cell",
"rand 0.9.1",
"ring",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tinyvec",
"tokio",
"tracing",
···
"rand 0.9.1",
"resolv-conf",
"smallvec",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
···
"metrics",
"serde",
"serde_json",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tokio-tungstenite 0.26.2",
"url",
···
[[package]]
+
name = "jwt-compact"
+
version = "0.9.0-beta.1"
+
source = "git+https://github.com/fatfingers23/jwt-compact.git#aed088b8ff5ad44ef2785c453f6a4b7916728b1c"
+
dependencies = [
+
"anyhow",
+
"base64ct",
+
"chrono",
+
"ciborium",
+
"hmac",
+
"lazy_static",
+
"rand_core 0.6.4",
+
"secp256k1",
+
"serde",
+
"serde_json",
+
"sha2",
+
"smallvec",
+
"subtle",
+
"zeroize",
+
]
+
+
[[package]]
+
name = "k256"
+
version = "0.13.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+
dependencies = [
+
"cfg-if",
+
"ecdsa",
+
"elliptic-curve",
+
"sha2",
+
]
+
+
[[package]]
name = "langtag"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "libsqlite3-sys"
+
version = "0.35.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
+
dependencies = [
+
"pkg-config",
+
"vcpkg",
+
]
+
+
[[package]]
name = "libz-sys"
version = "1.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"anyhow",
"fluent-uri",
"nom",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tinyjson",
···
[[package]]
name = "log"
-
version = "0.4.27"
+
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "loom"
···
[[package]]
name = "matchers"
-
version = "0.1.0"
+
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
-
"regex-automata 0.1.10",
+
"regex-automata",
[[package]]
···
"metrics",
"metrics-util 0.20.0",
"quanta",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
···
[[package]]
name = "nu-ansi-term"
-
version = "0.46.0"
+
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
-
"overload",
-
"winapi",
+
"windows-sys 0.52.0",
[[package]]
···
[[package]]
-
name = "overload"
-
version = "0.1.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
-
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"ucd-trie",
···
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
+
name = "pocket"
+
version = "0.1.0"
+
dependencies = [
+
"atrium-crypto",
+
"clap",
+
"jwt-compact",
+
"log",
+
"poem",
+
"poem-openapi",
+
"reqwest",
+
"rusqlite",
+
"serde",
+
"serde_json",
+
"thiserror 2.0.16",
+
"tokio",
+
"tracing-subscriber",
+
]
+
+
[[package]]
name = "poem"
version = "3.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"smallvec",
"sync_wrapper",
"tempfile",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tokio-rustls 0.26.2",
"tokio-stream",
···
"serde_json",
"serde_urlencoded",
"serde_yaml",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
···
"quote",
"regex",
"syn 2.0.103",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
[[package]]
···
"rustc-hash 2.1.1",
"rustls 0.23.31",
"socket2 0.5.9",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tracing",
"web-time",
···
"rustls 0.23.31",
"rustls-pki-types",
"slab",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tinyvec",
"tracing",
"web-time",
···
[[package]]
+
name = "reflector"
+
version = "0.1.0"
+
dependencies = [
+
"clap",
+
"log",
+
"poem",
+
"serde",
+
"tokio",
+
"tracing-subscriber",
+
]
+
+
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
dependencies = [
"aho-corasick",
"memchr",
-
"regex-automata 0.4.9",
-
"regex-syntax 0.8.5",
-
]
-
-
[[package]]
-
name = "regex-automata"
-
version = "0.1.10"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-
dependencies = [
-
"regex-syntax 0.6.29",
+
"regex-automata",
+
"regex-syntax",
[[package]]
···
dependencies = [
"aho-corasick",
"memchr",
-
"regex-syntax 0.8.5",
+
"regex-syntax",
[[package]]
name = "regex-syntax"
-
version = "0.6.29"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-
[[package]]
-
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
···
"spki",
"subtle",
"zeroize",
+
]
+
+
[[package]]
+
name = "rusqlite"
+
version = "0.37.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
+
dependencies = [
+
"bitflags",
+
"fallible-iterator",
+
"fallible-streaming-iterator",
+
"hashlink",
+
"libsqlite3-sys",
+
"smallvec",
[[package]]
···
[[package]]
+
name = "secp256k1"
+
version = "0.30.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
+
dependencies = [
+
"bitcoin_hashes",
+
"rand 0.8.5",
+
"secp256k1-sys",
+
]
+
+
[[package]]
+
name = "secp256k1-sys"
+
version = "0.10.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+
dependencies = [
+
"cc",
+
]
+
+
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"percent-encoding",
"ryu",
"serde",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
[[package]]
···
dependencies = [
"num-bigint",
"num-traits",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"time",
···
"rustls 0.23.31",
"serde",
"serde_json",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"time",
"tokio",
"tokio-util",
···
"serde",
"serde_json",
"serde_qs",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tinyjson",
"tokio",
"tokio-tungstenite 0.27.0",
···
[[package]]
name = "thiserror"
-
version = "2.0.12"
+
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
-
"thiserror-impl 2.0.12",
+
"thiserror-impl 2.0.16",
[[package]]
···
[[package]]
name = "thiserror-impl"
-
version = "2.0.12"
+
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
···
[[package]]
name = "tokio"
-
version = "1.47.0"
+
version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
+
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
"bytes",
···
[[package]]
name = "tracing-subscriber"
-
version = "0.3.19"
+
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
-
"regex",
+
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
···
"native-tls",
"rand 0.9.1",
"sha1",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"url",
"utf-8",
···
"log",
"rand 0.9.1",
"sha1",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"utf-8",
···
"serde_qs",
"sha2",
"tempfile",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tikv-jemallocator",
"tokio",
"tokio-util",
···
"reqwest",
"serde",
"serde_json",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"tokio",
"tokio-util",
"url",
···
"nom",
"oid-registry",
"rusticata-macros",
-
"thiserror 2.0.12",
+
"thiserror 2.0.16",
"time",
···
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"serde",
+
"zeroize_derive",
+
]
+
+
[[package]]
+
name = "zeroize_derive"
+
version = "1.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn 2.0.103",
[[package]]
+2
Cargo.toml
···
"who-am-i",
"slingshot",
"quasar",
+
"pocket",
+
"reflector",
]
+8 -1
Makefile
···
cargo test --all-features
fmt:
-
cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot
+
cargo fmt --package links \
+
--package constellation \
+
--package ufos \
+
--package spacedust \
+
--package who-am-i \
+
--package slingshot \
+
--package pocket \
+
--package reflector
cargo +nightly fmt --package jetstream
clippy:
+19
pocket/Cargo.toml
···
+
[package]
+
name = "pocket"
+
version = "0.1.0"
+
edition = "2024"
+
+
[dependencies]
+
atrium-crypto = "0.1.2"
+
clap = { version = "4.5.41", features = ["derive"] }
+
jwt-compact = { git = "https://github.com/fatfingers23/jwt-compact.git", features = ["es256k"] }
+
log = "0.4.27"
+
poem = { version = "3.1.12", features = ["acme", "static-files"] }
+
poem-openapi = { version = "5.1.16", features = ["scalar"] }
+
reqwest = { version = "0.12.22", features = ["json"] }
+
rusqlite = "0.37.0"
+
serde = { version = "1.0.219", features = ["derive"] }
+
serde_json = { version = "1.0.141" }
+
thiserror = "2.0.16"
+
tokio = { version = "1.47.0", features = ["full"] }
+
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
+17
pocket/api-description.md
···
+
_A pocket dimension to stash a bit of non-public user data._
+
+
+
# Pocket: user preference storage
+
+
This API leverages atproto service proxying to offer a bit of per-user per-app non-public data storage.
+
Perfect for things like application preferences that might be better left out of the public PDS data.
+
+
The intent is to use oauth scopes to isolate storage on a per-application basis, and to allow easy data migration from a community hosted instance to your own if you end up needing that.
+
+
+
### Current status
+
+
> [!important]
+
> Pocket is currently in a **v0, pre-release state**. There is one production instance and you can use it! Expect short downtimes for restarts as development progresses and occaisional data loss until it's stable.
+
+
ATProto might end up adding a similar feature to [PDSs](https://atproto.com/guides/glossary#pds-personal-data-server). If/when that happens, you should use it instead of this!
+5
pocket/src/lib.rs
···
+
mod server;
+
mod token;
+
+
pub use server::serve;
+
pub use token::TokenVerifier;
+8
pocket/src/main.rs
···
+
use pocket::serve;
+
+
#[tokio::main]
+
async fn main() {
+
tracing_subscriber::fmt::init();
+
println!("Hello, world!");
+
serve("mac.cinnebar-tet.ts.net").await
+
}
+202
pocket/src/server.rs
···
+
use crate::TokenVerifier;
+
use poem::{
+
Endpoint, EndpointExt, Route, Server,
+
endpoint::{StaticFileEndpoint, make_sync},
+
http::Method,
+
listener::TcpListener,
+
middleware::{CatchPanic, Cors, Tracing},
+
};
+
use poem_openapi::{
+
ApiResponse, ContactObject, ExternalDocumentObject, Object, OpenApi, OpenApiService,
+
SecurityScheme, Tags,
+
auth::Bearer,
+
payload::{Json, PlainText},
+
types::Example,
+
};
+
use serde::Serialize;
+
use serde_json::{Value, json};
+
+
#[derive(Debug, SecurityScheme)]
+
#[oai(ty = "bearer")]
+
struct XrpcAuth(Bearer);
+
+
#[derive(Tags)]
+
enum ApiTags {
+
/// Custom pocket APIs
+
#[oai(rename = "Pocket APIs")]
+
Pocket,
+
}
+
+
#[derive(Object)]
+
#[oai(example = true)]
+
struct XrpcErrorResponseObject {
+
/// Should correspond an error `name` in the lexicon errors array
+
error: String,
+
/// Human-readable description and possibly additonal context
+
message: String,
+
}
+
impl Example for XrpcErrorResponseObject {
+
fn example() -> Self {
+
Self {
+
error: "PreferencesNotFound".to_string(),
+
message: "No preferences were found for this user".to_string(),
+
}
+
}
+
}
+
type XrpcError = Json<XrpcErrorResponseObject>;
+
fn xrpc_error(error: impl AsRef<str>, message: impl AsRef<str>) -> XrpcError {
+
Json(XrpcErrorResponseObject {
+
error: error.as_ref().to_string(),
+
message: message.as_ref().to_string(),
+
})
+
}
+
+
#[derive(Object)]
+
#[oai(example = true)]
+
struct GetBskyPrefsResponseObject {
+
/// at-uri for this record
+
preferences: Value,
+
}
+
impl Example for GetBskyPrefsResponseObject {
+
fn example() -> Self {
+
Self {
+
preferences: json!({
+
"hello": "world",
+
}),
+
}
+
}
+
}
+
+
#[derive(ApiResponse)]
+
enum GetBskyPrefsResponse {
+
/// Record found
+
#[oai(status = 200)]
+
Ok(Json<GetBskyPrefsResponseObject>),
+
/// Bad request or no preferences to return
+
#[oai(status = 400)]
+
BadRequest(XrpcError),
+
}
+
+
#[derive(ApiResponse)]
+
enum PutBskyPrefsResponse {
+
/// Record found
+
#[oai(status = 200)]
+
Ok(PlainText<String>),
+
/// Bad request or no preferences to return
+
#[oai(status = 400)]
+
BadRequest(XrpcError),
+
// /// Server errors
+
// #[oai(status = 500)]
+
// ServerError(XrpcError),
+
}
+
+
struct Xrpc {
+
verifier: TokenVerifier,
+
}
+
+
#[OpenApi]
+
impl Xrpc {
+
/// com.bad-example.pocket.getPreferences
+
///
+
/// get stored bluesky prefs
+
#[oai(
+
path = "/com.bad-example.pocket.getPreferences",
+
method = "get",
+
tag = "ApiTags::Pocket"
+
)]
+
async fn app_bsky_get_prefs(&self, XrpcAuth(auth): XrpcAuth) -> GetBskyPrefsResponse {
+
let did = match self
+
.verifier
+
.verify("app.bsky.actor.getPreferences", &auth.token)
+
.await
+
{
+
Ok(d) => d,
+
Err(e) => return GetBskyPrefsResponse::BadRequest(xrpc_error("boooo", e.to_string())),
+
};
+
log::info!("verified did: {did}");
+
// TODO: fetch from storage
+
GetBskyPrefsResponse::Ok(Json(GetBskyPrefsResponseObject::example()))
+
}
+
+
/// com.bad-example.pocket.putPreferences
+
///
+
/// store bluesky prefs
+
#[oai(
+
path = "/com.bad-example.pocket.putPreferences",
+
method = "post",
+
tag = "ApiTags::Pocket"
+
)]
+
async fn app_bsky_put_prefs(
+
&self,
+
XrpcAuth(auth): XrpcAuth,
+
Json(prefs): Json<Value>,
+
) -> PutBskyPrefsResponse {
+
let did = match self
+
.verifier
+
.verify("app.bsky.actor.getPreferences", &auth.token)
+
.await
+
{
+
Ok(d) => d,
+
Err(e) => return PutBskyPrefsResponse::BadRequest(xrpc_error("boooo", e.to_string())),
+
};
+
log::info!("verified did: {did}");
+
log::warn!("received prefs: {prefs:?}");
+
// TODO: put prefs into storage
+
PutBskyPrefsResponse::Ok(PlainText("hiiiiii".to_string()))
+
}
+
}
+
+
#[derive(Debug, Clone, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
struct AppViewService {
+
id: String,
+
r#type: String,
+
service_endpoint: String,
+
}
+
#[derive(Debug, Clone, Serialize)]
+
struct AppViewDoc {
+
id: String,
+
service: [AppViewService; 1],
+
}
+
/// Serve a did document for did:web for this to be an xrpc appview
+
fn get_did_doc(domain: &str) -> impl Endpoint + use<> {
+
let doc = poem::web::Json(AppViewDoc {
+
id: format!("did:web:{domain}"),
+
service: [AppViewService {
+
id: "#pocket_prefs".to_string(),
+
r#type: "PocketPreferences".to_string(),
+
service_endpoint: format!("https://{domain}"),
+
}],
+
});
+
make_sync(move |_| doc.clone())
+
}
+
+
pub async fn serve(domain: &str) -> () {
+
let verifier = TokenVerifier::new(domain);
+
let api_service = OpenApiService::new(Xrpc { verifier }, "Pocket", env!("CARGO_PKG_VERSION"))
+
.server(domain)
+
.url_prefix("/xrpc")
+
.contact(
+
ContactObject::new()
+
.name("@microcosm.blue")
+
.url("https://bsky.app/profile/microcosm.blue"),
+
)
+
.description(include_str!("../api-description.md"))
+
.external_document(ExternalDocumentObject::new("https://microcosm.blue/pocket"));
+
+
let app = Route::new()
+
.nest("/openapi", api_service.spec_endpoint())
+
.nest("/xrpc/", api_service)
+
.at("/.well-known/did.json", get_did_doc(domain))
+
.at("/", StaticFileEndpoint::new("./static/index.html"))
+
.with(
+
Cors::new()
+
.allow_method(Method::GET)
+
.allow_method(Method::POST),
+
)
+
.with(CatchPanic::new())
+
.with(Tracing);
+
+
let listener = TcpListener::bind("127.0.0.1:3000");
+
Server::new(listener).name("pocket").run(app).await.unwrap();
+
}
+133
pocket/src/token.rs
···
+
use atrium_crypto::did::parse_multikey;
+
use atrium_crypto::verify::Verifier;
+
use jwt_compact::UntrustedToken;
+
use serde::Deserialize;
+
use std::collections::HashMap;
+
use std::time::Duration;
+
use thiserror::Error;
+
+
#[derive(Debug, Deserialize)]
+
struct MiniDoc {
+
signing_key: String,
+
did: String,
+
}
+
+
#[derive(Error, Debug)]
+
pub enum VerifyError {
+
#[error("The cross-service authorization token failed verification: {0}")]
+
VerificationFailed(&'static str),
+
#[error("Error trying to resolve the DID to a signing key, retry in a moment: {0}")]
+
ResolutionFailed(&'static str),
+
}
+
+
pub struct TokenVerifier {
+
domain: String,
+
client: reqwest::Client,
+
}
+
+
impl TokenVerifier {
+
pub fn new(domain: &str) -> Self {
+
let client = reqwest::Client::builder()
+
.user_agent(format!(
+
"microcosm pocket v{} (dev: @bad-example.com)",
+
env!("CARGO_PKG_VERSION")
+
))
+
.no_proxy()
+
.timeout(Duration::from_secs(12)) // slingshot timeout is 10s
+
.build()
+
.unwrap();
+
Self {
+
client,
+
domain: domain.to_string(),
+
}
+
}
+
+
pub async fn verify(&self, expected_lxm: &str, token: &str) -> Result<String, VerifyError> {
+
let untrusted = UntrustedToken::new(token).unwrap();
+
+
// danger! unfortunately we need to decode the DID from the jwt body before we have a public key to verify the jwt with
+
let Ok(untrusted_claims) =
+
untrusted.deserialize_claims_unchecked::<HashMap<String, String>>()
+
else {
+
return Err(VerifyError::VerificationFailed(
+
"could not deserialize jtw claims",
+
));
+
};
+
+
// get the (untrusted!) claimed DID
+
let Some(untrusted_did) = untrusted_claims.custom.get("iss") else {
+
return Err(VerifyError::VerificationFailed(
+
"jwt must include the user's did in `iss`",
+
));
+
};
+
+
// bail if it's not even a user-ish did
+
if !untrusted_did.starts_with("did:") {
+
return Err(VerifyError::VerificationFailed("iss should be a did"));
+
}
+
if untrusted_did.contains("#") {
+
return Err(VerifyError::VerificationFailed(
+
"iss should be a user did without a service identifier",
+
));
+
}
+
+
let endpoint =
+
"https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc";
+
let doc: MiniDoc = self
+
.client
+
.get(format!("{endpoint}?identifier={untrusted_did}"))
+
.send()
+
.await
+
.map_err(|_| VerifyError::ResolutionFailed("failed to fetch minidoc"))?
+
.error_for_status()
+
.map_err(|_| VerifyError::ResolutionFailed("non-ok response for minidoc"))?
+
.json()
+
.await
+
.map_err(|_| VerifyError::ResolutionFailed("failed to parse json to minidoc"))?;
+
+
// sanity check before we go ahead with this signing key
+
if doc.did != *untrusted_did {
+
return Err(VerifyError::VerificationFailed(
+
"wtf, resolveMiniDoc returned a doc for a different DID, slingshot bug",
+
));
+
}
+
+
let Ok((alg, public_key)) = parse_multikey(&doc.signing_key) else {
+
return Err(VerifyError::VerificationFailed(
+
"could not parse signing key form minidoc",
+
));
+
};
+
+
// i _guess_ we've successfully bootstrapped the verification of the jwt unless this fails
+
if let Err(e) = Verifier::default().verify(
+
alg,
+
&public_key,
+
&untrusted.signed_data,
+
untrusted.signature_bytes(),
+
) {
+
log::warn!("jwt verification failed: {e}");
+
return Err(VerifyError::VerificationFailed(
+
"jwt signature verification failed",
+
));
+
}
+
+
// past this point we're should have established trust. crossing ts and dotting is.
+
let did = &untrusted_did;
+
let claims = &untrusted_claims;
+
+
let Some(aud) = claims.custom.get("aud") else {
+
return Err(VerifyError::VerificationFailed("missing aud"));
+
};
+
if *aud != format!("did:web:{}#bsky_appview", self.domain) {
+
return Err(VerifyError::VerificationFailed("wrong aud"));
+
}
+
let Some(lxm) = claims.custom.get("lxm") else {
+
return Err(VerifyError::VerificationFailed("missing lxm"));
+
};
+
if lxm != expected_lxm {
+
return Err(VerifyError::VerificationFailed("wrong lxm"));
+
}
+
+
Ok(did.to_string())
+
}
+
}
+67
pocket/static/index.html
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="utf-8" />
+
<title>Pocket: atproto user preference storage</title>
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
<meta name="description" content="API Documentation for Pocket, a simple user-preference storage system for atproto" />
+
<style>
+
:root {
+
--scalar-small: 13px;
+
}
+
.scalar-app .markdown .markdown-alert {
+
font-size: var(--scalar-small);
+
}
+
.sidebar-heading-link-title {
+
line-height: 1.2;
+
}
+
.custom-header {
+
height: 42px;
+
background-color: #221828;
+
box-shadow: inset 0 -1px 0 var(--scalar-border-color);
+
color: var(--scalar-color-1);
+
font-size: var(--scalar-font-size-3);
+
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
+
padding: 0 18px;
+
justify-content: space-between;
+
}
+
.custom-header,
+
.custom-header nav {
+
display: flex;
+
align-items: center;
+
gap: 18px;
+
}
+
.custom-header a:hover {
+
color: var(--scalar-color-2);
+
}
+
+
.light-mode .custom-header {
+
background-color: thistle;
+
}
+
</style>
+
</head>
+
<body>
+
<header class="custom-header scalar-app">
+
<p>
+
TODO: thing
+
</p>
+
<nav>
+
<b>a <a href="https://microcosm.blue">microcosm</a> project</b>
+
<a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a>
+
<a href="https://github.com/at-microcosm">github</a>
+
</nav>
+
</header>
+
+
<script id="api-reference" type="application/json" data-url="/openapi"></script>
+
+
<script>
+
var configuration = {
+
theme: 'purple',
+
hideModels: true,
+
}
+
document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration)
+
</script>
+
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
+
</body>
+
</html>
+2
quasar/src/lib.rs
···
mod storage;
+
+
pub use storage::Storage;
+4
quasar/src/storage.rs
···
+
+
pub trait Storage {
+
+
}
+12
reflector/Cargo.toml
···
+
[package]
+
name = "reflector"
+
version = "0.1.0"
+
edition = "2024"
+
+
[dependencies]
+
clap = { version = "4.5.47", features = ["derive"] }
+
log = "0.4.28"
+
poem = "3.1.12"
+
serde = { version = "1.0.219", features = ["derive"] }
+
tokio = "1.47.1"
+
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
+9
reflector/readme.md
···
+
# reflector
+
+
a tiny did:web service server that maps subdomains to a single service endpoint
+
+
receiving requests from multiple subdomains is left as a problem for the reverse proxy to solve, since acme wildcard certificates (ie. letsencrypt) require the most complicated and involved challenge type (DNS).
+
+
caddy [has good support for](https://caddyserver.com/docs/caddyfile/patterns#wildcard-certificates) configuring the wildcard DNS challenge with various DNS providers, and also supports [on-demand](https://caddyserver.com/docs/automatic-https#using-on-demand-tls) provisioning via the simpler methods.
+
+
if you only need a small fixed number of subdomains, you can also use certbot or otherwise individually configure them in your reverse proxy.
+83
reflector/src/main.rs
···
+
use clap::Parser;
+
use poem::{
+
EndpointExt, Route, Server, get, handler,
+
listener::TcpListener,
+
middleware::{AddData, Tracing},
+
web::{Data, Json, TypedHeader, headers::Host},
+
};
+
use serde::Serialize;
+
+
#[handler]
+
fn hello() -> String {
+
"ɹoʇɔǝʅⅎǝɹ".to_string()
+
}
+
+
#[derive(Debug, Serialize)]
+
struct DidDoc {
+
id: String,
+
service: [DidService; 1],
+
}
+
+
#[derive(Debug, Clone, Serialize)]
+
struct DidService {
+
id: String,
+
r#type: String,
+
service_endpoint: String,
+
}
+
+
#[handler]
+
fn did_doc(TypedHeader(host): TypedHeader<Host>, service: Data<&DidService>) -> Json<DidDoc> {
+
Json(DidDoc {
+
id: format!("did:web:{}", host.hostname()),
+
service: [service.clone()],
+
})
+
}
+
+
/// Slingshot record edge cache
+
#[derive(Parser, Debug, Clone)]
+
#[command(version, about, long_about = None)]
+
struct Args {
+
/// The DID document service ID to serve
+
///
+
/// must start with a '#', like `#bsky_appview'
+
#[arg(long)]
+
id: String,
+
/// Service type
+
///
+
/// Not sure exactly what its requirements are. 'BlueskyAppview' for example
+
#[arg(long)]
+
r#type: String,
+
/// The HTTPS endpoint for the service
+
#[arg(long)]
+
service_endpoint: String,
+
}
+
+
impl From<Args> for DidService {
+
fn from(a: Args) -> Self {
+
Self {
+
id: a.id,
+
r#type: a.r#type,
+
service_endpoint: a.service_endpoint,
+
}
+
}
+
}
+
+
#[tokio::main(flavor = "current_thread")]
+
async fn main() {
+
tracing_subscriber::fmt::init();
+
log::info!("ɹoʇɔǝʅⅎǝɹ");
+
+
let args = Args::parse();
+
let service: DidService = args.into();
+
+
Server::new(TcpListener::bind("0.0.0.0:3001"))
+
.run(
+
Route::new()
+
.at("/", get(hello))
+
.at("/.well-known/did.json", get(did_doc))
+
.with(AddData::new(service))
+
.with(Tracing),
+
)
+
.await
+
.unwrap()
+
}