diff --git a/README.md b/README.md index 0c7fba0..67397ed 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,23 @@ State is an architectural boundary here, not a preference. One image, one Redis, one SPA. The only difference is what sits in front and which URL you hand to Telegram and Stalwart. +**The Cloudflare Worker gateway is optional.** Nothing in the backend requires it, and the +backend runs identically with or without it. It exists for one reason: Cloudflare's Load +Balancer product is not available on the free plan, so a Worker is the way a free-plan account +puts load sharing and failover in front of several backends. + +**It is also not a security layer, and it is not meant to be one.** There is no intent to hide +the backends and no intent to harden them; the origins stay directly reachable whether the +gateway is present or not, and no access control is added at the edge. The fixed route safelist +is what makes it a load balancer for a bounded set of paths, not a firewall. + | | Form A: backend origin only | Form B: behind the Cloudflare Worker gateway | |---|---|---| | What answers requests | one backend host | one Worker URL, N backend origins | | Failover | none — a down origin stays down | bounded: 2 attempts by default, only timeout and `5xx` retry | | Callback URL you register | `https://a.example` | `https://lb.example` | -| Reachable routes | every route, including `/api/bootstrap` and `/debug/*` | only the 19 safelisted paths | +| Backend access | direct from the network | equally direct — the gateway adds no access control | +| Gateway route set | n/a — there is no gateway | fixed at 19 paths; unknown paths answer `404` | | Health to watch | `GET /ready` on the origin | `GET /ready` proxied, plus `GET /healthz-worker` for the gateway aggregate | | Extra moving parts | none | one Worker deploy, one origin list | | Docs | `docs/deployment.md §2` | `docs/deployment.md §10`, `cloudflare-worker/README.md` | @@ -56,8 +67,10 @@ business config, because a callback can land on any instance and no instance can the one asked for (`C-LB-SHARED-SECRETS`). Mis-matched secrets show up as a random 401, not as a routing error. -`POST /api/bootstrap` and `/debug/*` stay origin-only in both forms, so neither is exposed -through the gateway. +`POST /api/bootstrap` and `/debug/*` are not in the gateway's route set, so requests addressed +to the Worker URL never reach them. That is the safelist working as designed for the paths that +need to be load balanced; it is not hiding anything, because the same origins are reachable at +their own addresses in both forms. ## 3. 5-minute setup @@ -136,6 +149,11 @@ startup. It is only compared in constant time, and is never echoed, logged, or s Redis ACL password, if you have one, authenticates the Redis connection alone and is not the credential for any HTTP endpoint. +The optional Cloudflare Worker gateway in front of the backends is not part of this boundary. +It holds no credential and checks no auth header, and the backends behind it are directly +reachable either way, so nothing about it changes what stands between an attacker and your +mail. See §2. + ## 6. Where to read more | Document | What it answers | diff --git a/README.zh-CN.md b/README.zh-CN.md index 5124a38..7037aec 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -29,23 +29,28 @@ MessageWeave 位于 JMAP 邮件服务器(例如 Stalwart)与 Telegram bot 同一个镜像、同一个 Redis、同一套 SPA。唯一的区别是前面放什么,以及你把哪个 URL 交给 Telegram 和 Stalwart。 +**Cloudflare Worker 网关是可选项。** 后端没有任何东西依赖它,有它和没有它跑起来完全一样。它存在的理由是单一的:Cloudflare 的 Load Balancer 产品在免费套餐上不可用,所以 Worker 是免费套餐账户在多个后端前面做负载均衡与故障转移的办法。 + +**它也不是安全层,本意不是拿来干这个的。** 没有隐藏后端的意图,也没有加固后端的意图;网关在不在,origin 都同样可以直接访问,边缘上也没有加任何访问控制。固定的路由白名单是让它在有限的一组路径上做负载均衡的手段,不是防火墙。 + | | 形态 A:仅后端 origin | 形态 B:前置 Cloudflare Worker 网关 | |---|---|---| | 谁应答请求 | 一个后端主机 | 一个 Worker URL 对 N 个后端 origin | | 故障转移 | 没有——origin 挂了就一直挂着 | 有界——默认 2 次尝试,只重试超时与 `5xx` | | 你注册的回调 URL | `https://a.example` | `https://lb.example` | -| 可达路由 | 全部路由,含 `/api/bootstrap` 与 `/debug/*` | 只有 19 个白名单路径 | +| 后端可达性 | 网络上直连可达 | 同样直连可达——网关不加访问控制 | +| 网关路由集 | 不适用——没有网关 | 固定 19 个路径;未知路径返回 `404` | | 要看哪个健康接口 | origin 上的 `GET /ready` | 透传的 `GET /ready`,外加网关聚合 `GET /healthz-worker` | | 额外组件 | 无 | 一个 Worker 部署、一份 origin 列表 | | 文档 | `docs/deployment.md §2` | `docs/deployment.md §10`、`cloudflare-worker/README.zh-CN.md` | 单 origin 且没有 HA 要求时选形态 A;两个及以上 origin、或想在滚动与重建某个实例时保持一个稳定的回调 URL,就选形态 B。 -切换是一次重新注册,不是迁移。注册是幂等的,注册新地址会注销旧订阅,所以从形态 A 换到形态 B(或反过来)只是 SPA 里改一个字段。不要同时注册两个 origin:注册槽只有一个,平台入口必须保持内网(`C-LB-SINGLE-REG-URL`)。 +切换是一次重新注册,不是迁移。注册是幂等的,注册新地址会注销旧订阅,所以从形态 A 换到形态 B(或反过来)只是 SPA 里改一个字段。不要同时注册两个 origin:注册槽只有一个,后端的平台入口不对外注册(`C-LB-SINGLE-REG-URL`)。注意这里说的是「不注册」,不是「不可达」——两种形态下 origin 都同样可以直接访问。 形态 B 里没有任何 secret 搬到网关。它不持有任何业务凭据,也不校验任何认证头。真正要求一致的是每个后端都携带同一组 `SAF-AUTH-*` secret 与同一份加密业务配置——因为回调可能落在任何一个实例上,而任何实例都证明不了自己是被点名的那一个(`C-LB-SHARED-SECRETS`)。凭据不一致表现为随机 401,而不是路由错误。 -`POST /api/bootstrap` 与 `/debug/*` 在两种形态下都只保留在 origin 直连,不经过网关暴露。 +`POST /api/bootstrap` 与 `/debug/*` 不在网关的路由集里,所以发给 Worker URL 的请求到不了它们。这是白名单在需要被负载均衡的那些路径上按设计工作,并不是在隐藏什么——两种形态下 origin 都同样可以通过自己的地址直接访问。 ## 3. 五分钟跑起来 @@ -107,6 +112,8 @@ Telegram 发送。它由双因子开关控制——进程必须带 `--debug` ** 有一条推论与部署直接相关:SPA 管理凭据——也就是 bootstrap 的信任根——就是 `CONFIG_ENCRYPTION_KEY` 本身,即启动时提供的那个 32 字节 hex 值。它只做常数时间比较,绝不回显、记录或落盘。Redis ACL 密码(如有)只认证 Redis 连接本身,不是任何 HTTP 端点的认证凭据。 +前置在后端前面的可选 Cloudflare Worker 网关不在这个边界内。它不持有任何凭据,也不校验任何认证头;网关在不在,后端都同样可以直接访问,所以它不会改变攻击者与你的邮件之间的任何东西。见 §2。 + ## 6. 去哪读更多 | 文档 | 回答什么 | diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index 99e8494..e77b3bf 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -10,11 +10,21 @@ multi-origin failover (HA/LB sub-project). **Pass-through model**: the Worker do business payloads and forwards the request as-is to the backend https origins; only *timeout / 5xx* triggers a bounded failover to another instance. -> This component is a **safelist-restricted edge load balancer**: the route set is fixed (19 -> whitelisted paths), the backend origins are fixed at deploy time and https-only, and every -> unknown path gets a `404`. It only forwards requests to those fixed backends and fails over -> between them. It does not accept an arbitrary target host and provides no general-purpose -> traffic-relay or access-hiding capability. +> This component is a **load balancer for the fixed set of routes MessageWeave needs behind one +> URL**: the route set is fixed (19 paths), the backend origins are fixed at deploy time and +> https-only, and every unknown path gets a `404`. It only forwards requests to those fixed +> backends and fails over between them. It does not accept an arbitrary target host and +> provides no general-purpose traffic-relay capability. +> +> **Why it exists.** Cloudflare's Load Balancer product is not available on the free plan, so +> this is the way a free-plan account gets multi-origin load sharing and failover in front of +> its MessageWeave backends. That is the whole reason for it, and the gateway is optional: the +> backend runs identically without it. +> +> **Why it is not a security feature.** There is no intent here to hide the backends and no +> intent to harden them. The origins stay directly reachable with or without the gateway and no +> access control is added at the edge, so the route safelist is not a firewall and nothing in +> this document is meant to be read as protection. > Design basis: `docs/design.md §11.3` (`NFR-HA-MULTI-INSTANCE`) and `docs/deployment.md §10`. > Security baseline and prohibitions: `docs/charter.md §3`, `docs/charter.md §5`; stable-ID @@ -29,6 +39,10 @@ business payloads and forwards the request as-is to the backend https origins; o the same set of `SAF-AUTH-*` secrets — `RECONCILE_TOKEN`, `TG_WEBHOOK_SECRET` and the encrypted business config — because a callback can land on any instance and no instance can prove which one was asked (`C-LB-SHARED-SECRETS`). Per-instance secrets mean a random 401. +- **Not a security layer.** It is a load balancer, not an access-control boundary. The backend + origins are directly reachable with or without it, and nothing here hides or hardens them. The + safelist's fail-closed answer on unknown paths is what makes the balancer well-defined for a + fixed set of paths, not a protection to be relied on. - **No origin discovery.** Origins come from the deploy-time origin list only. - **Zero runtime dependencies.** Pure ES2022 plus the platform `fetch`, `Headers`, `Request`, `Response` and `URL`. `wrangler` is a devDependency, used only by `check`, `deploy` and diff --git a/cloudflare-worker/README.zh-CN.md b/cloudflare-worker/README.zh-CN.md index 64d746c..0ae9c26 100644 --- a/cloudflare-worker/README.zh-CN.md +++ b/cloudflare-worker/README.zh-CN.md @@ -4,7 +4,11 @@ 面向一个或多个 MessageWeave 后端的统一 HTTPS 入口,带**有界多 origin 故障转移**(HA/LB 子项目)。**透传模型**:Worker 不解析业务载荷,把请求原样转发给后端 https origin;只有*超时 / 5xx* 才触发有界的故障转移到另一个实例。 -> 本组件是**基于白名单限制的边缘负载均衡器**:路由集合固定(19 个白名单路径)、后端 origin 在部署时固定且仅允许 https、任何未知路径一律 `404`。它只把这些固定后端之间的请求做转发与故障转移,不接受任意目标主机,也不提供任何通用的流量中转或隐身能力。 +> 本组件是**针对 MessageWeave 需要放在同一个 URL 后面的一组固定路由的负载均衡器**:路由集合固定(19 个路径)、后端 origin 在部署时固定且仅允许 https、任何未知路径一律 `404`。它只把这些固定后端之间的请求做转发与故障转移,不接受任意目标主机,也不提供通用的流量中转能力。 +> +> **它的用途是负载均衡,不是防护。** Cloudflare 的 Load Balancer 产品在免费套餐上不可用,所以这是免费套餐账户在它的那些 MessageWeave 后端前面做多 origin 负载分摊与故障转移的办法。这就是它存在的唯一理由,网关是可选的:后端有它和没有它跑起来完全一样。 +> +> **它不是安全特性。** 这里没有隐藏后端的意图,也没有加固后端的意图。网关在不在,origin 都同样可以直接访问,边缘上也没有加任何访问控制,所以路由白名单不是防火墙,本文档任何一处都不该被读成防护。 > 设计依据:`docs/design.md §11.3`(`NFR-HA-MULTI-INSTANCE`)与 `docs/deployment.md §10`。 > 安全基线与禁令:`docs/charter.md §3`、`docs/charter.md §5`;稳定 ID 注册表 `docs/charter.md §8`。 @@ -13,6 +17,7 @@ - **只转发。** method、headers(含认证头)、body 一律原样转发。Worker 自身不做任何认证,也不改写任何内容。 - **无 secret。** 没有 Telegram bot token,没有 JMAP 密码,没有 session secret。它收到什么认证头就转发什么,自己一个都不校验。真正共享的是后端之间必须一致的同一组 `SAF-AUTH-*` secret——`RECONCILE_TOKEN`、`TG_WEBHOOK_SECRET` 与加密后的业务配置——因为回调可能落在任何一个实例上,而任何实例都证明不了自己是被点名的那一个(`C-LB-SHARED-SECRETS`)。凭据不一致的表现是随机 401,而不是路由错误。 +- **不是安全层。** 它是负载均衡器,不是访问控制边界。后端 origin 有没有它都同样直连可达,这里既没有隐藏也没有加固它们。白名单对未知路径的 fail-closed 应答,是让负载均衡器在一组固定路径上定义明确的手段,不是可以依赖的防护。 - **不做 origin 发现。** origin 只来自部署时的 origin 列表。 - **零运行时依赖。** 纯 ES2022 加平台提供的 `fetch`、`Headers`、`Request`、`Response`、`URL`。`wrangler` 是 devDependency,只用于 `check`、`deploy`、`dev`。 - **零状态。** 不连 Redis、不连 JMAP、不连数据库(`C-NO-DB`、`C-REDIS-ONLY-STATE`)。唯一的可变状态是 isolate 内的健康探测缓存。 diff --git a/docs/charter.md b/docs/charter.md index baa8162..4e4cc3d 100644 --- a/docs/charter.md +++ b/docs/charter.md @@ -265,7 +265,7 @@ Answer only one question per document; **Tables, lists, or values ​​may not | `SAF-JMAP-URL` | docs/design.md §7.1 / §4 | JMAP URL constraints: HTTPS only, disallow inline credentials, deny dangerous query | Security | | `REQ-JMAP-RAW-MULTIPART` | docs/design.md §3.2/§10.1 | `read_email` Multi-part Original text: Splice the parts "with part_id and bodyValue" in text_body order; no available part → clear error | Requirements | | `GATE-G1-JMAP-READONLY` | docs/design.md §10.1 / §6 | G1 access control: read-only adapter **Code has been implemented** (mock + `#[ignore]` real machine test), **to be real `cargo test -- --ignored jmap::` verification** | Process | -| `ARCH-LB-WORKER` | docs/deployment.md §10 | Multi-instance LB/HA: Free Cloudflare Worker as the only external entrance + failover, the backend is multi-platform with the same mirror | Architecture | +| `ARCH-LB-WORKER` | docs/deployment.md §10 | Multi-instance LB/HA: a free-plan Cloudflare Worker as the only *registered* entrance, plus failover. The backends stay directly reachable and it adds no access control | Architecture | | `C-LB-SINGLE-REG-URL` | docs/deployment.md §10.1 | Telegram/Push/Cron only registers the stable URL of the Worker; the backend platform entrance does not register externally | Constraints | | `C-LB-SHARED-SECRETS` | docs/deployment.md §10.3 | Multiple instances must share the same set of `SAF-AUTH-*` secrets, otherwise a random 401 | Constraints | | `SAF-LB-PASSTHRU` | docs/deployment.md §10.3 / §3 | Trust model = transparent transmission: Worker does not overwrite authentication; the backend must continue fail-closed verification (the backend may be directly connected to the public network) | Security | diff --git a/docs/charter.zh-CN.md b/docs/charter.zh-CN.md index e30110a..fd1919a 100644 --- a/docs/charter.zh-CN.md +++ b/docs/charter.zh-CN.md @@ -265,7 +265,7 @@ Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查 | `SAF-JMAP-URL` | docs/design.md §7.1 / §4 | JMAP URL 约束:仅 HTTPS、禁止内嵌凭据、拒绝危险 query | 安全 | | `REQ-JMAP-RAW-MULTIPART` | docs/design.md §3.2/§10.1 | `read_email` 多 part 原文:按 text_body 顺序拼接"有 part_id 且 bodyValue"的部分;无可用部分→明确错误 | 需求 | | `GATE-G1-JMAP-READONLY` | docs/design.md §10.1 / §6 | G1 门禁:只读 adapter **代码已实现**(mock + `#[ignore]` 真机测试),**待真实 `cargo test -- --ignored jmap::` 验证** | 流程 | -| `ARCH-LB-WORKER` | docs/deployment.md §10 | 多实例 LB/HA:免费 Cloudflare Worker 作唯一对外入口 + 故障转移,后端为多平台同镜像 | 架构 | +| `ARCH-LB-WORKER` | docs/deployment.md §10 | 多实例 LB/HA:免费套餐 Cloudflare Worker 作唯一*注册*入口 + 故障转移;后端仍可直接访问,网关不加访问控制 | 架构 | | `C-LB-SINGLE-REG-URL` | docs/deployment.md §10.1 | Telegram/Push/Cron 只登记 Worker 的稳定 URL;后端平台入口不对外登记 | 约束 | | `C-LB-SHARED-SECRETS` | docs/deployment.md §10.3 | 多实例必须共享同一组 `SAF-AUTH-*` secret,否则随机 401 | 约束 | | `SAF-LB-PASSTHRU` | docs/deployment.md §10.3 / §3 | 信任模型=透传:Worker 不改写鉴权;后端必须继续 fail-closed 校验(后端可能被公网直连) | 安全 | diff --git a/docs/deployment.md b/docs/deployment.md index e6a0f39..0a29e01 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -42,7 +42,8 @@ only port `8080`. Do not mount a data or log volume. Full image contents are in Open the backend's configuration page at its origin for initial setup. Use `CONFIG_ENCRYPTION_KEY` to establish the admin session, then complete the one-time bootstrap -form. The bootstrap route is not exposed by the Worker gateway. The one-time bootstrap and +form. The bootstrap route is not in the gateway's route set, so it is only reachable at the +backend's own address. The one-time bootstrap and admin-session handlers are `src/notify.rs:834` and `src/notify.rs:1187`; saving business configuration uses `src/notify.rs:694`. Later edits can use the protected page through the Worker URL. @@ -176,10 +177,18 @@ The stable IDs in this section point to the project charter's registry. ## 10. Optional Cloudflare Worker gateway -The Worker provides one public HTTPS entry point for multiple backend origins. All backends -must share the same Redis and business configuration. The Worker forwards only its fixed -route allowlist; bootstrap and `/debug/*` remain origin-only. Detailed route facts are in -the [gateway route matrix](reference.md#4-gateway-vs-backend-route-matrix). +The gateway is optional; nothing in the backend requires it. It provides one public HTTPS +entry point for multiple backend origins. All backends must share the same Redis and business +configuration. Detailed route facts are in the +[gateway route matrix](reference.md#4-gateway-vs-backend-route-matrix). + +Its purpose is load balancing, not protection. Cloudflare's Load Balancer product is not +available on the free plan, and this is how a free-plan account puts load sharing and failover +in front of several origins. There is no intent to hide the backends and no intent to harden +them: the origins are directly reachable whether the gateway is present or not, and no access +control is added at the edge. The fixed route allowlist is what bounds the paths the balancer +forwards — bootstrap and `/debug/*` are not in it, so requests addressed to the gateway never +reach them — but it is not a firewall. ### 10.1 Topology diff --git a/docs/deployment.zh-CN.md b/docs/deployment.zh-CN.md index f829342..7da9aab 100644 --- a/docs/deployment.zh-CN.md +++ b/docs/deployment.zh-CN.md @@ -41,7 +41,7 @@ docker run --rm --env-file .env -p 8080:8080 messageweave:latest 在原点打开后端的配置页面进行初始设置。使用 `CONFIG_ENCRYPTION_KEY` 建立管理会话,然后完成一次性引导 -表单。Worker 网关不会公开 bootstrap 路由。bootstrap 和管理会话处理器位于 +表单。bootstrap 路由不在网关路由集内,因此只能在后端自己的地址上访问。bootstrap 和管理会话处理器位于 `src/notify.rs:834` 与 `src/notify.rs:1187`;业务配置保存处理器位于 `src/notify.rs:694`。后续修改可通过 Worker URL 打开受保护的配置页。 @@ -156,10 +156,16 @@ Telegram 和 Stalwart 的稳定 Worker URL;不要在网关处公开凭据。 ## 10. 可选的 Cloudflare Worker 网关 -Worker 为多个后端源提供一个公共 HTTPS 入口点。所有后端 -必须共享相同的Redis和业务配置。 Worker 仅转发其固定的 -路由白名单; bootstrap 和 `/debug/*` 仍然仅限原始。详细路线事实在 -[网关路由矩阵](reference.zh-CN.md#4-网关与后端路由对照表)。 +网关是可选项,后端没有任何东西依赖它。它为多个后端 origin +提供一个公共 HTTPS 入口。所有后端必须共享同一 Redis 和业务配置。 +详细的路由事实在[网关路由矩阵](reference.zh-CN.md#4-网关与后端路由对照表)中。 + +它的用途是负载均衡,不是防护。Cloudflare 的 Load Balancer 产品在免费 +套餐上不可用,这就是免费套餐账户在多个 origin 前面做负载均衡与故障转移 +的办法。没有隐藏后端的意图,也没有加固后端的意图:网关在不在,origin +都同样可以直接访问,边缘上也没有加任何访问控制。固定的路由白名单只是 +限定负载均衡器转发哪些路径——bootstrap 和 `/debug/*` 不在其中,所以发给 +网关的请求到不了它们——但它不是防火墙。 ### 10.1 部署拓扑 diff --git a/docs/reference.md b/docs/reference.md index 4366aac..4af0c44 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -319,8 +319,8 @@ backend pool returns **503** rather than passing the request through. | Path | Why it is absent from the gateway | |---|---| -| `POST /api/bootstrap` | One-shot trust bootstrap; kept off the public path | -| `/debug/*` (7 routes) | Opt-in remote-debug surface (`SAF-DEBUG-GATE`); absent from `SAFE_ROUTES`, so it is reachable **only** by talking to the backend origin directly | +| `POST /api/bootstrap` | One-shot trust bootstrap; not in the route set, so it is only reachable at the backend's own address | +| `/debug/*` (7 routes) | Opt-in remote-debug surface (`SAF-DEBUG-GATE`); not in `SAFE_ROUTES`, so it is reachable **only** at the backend origin's own address | `GET, PUT /api/enabled` (the `SAF-ENABLE-FLAG` kill switch) **is** forwarded, because the admin SPA serves it at the Worker URL and toggles it from the service card (`loadEnabled` @@ -700,9 +700,9 @@ variable semantics and Redis business configuration fields are in [§5](#5-envir Telegram, JMAP, allowlist, worker, reconcile, and LLM business settings are Redis-resident, not process environment variables. The field list and validation rules in [§5.2](#52-redis-resident-business-configuration) -are authoritative. A successful initial save can use the one-shot `/api/bootstrap` endpoint -on the backend origin; that route is not exposed through the Worker. Subsequent edits use the -protected configuration API. +are authoritative. A successful initial save can use the one-shot `/api/bootstrap` endpoint at +the backend's own address; it is not in the gateway's route set, so a request addressed to the +Worker never reaches it. Subsequent edits use the protected configuration API. ### 9.3 Callback registration and scheduled work diff --git a/docs/reference.zh-CN.md b/docs/reference.zh-CN.md index 779f81e..57dd3e8 100644 --- a/docs/reference.zh-CN.md +++ b/docs/reference.zh-CN.md @@ -314,8 +314,8 @@ all:`SAFE_ROUTES` 中缺少的路径返回 **404**,一个已注册的路径 |路径|为什么网关上没有它 | |---|---| -| `POST /api/bootstrap` |一次性信任引导;远离公共道路| -| `/debug/*`(7 条路线)|选择加入远程调试界面(`SAF-DEBUG-GATE`); “SAFE_ROUTES”中不存在,因此**只能**通过直接与后端源通信来访问它 | +| `POST /api/bootstrap` | 一次性信任引导;不在路由集内,只能在后端自己的地址上访问 | +| `/debug/*`(7 条路线)| 选择加入的远程调试界面(`SAF-DEBUG-GATE`);不在 `SAFE_ROUTES` 中,因此**只能**在后端源自己的地址上访问 | “GET, PUT /api/enabled”(“SAF-ENABLE-FLAG”终止开关)**被转发,因为 管理 SPA 在 Worker URL 上提供服务,并从服务卡(`loadEnabled` @@ -690,9 +690,9 @@ HostStack 部署不使用仓库的 Dockerfile;镜像构建和运行时由平 Telegram、JMAP、allowlist、worker、reconcile 和 LLM 业务设置驻留在 Redis 中, 不是进程环境变量。 [§5.2](#52-存储在-redis-中的业务配置)中的字段列表和验证规则 -是权威的。成功的初始保存可以使用一次性“/api/bootstrap”端点 -在后端原点;该路由不通过 Worker 公开。后续编辑使用 -受保护的配置 API。 +是权威的。成功的初始保存可以使用一次性 `/api/bootstrap` 端点,在后端 +自己的地址上完成;该路由不在网关路由集内,所以发给 Worker 的请求到不了它。 +后续编辑使用受保护的配置 API。 ### 9.3 回调注册与定时任务