Build and release image / build (push) Successful in 1m9s
No change to how the vault is written or recovered. This fixes what the service says when recovery is not possible, and adds a way out that is not "edit files inside a container that will not start". The refusal message was wrong in the case that actually matters. It said the backup "could not be used either" whether the backup was unparseable, empty, or simply absent — and absent is the common one, because a vault that has not completed a single write since v0.1.3 introduced backups has no .bak at all. That wording reads as though a file exists, and sends the operator hunting for it. The message now names which of the three it is, and lists any vault.json.corrupt-* files left by earlier recoveries. DA_VAULT_RESET=1 boots an unreadable vault as empty. It is checked only on the failure path, so it cannot affect a vault that loads, and it quarantines the unreadable file *before* writing the fresh one — the first draft of this logged that the file was preserved while persist would in fact have overwritten it. Refusing by default is still right: booting empty is indistinguishable from a vault emptied on purpose. But a refusal with no supported way forward is not a safety feature, it is a dead end. 149 tests.
116 lines
4.0 KiB
TOML
116 lines
4.0 KiB
TOML
[workspace]
|
|
resolver = "2"
|
|
members = [
|
|
"crates/core",
|
|
"crates/render",
|
|
"crates/discord",
|
|
"crates/webui",
|
|
"crates/http-api",
|
|
"crates/discord-authenticator",
|
|
]
|
|
|
|
[workspace.package]
|
|
version = "0.1.4"
|
|
edition = "2021"
|
|
rust-version = "1.90"
|
|
license = "MIT OR Apache-2.0"
|
|
repository = "https://gitea.milesward.dev/mward4/Discord-Authenticator"
|
|
authors = ["Discord Authenticator 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"
|
|
|
|
da-core = { path = "crates/core" }
|
|
da-render = { path = "crates/render" }
|
|
da-discord = { path = "crates/discord" }
|
|
da-webui = { path = "crates/webui" }
|
|
da-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
|