Platform-neutral successor to Discord Authenticator, forked from its v0.1.2
state with fresh history. Same vault, same UI, same live-message mechanism —
now behind one `Sink` that speaks three platforms.
Why each platform needs what it needs (this drove the whole settings model):
- Discord: an incoming-webhook URL. Webhooks can edit their own messages.
- Slack: a bot token (xoxb-, scope chat:write) + channel ID. Slack incoming
webhooks can post but never edit, and editing IS the live mechanism, so
chat.postMessage / chat.update via a bot is the only path.
- Telegram: a bot token + chat id. No webhook concept exists. The app detects
the chat id itself from the bot's pending updates ("send /start, then
Detect"), since users rarely know it.
- core: Provider enum; settings hold all three configs so switching back and
forth never re-asks for tokens; per-platform shape validation that names the
common mistakes (xoxp- user token, #name instead of channel ID); a
destination key that clears message ids on platform/channel change but not
on token rotation
- delivery: shared DeliveryError + pacing Governor; Discord client refactored
onto them; new Slack client (classifies Slack's ok:false-on-HTTP-200
errors) and Telegram client ("message is not modified" is success; chat
detection via getUpdates); provider-agnostic message builders — code in a
copyable span everywhere, rollover time as live as each platform allows
- http-api: setup accepts an optional provider; settings PUT/GET carry the
full model; /api/slack/whoami, /api/telegram/whoami, /api/telegram/chats
- webui: three-way optional picker on setup and in Settings → Delivery with
guided per-platform fields; "Check token" (Slack) and "Detect chat"
(Telegram); inline white mark via currentColor
- brand: white mark on the dark plate; blue stays with the Discord sibling
- docs/TEAMS.md: exploration for v0.2 — Teams connectors were disabled May
2026 and Workflows webhooks can't edit, so live tiles need a Bot Framework
bot; options for the inbound endpoint on a homelab and a recommended scope
Verified against mock Discord, Slack and Telegram servers speaking each
platform's real protocol shape: edits target the same message rather than
re-posting, deleted tiles are recovered, dead credentials are surfaced with a
next step, renames refresh without a rollover, and switching platform reposts
on the new one and leaves the old one alone. 138 tests.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
116 lines
4.0 KiB
TOML
116 lines
4.0 KiB
TOML
[workspace]
|
|
resolver = "2"
|
|
members = [
|
|
"crates/core",
|
|
"crates/render",
|
|
"crates/delivery",
|
|
"crates/webui",
|
|
"crates/http-api",
|
|
"crates/openmfa",
|
|
]
|
|
|
|
[workspace.package]
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
rust-version = "1.90"
|
|
license = "MIT OR Apache-2.0"
|
|
repository = "https://gitea.milesward.dev/mward4/OpenMFA"
|
|
authors = ["OpenMFA contributors"]
|
|
|
|
[workspace.dependencies]
|
|
tokio = { version = "1", features = ["full"] }
|
|
axum = { version = "0.8", features = ["macros", "multipart"] }
|
|
tower-http = { version = "0.6", features = ["trace", "limit"] }
|
|
# rustls, never native-tls: an OpenSSL dependency would break the static
|
|
# musl build. `multipart` carries the rendered tile up to Discord.
|
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "multipart"] }
|
|
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
|
anyhow = "1"
|
|
thiserror = "2"
|
|
clap = { version = "4", features = ["derive", "env"] }
|
|
uuid = { version = "1", features = ["v4", "serde"] }
|
|
time = { version = "0.3", features = ["serde", "serde-human-readable", "formatting"] }
|
|
parking_lot = "0.12"
|
|
url = "2"
|
|
percent-encoding = "2"
|
|
base64 = "0.22"
|
|
|
|
# ── TOTP primitives ──
|
|
# The RFC 6238 core is ~30 lines over HMAC, and we implement it directly
|
|
# rather than pulling `totp-rs`. The deciding factor was otpauth:// parsing:
|
|
# real-world URIs routinely carry a path issuer that disagrees with the
|
|
# `?issuer=` query param (Microsoft does this), and secrets that are
|
|
# lowercase or `=`-padded (Google, Dropbox). totp-rs rejects all of those,
|
|
# so we'd be writing the normalisation and reconciliation layer anyway.
|
|
# Pinned to the mature 0.10/0.12 RustCrypto generation.
|
|
hmac = "0.12"
|
|
sha1 = "0.10"
|
|
sha2 = "0.10"
|
|
base32 = "0.5"
|
|
|
|
# ── Secrets at rest ──
|
|
# XChaCha20-Poly1305 over AES-GCM for the 192-bit nonce: large enough to
|
|
# generate randomly per record with no birthday-collision worry, and
|
|
# constant-time in software on every target without AES-NI.
|
|
chacha20poly1305 = "0.10"
|
|
argon2 = "0.5"
|
|
rand = "0.8"
|
|
zeroize = { version = "1", features = ["derive"] }
|
|
|
|
# ── Imaging ──
|
|
# default-features = false is REQUIRED: `image`'s default feature set
|
|
# enables `avif`, dragging in the whole rav1e encoder for a format we never
|
|
# touch. (Never enable `avif-native` — that one is C bindings to libdav1d
|
|
# and would break the static musl build outright.)
|
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "bmp", "webp"] }
|
|
rqrr = "0.10"
|
|
|
|
# Tile rendering. The tile is drawn as SVG and rasterised, rather than
|
|
# blitting glyphs by hand: text centring, rounded corners and gradients are
|
|
# all one attribute each in SVG and a pile of arithmetic otherwise.
|
|
#
|
|
# `default-features = false` matters here too — resvg defaults to
|
|
# `system-fonts` + `memmap-fonts`, and a static binary in a slim container has
|
|
# no system fonts to find. Text would silently render blank. We disable both
|
|
# and register the embedded faces explicitly.
|
|
resvg = { version = "0.48", default-features = false, features = ["text"] }
|
|
usvg = { version = "0.48", default-features = false, features = ["text"] }
|
|
tiny-skia = "0.12"
|
|
|
|
openmfa-core = { path = "crates/core" }
|
|
openmfa-render = { path = "crates/render" }
|
|
openmfa-delivery = { path = "crates/delivery" }
|
|
openmfa-webui = { path = "crates/webui" }
|
|
openmfa-http-api = { path = "crates/http-api" }
|
|
|
|
[workspace.lints.rust]
|
|
unsafe_code = "deny"
|
|
rust_2018_idioms = { level = "warn", priority = -1 }
|
|
|
|
[workspace.lints.clippy]
|
|
pedantic = { level = "warn", priority = -1 }
|
|
module_name_repetitions = "allow"
|
|
missing_errors_doc = "allow"
|
|
missing_panics_doc = "allow"
|
|
must_use_candidate = "allow"
|
|
doc_markdown = "allow"
|
|
items_after_statements = "allow"
|
|
cast_possible_truncation = "allow"
|
|
cast_lossless = "allow"
|
|
cast_sign_loss = "allow"
|
|
cast_precision_loss = "allow"
|
|
similar_names = "allow"
|
|
too_many_lines = "allow"
|
|
|
|
[profile.release]
|
|
lto = "thin"
|
|
codegen-units = 1
|
|
strip = "symbols"
|
|
opt-level = 3
|