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]>
82 lines
3.2 KiB
Docker
82 lines
3.2 KiB
Docker
# syntax=docker/dockerfile:1.7
|
||
#
|
||
# OpenMFA — multi-stage build.
|
||
#
|
||
# build : cross-compiles to x86_64-unknown-linux-musl with cargo-zigbuild.
|
||
# runtime : Alpine + the static binary. ~15 MB total.
|
||
#
|
||
# ## Why cross-compile instead of building for the target platform
|
||
#
|
||
# The obvious `FROM --platform=linux/amd64 rust` runs the entire compiler
|
||
# under QEMU when the builder is an Apple-Silicon Mac. That is roughly 15×
|
||
# slower, and the emulated linker intermittently SIGSEGVs part-way through.
|
||
#
|
||
# Pinning this stage to $BUILDPLATFORM instead runs rustc natively at full
|
||
# speed and emits x86_64 via cargo-zigbuild, which uses `zig cc` as the
|
||
# cross-linker — zig ships a musl sysroot for every target, so there is no
|
||
# cross-gcc toolchain to assemble. Deterministic, and minutes rather than
|
||
# half an hour.
|
||
|
||
ARG RUST_VERSION=1.95
|
||
|
||
########## build ##########
|
||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-bookworm AS build
|
||
WORKDIR /src
|
||
|
||
# zig (via the `ziglang` pip package — cargo-zigbuild discovers it as
|
||
# `python3 -m ziglang`) supplies the musl sysroot and the linker.
|
||
RUN apt-get update \
|
||
&& apt-get install -y --no-install-recommends python3 python3-pip \
|
||
&& rm -rf /var/lib/apt/lists/* \
|
||
&& rustup target add x86_64-unknown-linux-musl \
|
||
&& pip3 install --no-cache-dir --break-system-packages ziglang \
|
||
&& cargo install --locked cargo-zigbuild
|
||
|
||
# Deliberately NOT copying rust-toolchain.toml: it pins `channel = "stable"`
|
||
# for local development, and inside the image that makes rustup download a
|
||
# second full toolchain — slow, and enough to fill a small Colima disk.
|
||
COPY Cargo.toml Cargo.lock ./
|
||
COPY crates/ crates/
|
||
|
||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||
--mount=type=cache,target=/src/target,sharing=locked \
|
||
cargo zigbuild --release --target x86_64-unknown-linux-musl \
|
||
--bin openmfa \
|
||
&& cp target/x86_64-unknown-linux-musl/release/openmfa /openmfa \
|
||
&& ls -l /openmfa
|
||
|
||
########## runtime ##########
|
||
# Alpine, not Debian: the binary is fully static musl and shells out to
|
||
# nothing, and its TLS roots are compiled in via webpki-roots — so the image
|
||
# needs no ca-certificates package and no runtime libraries at all. What it
|
||
# does need is a shell, for the bind-mount chown in the entrypoint (see there
|
||
# for why that matters on Unraid).
|
||
FROM alpine:3.21 AS runtime
|
||
|
||
RUN apk add --no-cache tini su-exec \
|
||
&& addgroup -g 10001 -S openmfa \
|
||
&& adduser -u 10001 -S -G openmfa -h /var/lib/openmfa openmfa \
|
||
&& mkdir -p /var/lib/openmfa \
|
||
&& chown -R openmfa:openmfa /var/lib/openmfa
|
||
|
||
COPY --from=build /openmfa /usr/local/bin/openmfa
|
||
COPY deploy/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/openmfa
|
||
|
||
# NOT `USER openmfa` — the entrypoint starts as root to fix bind-mount ownership,
|
||
# then drops to uid 10001 via su-exec. Its non-root branch handles the case
|
||
# where the platform has already assigned a uid.
|
||
WORKDIR /var/lib/openmfa
|
||
|
||
ENV OPENMFA_WORK_DIR=/var/lib/openmfa \
|
||
OPENMFA_HTTP_ADDR=0.0.0.0:8720 \
|
||
OPENMFA_LOG=info,openmfa=info
|
||
|
||
EXPOSE 8720/tcp
|
||
VOLUME ["/var/lib/openmfa"]
|
||
|
||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||
CMD wget -qO- http://127.0.0.1:8720/healthz || exit 1
|
||
|
||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"]
|