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.
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.
Code (src/config.rs 521->394, src/main.rs 232->200):
- Config::from_env() + required_secret / required_nonblank / env_bool deleted.
config.rs now performs no std::env read; all business fields come from Redis
business config (BusinessConfigWire -> validate_business_wire -> validate_nonblank).
- RUN_MODE deleted: variable, Config.run_mode field and the main.rs validation
block. The two modes never changed runtime behaviour -- webhook and reconcile
share one router, POST /reconcile is a standalone endpoint.
- jmap_password() accessor deleted; the field name is kept in
BusinessConfig / BusinessConfigWire because it is real data, not a stub.
- Config.port and Config.redis_url deleted: both had zero readers, and port was
hardcoded to 8080 in both constructors while the listener binds straight from
the PORT env var, so the field could silently disagree with the real port.
Config is now 6 fields: telegram / jmap / account_id / llm / auth / worker_token.
- redis_only() and from_business(value) signatures simplified accordingly.
- #[expect(dead_code)] count in src/ is now 0.
Test coverage: the from_env()-driven startup_rejects_blank_auth_secret case is
gone with the path it tested. The non-leak assertion it carried was reattached
to the real validation route (BusinessConfigWire -> TryFrom -> validate_nonblank)
with a stronger third assertion that the rejected value is not echoed either.
Docs: design.md 7.1 re-scoped to the Redis business config table; reference.md
5.1/5.3 rewritten (no legacy env path); deployment.md 5 and the runtime-mode
table rewritten; retired.md gains section 4 registering every deletion;
README x2, .env.example, charter.md updated.
GATE-P0: fmt / check / clippy -D warnings green, 61 passed / 0 failed / 1
ignored (was 62; the deleted case). GATE-DOCS: PASS 5/5, ANCHOR 247 line-refs /
0, SECTION-REF 216 / 0.
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.
Code (2 sites):
- main.rs logged bootstrap_config.port, which is always the hardcoded 8080 in
the two-variable production config; the listener actually binds the port
parsed at main.rs:31-34. Log the bound port.
- That exposed Config.port as genuinely unread (clippy -D warnings), so mark it
with #[expect(dead_code, reason=...)] instead of silently deleting a
documented env-parsing site.
Docs, phantom identifiers (names that do not exist in src/):
- RuntimeConfig x3 -> OutboundConfig via RuntimeConfigProvider (state.rs:34/41)
- LlmClient::noop() x2 -> None in Option<Arc<LlmClient>> (main.rs:125)
- main.rs::validate_env_or_exit x2 -> config.rs:338 read, main.rs:75 validation
- TelegramChannel::send_message -> TelegramClient::send_text (channel.rs:134)
Docs, stale facts:
- LLM table: ENABLED and ALLOW_NET defaulted true,true -> false,false;
API_KEY, BASE_URL, MODE were "required" -> only required when enabled. This
contradicted design.md:385-387, which already had the correct values.
- design.md:560 said RUN_MODE "is actually consumed"; it is only read and
validated and never affects behaviour (main.rs:73-74).
- RUN_MODE operator docs (README, reference, deployment, .env.example) now say
both accepted values produce identical behaviour.
- retired.md: 5 intents -> 6 (/search shipped, worker.rs:517); 4 LLM env vars
-> 6; main.rs tracing events 6 -> 7.
- design.md:494 tracing count 6 -> 7, debug-gate warning added to the list.
- design.md:350 code-gap count 2 -> 1 (multi-instance window done in 6c99ce5).
- design.md:554 marks the 45-paused stage-0 baseline explicitly as historical.
- .env.example said the Redis ACL password was the bootstrap trust root; it is
CONFIG_ENCRYPTION_KEY (main.rs:88-91).
Docs, re-anchored after the main.rs shrink (6 refs shifted by -2).
GATE-P0 62 passed / 0 failed / 1 ignored; GATE-DOCS 4/4 clean
(228 line anchors, 211 section refs, 0 errors).
- GET /ready: ready -> 200 + diagnostic report (unchanged success body);
not ready -> 503 + {"error":"service_unavailable","request_id":id}
+ Retry-After: 30, consistent with the other public entry points
- drop the legacy {"status":"not_ready"} diagnostic block from the 503 body
(backward compatibility not required)
- HTTP 200/503 semantics preserved, so Uptime Kuma HTTP(s) monitors
(status-code based) keep working
- sync docs: README.md, README.zh-CN.md, docs/design.md, docs/deployment.md,
AGENTS.md, docs/todo.md
- README.md is now English; add README.zh-CN.md with cross-links; both versions
cover the same 40 sections and the same key facts
- correct stale docs/design.md 10.5.1 and docs/todo.md to the current code
(XAUTOCLAIM threshold, malformed-entry ACK, error envelope now done)
- replace the temporary HANDOFF.md with docs/todo.md (unfinished work + blockers)
- adjust docs/deployment.md and AGENTS.md accordingly