新增 src/debug.rs:/debug/ping、/config、/redis、/jmap、/telegram、/worker 六个只读 探针 + POST /debug/notify 一条测试出站消息。 SAF-DEBUG-GATE 双因子,缺任一即完全不挂载路由(请求落到通用 404,无"未配置即放行" 降级): - 启动命令带 --debug(main.rs 最小 std::env::args 解析,未新增依赖) - Secret 环境变量 DEBUG_TOKEN 非空 两层执行点:main.rs 推导 debug_token 并传入 router builder(None 则 debug_router 不被 merge);debug.rs 侧 auth_token 缺失时直接返回空 Router。 SAF-DEBUG-AUTH:每个 handler 先做 Authorization: Bearer DEBUG_TOKEN 常数时间比较 (复用 worker_authorized),失败 401,鉴权通过前不产生任何副作用。 探针只读侧全部复用现成实现,不另写一套:Redis 走 is_enabled,JMAP 走 probe_jmap_session,Telegram 走 probe_telegram_get_me,出站走唯一构造点 business_client(notify.rs 新增 pub(crate))到 send_text,/debug/notify 与 refresh_business_config 共用同一条客户端构造路径。 SAF-NO-SECRET-ECHO / SAF-LOG-PURITY:/debug/config 只回"是否配置"、计数与超时等 运行时参数,bot token / JMAP password / worker / webhook / reconcile token / LLM key 一律不出现在响应体;DEBUG_TOKEN 只在鉴权内部暴露,不回显、不入日志,日志仅一条 "debug endpoints enabled" 且不含令牌值。allowlist 用 chat_allowlist_snapshot 取生效值 (运行时业务配置优先、否则启动值),/debug/notify 显式 chat_id 不在 allowlist 则 403。 ARCH-LB-WORKER 白名单不含 /debug/*,该面仅供直连 origin 联调,公网入口不可达。 AGENTS.md 登记 4 个稳定 ID:MOD-DEBUG / SAF-DEBUG-GATE / SAF-DEBUG-AUTH / REQ-DEBUG-ENDPOINTS(§2 第 19 条硬边界、§5 测试条、§7 索引 4 行)。 调试路由测试(4 项,notify.rs + debug.rs 内):Bearer 放行 200、缺失/错误令牌 401、 未提供 DEBUG_TOKEN 因子时 /debug/* 完全不注册而落 404、/debug/notify POST 未认证 401。 门禁四段全绿(rust:1-slim-bookworm):cargo fmt --all -- --check / cargo check --locked / cargo clippy --locked --all-targets -- -D warnings / cargo test --locked = 49 passed / 0 failed / 1 ignored。 |
||
|---|---|---|
| .github/workflows | ||
| cloudflare-worker | ||
| docs | ||
| src | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| README.md | ||
| README.zh-CN.md | ||
| hoststack.yaml | ||
README.md
MessageWeave
中文文档 / Chinese documentation → README.zh-CN.md
One Telegram bot per mailbox. New mail arrives as metadata; the original is always fetched read-only from your own JMAP server.
1. What it is
MessageWeave sits between a JMAP mail server (for example Stalwart) and a Telegram bot.
- Inbound. A JMAP push subscription tells the service that a message changed. Nothing is polled at rest.
- Outbound. Each event becomes a short, metadata-only Telegram notification: sender, subject, timestamp. Mail bodies are never sent to Telegram.
- On demand. When you ask, the service reads the original through a fresh JMAP read call. Nothing is cached in between.
- Optionally summarised. If you explicitly opt in, an OpenAI-compatible LLM returns a short summary. Requests, responses and attachment bytes are never persisted.
State is an architectural boundary here, not a preference.
- Redis is the only state store. No database, no embedded store, no second store.
- The process writes nothing to disk. No log files, no data files, no local volume.
- No long-lived connections. No SSE, no WebSocket, no long polling.
- Logs go to stdout only. The platform's collector owns log storage and retention.
- What it is not. It is not a mail client, not a proxy, and not a relay. It forwards metadata to one bot; it does not read arbitrary URLs, open tunnels, or broker traffic.
2. 5-minute setup
Requires a reachable Redis 7 instance and a JMAP session plus a Telegram bot token.
cp .env.example .env # fill REDIS_URL and CONFIG_ENCRYPTION_KEY
docker build -t messageweave:latest .
docker run --env-file .env -p 8080:8080 messageweave:latest
Then open http://localhost:8080 in a browser. The first-boot SPA calls
POST /api/bootstrap, stores the business secrets in Redis, and creates the admin session.
From that point the browser configures the service through Redis — no environment variable
is read or written again for business configuration.
For local builds without Docker: cargo build --locked produces the message-weave binary.
If the service answers requests but does nothing useful, check the logs for a startup warning: a missing required variable does not crash the process. It serves a read-only setup router over in-memory state instead. See §3.
3. Configuration entry points
Three layers. They are not interchangeable.
Boot-time environment — 2 required, 2 optional.
| Variable | Required | Default |
|---|---|---|
REDIS_URL |
yes | — |
CONFIG_ENCRYPTION_KEY |
yes | — |
PORT |
no | 8080 |
RUN_MODE |
no | webhook |
RUN_MODE accepts webhook or reconcile. .env.example ships only the two required
variables; the rest are documented defaults.
Business configuration — Redis, written by the browser.
POST /api/bootstrap is the one-shot trust bootstrap; PUT /api/business-config hot-
reloads individual settings within about one second. The process never reads business
secrets from the environment in normal operation.
Health.
GET /healthz returns 200 unconditionally — it is liveness, not readiness. GET /ready
returns 503 until configuration, Redis, and both upstreams (a JMAP session GET and
Telegram getMe, 3s each) are reachable. Probe /ready, not /healthz, when deciding
whether to route traffic — but /ready is the heavier probe (~3s worst case), so a monitor
that must stay cheap should watch the gateway's aggregated /healthz.
4. Security boundary, in one sentence
All state lives in Redis; the process reads only two environment variables and writes nothing to disk.
One corollary matters for setup: the bootstrap one-shot token is the password component of
REDIS_URL. The Redis ACL credential is therefore both the application connection
credential and the trust root for bootstrap — keep it private and rotate it as one value.
5. Where to read more
| Document | What it answers |
|---|---|
docs/design.md |
Why the system is shaped this way: data flow, JMAP semantics, Redis streams, AI consent rules |
docs/deployment.md |
How to deploy: Dockerfile, secrets, Redis hosting, webhook/push/reconcile routing, multi-instance load balancing |
docs/reference.md |
Single source of truth for verifiable facts — Redis keys and TTLs, error codes, routes, environment layers, budgets |
docs/roadmap.md |
Gaps, blockers and the next phase |
docs/retired.md |
What was tried and dropped — abandoned routes, unreleased designs, and names that never existed |
AGENTS.md |
Contribution rules, hard boundaries, the stable-ID index for cross-document references |
Facts that matter are traceable. If two documents disagree, docs/reference.md wins.