Commit Graph

24 Commits

Author SHA1 Message Date
anonsaber ed0f8172eb docs: state the gateway's actual design intent — a free-tier load balancer, not a security layer
Reframe the Cloudflare Worker gateway across the entry points where a
reader forms an impression of it, in both languages:

- README §2 / §5 (en+zh): the gateway is optional and exists because
  Cloudflare's Load Balancer product is not on the free plan. Drop the
  comparison row that listed 'only the 19 safelisted paths' against
  'every route' as if the safelist were a benefit; replace it with
  backend reachability, which is identical either way, and move the
  route-count fact to its own neutral row. Add one sentence to §5 that
  the gateway is outside the secret boundary.
- deployment.md §10 (en+zh): purpose is load balancing, not protection;
  no intent to hide or harden the backends, no access control at the
  edge, the allowlist is not a firewall.
- cloudflare-worker README (en+zh): new 'why it exists' / 'why it is
  not a security feature' block, plus a 'not a security layer' bullet.
  Drop 'access-hiding capability' from the non-capabilities list, since
  claiming invisibility as something we refuse to provide implies it is
  the interesting axis.
- reference.md §4 / §9.2 (en+zh) and deployment.md zh §2: stop saying
  bootstrap and /debug/* are 'not exposed' / 'kept off the public path'.
  They are simply not in the route set, which is why a request to the
  Worker never reaches them — reachable at the backend's own address
  either way.
- charter ARCH-LB-WORKER (en+zh): 'only external entrance' -> 'only
  *registered* entrance', which is what C-LB-SINGLE-REG-URL actually
  says, and backends stay directly reachable.

SAF-LB-PASSTHRU already records the security-side invariant (no auth
or body rewriting, backend may be directly connected to the public
network), so no new stable ID is needed. No code changes; GATE-DOCS
6/6.
2026-10-01 17:47:49 +08:00
anonsaber 1c1b1e644e worker: add the second origin messageweave-eu-2 to the LB origin list
BACKEND_ORIGINS_JSON now carries eu-1 and eu-2. Both are public https
origins, so the existing [vars] plaintext exception still applies and
nothing moves to secrets.

LB_VERSION stays at 2026.10.4: it is a code-version marker and the LB
logic is unchanged. The origin list is observable anyway through the
backends array in GET /healthz-worker, which now reports total 2.

Also drop a stale clause that said the origin list travels through
secret; it has lived in [vars] since aa78c13 and the same file says so
twice below it.

eu-2 needs its DNS record and a live backend before this deploy: with
LB_MAX_ATTEMPTS=2 both origins are covered, so a not-yet-deployed eu-2
costs a failed round-trip on about half the requests, and /reconcile
plus /worker run with maxAttempts=1 so those cron runs fail outright
on the half that land there first.
2026-10-01 17:18:35 +08:00
anonsaber 42128433ed docs: add the worker bilingual README and name both deployment forms
Root README gains §2 "Two ways to run it": a side-by-side table of
Form A (backend origin only) against Form B (behind the Cloudflare Worker
gateway) — what answers, failover, the callback URL to register, the
reachable route surface, which health endpoint to watch, and the docs to
read. Late in the switch: registration is idempotent and re-adding retires
the previous subscription, so the switch is one SPA field, not a migration;
nothing secret moves to the gateway; /api/bootstrap and /debug/* stay
origin-only either way. Later sections renumbered, and the monitoring
paragraph now points at the gateway aggregate GET /healthz-worker instead
of the origin-passthrough /healthz.

cloudflare-worker/README.md rewritten as the canonical English version and
cloudflare-worker/README.zh-CN.md added as its translation, covering the
route safelist (19 paths with per-path methods), the timeout/retry budget
with per-route overrides, bounded failover, the two health endpoints,
callback registration, config, local verification, deploy, and the design
invariants.

Both fixes along the way:

- README.zh-CN said the startup env block has 2 required + 2 optional
  variables; the table has 3 rows, 2 required + 1 optional.
- docs/charter.md §8 / registry list now names the zh-CN twin next to the
  gateway README.

scripts/docs_check gains the new file in validate_docs, check_tables,
audit_anchors and audit_anchor_semantics so it is checked like every other
document. check_sec_refs resolves §-refs against the document being scanned
when a basename is shared: README.md exists both at the repo root and under
cloudflare-worker/, and the basename-keyed heading map silently dropped one
of them. The gate was passing for the wrong reason before.

GATE-DOCS 6/6 clean: 72 anchors, 454 section refs, 727 path refs.
audit_anchor_semantics still reports the same 7 pre-existing candidates,
none in the new or edited files.
2026-10-01 13:40:39 +08:00
anonsaber 1d4959a855 feat(worker): redirect HTTP to HTTPS 2026-10-01 02:52:01 +08:00
anonsaber 7a27f7ff00 docs: add bilingual guides and callback setup in SPA 2026-10-01 02:36:03 +08:00
anonsaber f9889288ba docs: 同步 /worker 进 safelist(17 -> 18 条),cron 示例收敛到单一域名
- deployment.md:§6.3.1 两条端点都可直接打 Worker 域名,手动 curl 与 crontab、
  k8s CronJob 示例去掉 WORKER_ORIGIN 只留 APP_URL,-m 分别给 320s / 300s(要略大于
  LB 侧 per-route 超时,否则 curl 先掐掉一个本来能完成的排空);§10.4 safelist
  18 条并补 /worker、不在 safelist 的从 3 类减为 2 类;LB_WORKER_TIMEOUT_MS 补进
  secret put 清单与 §10.9 变量表;测试计数 35 -> 37(index 12 -> 14)
- README.md:路由白名单 17 -> 18 条、/worker 入列、LB_WORKER_TIMEOUT_MS 补进
  变量表与配置片段、/reconcile 例外段落扩成 /reconcile + /worker 两条长超时
- charter.md / reference.md:17 -> 18 条、Safe Routes 16 -> 18 entries
- cron-drain.sh:MW_WORKER_URL 由「必须指源站直连地址」改为「可选,默认走平台域名」;
  404 route not forwarded 的提示改为 LB 版本太旧(需 >= 2026.10.3),升级或绕过 LB

顺手修正 6 处已漂移的行号锚点(此前指向空行或已迁移的文件):
backends.js:10-28 -> 10-30、index.js:47-65 -> 54-73、index.js:76-78 -> 87-91、
index.js:87-89 -> 95-96、index.js:93 -> 105、src/index.js:138 -> src/lb.js:155
(x-lb-backend 的 catch 在 656034d 修不可变响应头时已迁到 lb.js,文档一直指旧文件)。
GATE-DOCS PASS(115 line-refs / 477 path-refs / 252 section-refs,0 error)。
2026-09-30 23:08:57 +08:00
anonsaber 6bb57801ac feat(lb): POST /worker 加入路由 safelist,走独立长超时且不故障转移
外部 cron 触发排空队列时,/worker 走 Worker 域名会被 404 route not forwarded
挡掉,只能直连后端 origin;而 /reconcile 早就能走 Worker。cron 脚本因此被迫
维护两个域名。现把 /worker 与 /reconcile 成对放进 safelist(17 -> 18 条),
cron 只需一个平台域名。

同时给它配独立超时:全局 LB_REQUEST_TIMEOUT_MS 默认 10s 会把一次正常的排空
误判成失败,而 maxAttempts=2 切到第二实例毫无意义——第二个实例只会重复排
同一批消息。故新增 LB_WORKER_TIMEOUT_MS(默认 300000ms,等于后端单条事件
下界 state.rs SINGLE_EVENT_CEILING_FLOOR_MS),并强制 maxAttempts=1。

- SAFE_ROUTES 加 "/worker";ROUTE_METHODS 加 "/worker": ["POST"](GET => 405)
- 新增 DEFAULT_WORKER_TIMEOUT_MS = 300_000 与 per-route 覆盖分支
- LB_VERSION 2026.10.2 -> 2026.10.3(改 LB 逻辑须 bump)
- test/index.test.js 新增回归:/worker maxAttempts=1、默认 300s、env 覆盖、
  GET /worker => 405 且不打到后端
- test/backends.test.js safelist 用例补 /worker 与 /api/business-config/preflight

node --test:37 通过 / 0 失败(index 14 / backends 6 / health 5 / lb 12)。
2026-09-30 23:08:41 +08:00
anonsaber 89dfe5cf3d feat(lb): /healthz 透传到源站,LB 聚合探针改挂 /healthz-worker
源站自己也暴露 /healthz;两者同路径时走 Worker 域名只能看到 LB 的聚合
结果,看不到源站自己的健康状态。现在拆开:

- GET /healthz        透传到源站(源站自己的健康检查)
- GET /healthz-worker LB 聚合探针(LB_VERSION + 后端汇总)

SAFE_ROUTES 增列 /healthz、ROUTE_METHODS 增列 /healthz: [GET],白名单 16 -> 17。
LB 探测源站的路径仍是 /healthz,不变。LB_VERSION 2026.10.1 -> 2026.10.2。

GATE-DOCS 6/6;node --check 全部通过。
2026-09-30 21:36:37 +08:00
anonsaber 15fa7f1e35 feat(lb): /healthz 回显版本号 LB_VERSION,确认线上是哪次部署在回答
上次生产事故里「改了代码不知道有没有生效」缺少直接证据;现在 /healthz 的
JSON 多一个 version 字段,取 env.LB_VERSION(写在 wrangler.toml 的 [vars],
当前 2026.10.1),未配置回退 unknown,探针永不因缺该字段而失败。

healthResponse(summary, version) 的第二参数可选:省略时 JSON 不含该字段
(JSON.stringify 丢弃 undefined),既有调用与用例不受影响。

放 [vars] 而不是 dashboard Add Variable 的理由:它必须跟代码同版本、可 diff,
写进 dashboard 反而容易漂。代价是改 LB 逻辑的提交要顺手 bump 它。

文档同步:§10.4 响应体形状加 version、§10.9 [vars] 示例与验证步骤、
README 环境变量表补一行;用例计数 32 → 35。

验证:node --test 35/35;wrangler deploy --dry-run 通过;docs gate 6/6。
2026-09-30 20:21:48 +08:00
anonsaber 656034d71d fix(lb): 可观测头写入不可变响应头不再把健康的 200 变成 503
mark() 往代理响应上写 `x-lb-backend`,但跨域且无 CORS 允许头的响应其
headers guard 是 immutable,`set` 抛 `TypeError: Can't modify immutable
headers.`;异常冒泡到 forwardWithFailover 的 catch 后被判成后端故障
(`backend ... unavailable (TypeError)`),重试耗尽返回 503。

后端一直健康:GET / 实际返回 200。/healthz 走 healthResponse() 自建
响应、不调 mark(),所以不受影响——这也正是它掩盖问题多年的原因。

mark() 现在在响应头不可变时静默放弃观测头,绝不阻塞响应返回;补两条
回归测试(2xx 不可变响应必须原样返回且不触发故障转移;5xx 不可变响应
仍正常故障转移)。

验证:wrangler dev --local 下 / /healthz /ready /api/status /assets/*
全部 200(修复前 / 与 /api/status 为 503);node --test 34/34 通过。
2026-09-30 20:02:18 +08:00
anonsaber aa78c13161 worker: BACKEND_ORIGINS_JSON 写入 wrangler.toml 的 [vars](明文例外)
Cloudflare Git 集成会提示"Update your Wrangler configuration with these changes to
keep deployments in sync",原因是 UI 上的 origin 列表没在仓库里。接受该建议:origin
列表只是公开可达的 https 地址,不是凭据,所以可以随仓库走——配置在 PR 里可审,
预览环境也能拿到它(secret 只存在于 dashboard,preview 拿不到)。

wrangler.toml 加了「明文例外的边界」评论:一旦出现内网地址 / 带内嵌凭据的 URL /
不想公开的 staging 主机名,立刻从 [vars] 删掉改用 wrangler secret put(git 历史
不可逆)。其余三个 LB_* 保持可调参数不写死。

已用实际解析器验证:parseBackendOrigins 返回 [{url: https://messageweave-eu-1.motofans.club,
healthPath: /healthz}];npm test 32 通过 0 失败;wrangler deploy --dry-run 通过,
配置里可见 env.BACKEND_ORIGINS_JSON 绑定。
2026-09-30 19:07:17 +08:00
anonsaber 7737693c2b worker: wrangler 升到 4.144.0,消掉 CI 里的 out-of-date 警告
CI 实测装出 3.114.17 并打印
'The version of Wrangler you are using is now out-of-date ... to prevent
critical errors'。锁文件重新生成后 wrangler 4.144.0:

- node 24.21.0 / npm 11.19.0: npm ci 干净装出 35 个包
- npm test: 32/32 通过
- npx wrangler deploy --dry-run --outdir=.build-check: 32.30 KiB 上传,
  无配置警告、无 out-of-date 提示
2026-09-30 18:08:48 +08:00
anonsaber caa78787a3 worker: 提交 package-lock.json,让 Cloudflare 的 Git 集成走 npm ci
没有 lockfile 时 Cloudflare CI 跑 npm install,wrangler ^3.57.0 是浮动范围,
每次 CI 可能解析到不同小版本,构建不可复现。锁定后 npm ci 精确复现
wrangler 3.114.17(= 本地已验证的版本)。

从干净目录端到端验证:npm ci → npm test 32/32 通过 → npm run check
(wrangler deploy --dry-run)通过,上传 31.48 KiB。
2026-09-30 16:47:27 +08:00
anonsaber 826b41e781 worker: 修掉两个会让 Worker 部署直接失败的配置错误
- wrangler.toml 用 JSDoc 的 /** */ 注释,TOML 解析器把 '*' 当未知字符
  (wrangler 报 Unknown character 47),deploy/secret put/dev 在读取配置阶段
  就失败,根本走不到你的 secret。全部展开成 '#' 行。
- npm test 写成 node --test test/,Node 22.23 会把 test/ 当模块路径去
  require,报 MODULE_NOT_FOUND。改成 node --test(无参数自动发现 test/),
  仍为 32 个用例全通过(backends 6 / health 4 / index 12 / lb 10)。

wrangler deploy --dry-run 现通过(wrangler 3.114.17,上传 31.48 KiB)。
2026-09-30 16:24:32 +08:00
anonsaber e3924a53c1 test: cover business config read-back and partial submit
Adds the missing test coverage for the GET /api/business-config read-back
and the partial-patch PUT, then syncs the docs to the behaviour the code
actually has.

src/notify.rs
- PUT response body now carries "revision" alongside the
  x-business-config-revision header, which the SPA surfaces in its status line.
- 4 test helpers (a router that returns (Router, MemoryState) so a config can be
  seeded first, a full valid wire value, a direct state writer that skips
  validation, and a wire value whose JMAP URL fails fast) plus 9 tests:
  - GET 401 leaks no business field at all (body is exactly error + request_id)
  - GET echoes the 10 non-secret values and returns secrets only as presence
    booleans; the serialised body is asserted to contain none of the 6 secret
    values (SAF-NO-SECRET-ECHO)
  - GET with nothing stored returns 200 configured:false revision:0, not 404
  - a first save with an incomplete patch is rejected and writes nothing
  - a partial submit replaces only the fields it carries, verified by reading
    the merged result back over the same path the SPA uses
  - validation runs on the merged result, so changing one field cannot bypass
    the rest, and a rejected submit neither persists nor bumps the revision
  - telegram_chat_id is accepted as a number or a numeric string and a garbage
    string is rejected without overwriting the stored value
  - a typo in a field name is rejected rather than silently dropped

src/config.rs
- Pure deserialisation test for de_i64_from_number_or_string, with no router and
  no store. This is the only place that separates "the custom deserializer
  refuses the string" from "a later validation step refused it" - a real
  distinction, since the SPA submits telegram_chat_id as a string on a partial
  submit while the read-back model returns a number.

cloudflare-worker/test/index.test.js
- The forwarded-route test asserted a fixed upstream origin, which is not stable:
  the LB rotates its start index between tests. Assert the origin is one of the
  configured backends and check the path and method exactly instead.

docs
- 17 spots bring the docs in line with the shipped contract: GET is now
  documented, PUT as a partial patch, the worker safelist at 16 routes, and the
  preflight path listed as forwarded.
- Corrections the docs had backwards: the response field is "configured"
  (revision is a u64, 0 when nothing is stored); Invalid returns 422, only
  Unreachable returns 503; there is no chat_allowlist_empty error code.
- "Blank keeps the stored value" is now stated as a client-side contract. The
  backend does not distinguish an omitted field from an empty string - apply()
  uses unwrap_or_else, so an explicitly submitted "" really does clear the
  secret (validation then rejects it). Saying it was server-side was wrong.
- POST /worker was listed as a forwarded route though it is not in SAFE_ROUTES,
  and the per-category counts did not match the lists they introduced.
- POST /api/bootstrap does not share the patch-merge contract; it deserialises a
  full BusinessConfigWire, which is the only path that can create a config from
  nothing.
- Re-anchored 9 src/notify.rs line refs that had drifted blank and raised the
  baseline to 86 passed / 0 failed / 2 ignored.

Gates: fmt, clippy -D warnings, test 86/0/2 ignored, worker 32/32, SPA 17/17,
GATE-DOCS 6/6 clean.
2026-09-30 01:24:42 +08:00
anonsaber 0cee50b4c9 feat: read business config back and accept partial patches
GET /api/business-config returns the stored values with presence-only secrets, and PUT now merges a patch against the stored config instead of replacing it, so the SPA can show what is already set and send back only the fields that changed.

The SPA marks secrets as set-but-unreadable, edits chat_allowlist as an unordered set, and omits fields it did not touch; blank required fields keep the stored value, blank optional fields clear it. The read-back is Serialize-only, so a client cannot inject secrets_present.

Forward both paths at the gateway: /api/business-config accepted PUT only, so the read-back returned 405, and the preflight path was missing from the safelist entirely, so it returned 404. The SPA is served from the Worker domain, so the whole edit experience was unreachable behind the production gateway. Route methods are now asserted per path, which the safelist test never checked.

Also: /debug/config reports timezone again, the timezone select is styled like the other fields, and the docs follow the new contract.
2026-09-30 00:53:58 +08:00
anonsaber 638c3a8cbc docs: consume all four self-checks; make the file-size rule enforceable
Every member's audit is now read and each real finding fixed, not just reported.

AGENTS.md §2.1
- The 500-line soft cap now has a machine-checkable exception: an over-limit
  file must carry a one-line `SPLIT-EVAL:` marker stating the split decision.
- Six files were over the cap with no justification: notify.rs 2036, state.rs
  1586, worker.rs 1045, config.js 1079, client.rs 601, config.rs 521. All now
  carry the marker. client.rs was missed by my own earlier line count, which
  did not descend into src/domain/jmap/.
- New validator scripts/docs_check/check_file_size.py; GATE-DOCS 4/4 -> 5/5.
  It proves the marker exists, not that the written reason is sound.
- Re-based 144 line anchors across six docs for the +1 line each file gained;
  every shifted anchor verified against the code, not just for non-blankness.

audit-arch.md
- design.md §6.1 module matrix omitted the `debug` module entirely; added.
- Seven numeric duplications in design.md now defer to reference.md §6.1 as
  the single authority instead of restating the values.
- reference.md §5.1 was missing DEBUG_TOKEN; §5.3 listed six recognised names
  and guessed `JMAP_*`. It now lists all 20 exactly as config.rs reads them.
- Corrected two wrong claims in the audit itself: debug.rs is compiled
  unconditionally, not under #[cfg(debug_assertions)] (there are no cargo
  features at all), and /debug/* exposes 7 routes, not the invented session
  list / resend / rebuild surface.

audit-fileplanner.md
- reference.md cited a nonexistent `jmap_service.rs`; replaced with the real
  adapter chain worker.rs -> domain/jmap.rs -> domain/jmap/client.rs.
- cloudflare-worker/README.md was outside every validator's scope; all four
  doc validators now include it (208 section refs, 250 line refs).
- README.zh-CN.md stated only half of what README.md says about RUN_MODE; the
  forward-placeholder sentence is now bilingual.
- charter.md §7 boundary table was missing rows for itself and for web/.

audit-rust.md / audit-frontend.md
- Error envelope corrected: `error` is a bare machine code, no `message`.
- Status table, AI-consent labels and ~15 stale or phantom facts fixed against
  notify.rs; `TelegramClient::SendMessage` and `web_config` phantoms removed.
- GET|PUT /api/enabled forwarded by the worker so the SPA kill switch works.
- Ten UI strings that called the admin credential the Redis ACL password now
  say CONFIG_ENCRYPTION_KEY.
- POST /healthz returns 405; dead HTTP_STATUS removed.
- audit_anchors.py now flags identifiers that do not exist in the file they
  are anchored to.

charter.md
- `SAF-ENABLE-FLAG` registered (85 IDs); `SAF-ADMIN-SESSION` row corrected;
- `REQ-PUSH-TYPES` registered; the stale `admin_session_digest` removed.

Gates: GATE-DOCS PASS 5/5 (250 line-refs, 208 section refs, 0 errors);
GATE-P0 62 passed / 0 failed / 1 ignored, baseline unchanged; worker and SPA
36 tests, 0 failures.

Real Stalwart / Telegram / managed Redis verification stays blocked on missing
credentials and remains labelled as unverified.
2026-09-29 01:16:11 +08:00
anonsaber e0882c7c68 docs: de-projectize AGENTS.md into language-agnostic norms; add docs/charter.md
AGENTS.md (276 -> 239 lines) is now a language-agnostic engineering-norms
document: code style, config and secrets handling, build environment,
testing gates, document governance. It states no project fact and names no
language, framework, dependency, or file. Verified: zero hits for JMAP,
Telegram, Redis, axum, Cloudflare, LLM, *.rs, and every project env var.

All project-specific content moved to the new docs/charter.md (~276 lines):
project goals, locked technology choices, 23 numbered security invariants,
implementation stages, prohibitions, test/acceptance gates with the full
GATE-P0 command and baseline, document boundary rules, and the stable ID
registry.

docs/charter.md §8 is the single authority index for stable IDs: 83 rows,
no duplicates, ID | definition file | one-line statement | category. Four
IDs that were referenced across the docs but never registered are now
registered: ARCH-STATE-REDIS, C-REDIS-EXTERNAL, GATE-UPTIME-KUMA, GATE-DOCS.
All previously registered IDs are preserved.

Defect fixed while rebuilding the registry: SAF-ADMIN-SESSION cited
src/config.rs `admin_session_digest`, a name that has 0 hits in src/ -- a
phantom identifier that survived in the authoritative registry since before
this restructure. Corrected to `session_digest` (src/config.rs:7). All 8
distinct backticked symbols in the registry's definition-file column now
verify against src/.

Two false project facts removed from the docs (both claimed the SPA writes
through POST /api/bootstrap, which web/config.js never calls -- 0 hits; the
actual path is PUT /api/business-config): README.md, README.zh-CN.md,
docs/reference.md. The first successful write creates the configuration and
later writes hot-reload it; POST /api/bootstrap remains the one-shot
automation endpoint.

Cross-reference redirections in 9 files: README.md, README.zh-CN.md,
docs/deployment.md, docs/design.md, docs/reference.md,
cloudflare-worker/README.md, cloudflare-worker/src/index.js,
cloudflare-worker/wrangler.toml now point project facts at charter.md and
language-agnostic quality rules at AGENTS.md.

Four validators extended to cover docs/charter.md: DOCS lists in
audit_anchors.py, check_tables.py, validate_docs.py, check_sec_refs.py, plus
charter.md added to check_sec_refs.py's TARGET_BEFORE regex and to
validate_docs.py's index-file cross-doc exemption.

Gates: GATE-DOCS PASS 4/4 (VALIDATION 0 errors, TABLE STRUCTURE 0
mismatches, ANCHOR AUDIT 227 line-refs / 0 errors, SECTION-REF AUDIT 191
refs / 0 errors). GATE-P0 PASS, baseline unchanged 62 passed / 0 failed /
1 ignored. Anchor audit proves line reachability only, not that the line
contains what the prose claims.
2026-09-28 23:39:26 +08:00
anonsaber 82794c95cf worker: /reconcile 走 per-route 长超时且绝不故障转移(01a0e595)
POST /reconcile 是后端同步长任务:后端持集群级单飞锁 lock:reconcile(初租 300 s,
期间每 30 s 心跳续租至 90 s,notify.rs:159/172),但网关原先只有全局超时 10 s、
最多 2 次尝试。一次完整对账很容易撑过网关窗口被误判失败,随后故障转移到第二实例,
而第二实例必然立刻返回 409;客户端既拿不到 204 也拿不到 409,只能看到
"all backends failed"。

改动:
- handleFetch 为 /reconcile 单独覆盖超时与尝试次数:超时取 LB_RECONCILE_TIMEOUT_MS
 (默认 320000 ms,大于锁初租 300 s + 心跳余量),maxAttempts=1 绝不故障转移
  (切实例只会撞锁,没有任何收益)
- 其余快路径完全不受影响,仍用全局 LB_REQUEST_TIMEOUT_MS / LB_MAX_ATTEMPTS
- 新增测试:记录 setTimeout 收到的延迟来断言 /reconcile 走 320000 ms,并用 503 响应
  断言它只尝试 1 个 origin;同时断言 /webhook/tg 仍用 5000 ms 且仍故障转移到第二
  实例;并断言 env 覆盖生效而 maxAttempts 恒为 1
- worker README 补 LB_RECONCILE_TIMEOUT_MS 说明与 /reconcile 例外
- deployment.md §6.3.1 删掉「请把 LB_REQUEST_TIMEOUT_MS 调到大于锁租期」的旧 workaround
  建议(现已由 per-route 覆盖解决),§10 worker 故障转移条目补例外,secret 清单补新变量
- 顺手清掉 health.js 注释里已退役的 GATE-READY-DEPS,改为 ARCH-READY-BASELINE

验证:cloudflare-worker node --test 29/29 通过(原 28 + 新增 1);
web config.test.mjs 6/6 通过。无 src/ 改动,Rust 门禁不受影响。
2026-09-28 12:18:04 +08:00
anonsaber a39ebcf9d1 feat(worker): safelist 纳入 push/register 与 push/disable
网关白名单此前无条件 fail-closed,后端注册的 18 条路由中只有
POST /api/push/register 与 POST /api/push/disable 两条需要业务流量却未
被转发,导致 PWA 订阅/退订必须绕过网关直连后端。

- SAFE_ROUTES 12 → 14 条(backends.js:9-24)
- ROUTE_METHODS 同步补两条 POST(index.js:40-55)
- 头注释路由清单同步
- backends.test.js 转发集合断言补齐两条
- index.test.js 新增转发端到端用例(含鉴权头透传路径)

单入口边界:两条路由仍经 SAFE_ROUTES 无条件校验,未放宽白名单语义。
2026-09-28 10:02:18 +08:00
anonsaber 4f5eb9293d chore(worker): rename proxy to lb and ignore build cache
- cloudflare-worker: unify terminology proxy -> lb/forward
  (PROXIED_ROUTES -> LB_ROUTES, proxyWithFailover -> forwardWithFailover,
  proxy.js -> lb.js, related tests and README); renames/wording only, no behavior change
- Dockerfile: crates.io index mirror rsproxy.cn -> mirrors.ustc.edu.cn
- .gitignore: ignore build verification cache .gate-cache/ (cargo registry + rustup, ~1.5G)
2026-09-27 12:52:55 +08:00
anonsaber c75858fda0 refactor: rename project to MessageWeave 2026-09-25 10:29:18 +08:00
anonsaber 9064fc5f1d fix: hide admin authorization in setup mode 2026-09-25 10:05:47 +08:00
anonsaber dceecaf2f4 feat: initial MessageWeave release 2026-09-25 09:14:04 +08:00