feat: add EdgeKV driver with Edge Function proxy for EdgeOne Pages - #41
Merged
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 29cf3a3 | Sep 13 2026, 02:43 AM |
PIKACHUIM
force-pushed
the
feat/edgeone-kv-proxy
branch
15 times, most recently
from
September 11, 2026 14:36
fbf1255 to
4784b61
Compare
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | 29cf3a3 | Sep 13 2026, 02:42 AM |
PIKACHUIM
force-pushed
the
feat/edgeone-kv-proxy
branch
3 times, most recently
from
September 11, 2026 15:32
dca5b32 to
007e8a5
Compare
Binding resolution is now hardened across every driver. EdgeOne Node
Functions inject a binding named 'KV' that speaks RESP/Redis over a TCP
socket rather than the Web KV API, so calling put/get on it failed with
'cannot find the collection by name' (stack through processResponses /
Socket / TCP.onStreamRead). Any truthy value at that name was previously
accepted, so the RESP client was used as a KV binding and the proxy branch
was skipped. Resolution now requires the expected interface shape:
- kv.ts get() and (put() or set())
- blob.ts get() and (put() or set())
- d1.ts prepare()
- do.ts idFromName()
All probes also check env and globalThis independently. The previous form
'(env && env[key]) || g[key]' short-circuits when env[key] exists, so a
usable globalThis binding was never reached when env held a trap value.
Property access is wrapped in try/catch because proxy objects and lazily
initialised SDKs can throw from getters; a throwing binding must not crash
probing.
Verified against trap shapes for every driver: RESP client, plain string
(binding name), number, boolean, null, undefined, empty object, get-only,
put-only, array, and throwing getters. Correct shapes are still detected.
Earlier fixes retained in this branch:
1. KV-safe keys. EdgeOne KV restricts keys to [A-Za-z0-9_], but DB_FORMAT=key
used ':' as a separator and embedded raw UUIDs (with '-'), so every write
was rejected with 'Key can only contain letters, numbers, and underscores'.
store/keycodec.ts encodes keys: '_' as separator, '_' escaped to 'xx',
other characters escaped per UTF-8 byte as xHH, code-point iteration so
surrogate pairs (emoji) are not split, empty input mapped to '0'. Used by
format/key.ts and store/kv.ts. No reverse parsing is needed because entity
primary keys always come from the record JSON.
2. Serverless runtimes must not silently fall back to in-memory storage.
autoDetectDriver() throws when nothing is available in a serverless
environment; local and container deployments keep the fallback.
3. Missing or unusable storage returns an actionable error
(NO_STORAGE_MESSAGE) listing what to configure. isServerlessRuntime()
detects the runtime from behaviour only (WebSocketPair, caches.default,
injected request context, EdgeOne bindings).
4. The error surfaces on the init page and the home page: global middleware
rejects non-static, non-navigation requests with 503 and
{ code, message, data: { error: 'STORAGE_CONFIG_ERROR' } }; static assets
and HTML shells pass through; /healthz reports 503 with
mode='unavailable'; getStoreStatus() returns driver='none' plus
configError instead of throwing.
5. Storage backend cache key included only ':', so under
DB_DRIVER=auto a local env (memory) and a serverless env (error) could
share an entry. The key now includes a runtime tag.
6. Legacy path supports the KV HTTP proxy (store/json.ts): proxy branch in
getKvBinding tried before Blob when DB_DRIVER=kv and no usable native
binding, createProxyBinding() over functions/kv-*, so all 16 existing call
sites work unchanged. This fixes 'configured kv but kv not used':
DB_DRIVER only affected store/backend.ts, while the JWT secret, revocation
list and login-failure counters went through store/json.ts.
7. Blob probe no longer caches failure permanently (_blobChecked was set
before the probe, so one early failure disabled storage for the instance
lifetime and produced the contradictory log pair 'No persistence backend
configured' followed by 'using EdgeOne Blob storage').
8. Secrets are generated only during setup and never overwritten
(readPersistedSecret / writePersistedSecret / generateSecret /
ensureEncryptionSecret, existence-gated). getEncryptionKey() reads env
then storage and never generates, so a transient read failure cannot
rotate the key and make existing encrypted data undecryptable.
9. The KV proxy verifies callers: HS256 signature, exp and nbf via Web
Crypto, admin role required, constant-time comparison of X-Internal-Call
against the first 16 chars of the secret.
Local tests: binding shape hardening 68/68, end-to-end regression 32/32 (KV
proxy with strict key validation plus a RESP trap, key legality and
round-trip, secret lifecycle, config enforcement, serverless guard, error
surfacing), build artifact audit 19/19, healthz 4/4. The 6 failures in the
full suite pre-date this branch.
PIKACHUIM
force-pushed
the
feat/edgeone-kv-proxy
branch
from
September 11, 2026 15:41
007e8a5 to
17ec8cf
Compare
…names - Merge ENCRYPTION_SECRET / CRON_SECRET into JWT_SECRET (sign + encrypt + cron) - Rename ADMIN_PASSWORD -> ADMIN_PASS, ALLOWED_ORIGINS -> ALLOW_URLS - Remove DATABASE_JSON, TABLE_PREFIX (fixed to x_), DB_JSON_BACKEND - Collapse KV/DO binding names to fixed KV / DO; drop SCF_FUNCTIONNAME alias - Remove Edge KV proxy secret cache entirely (always read fresh) - Fix MySQL upsert dialect (ON DUPLICATE KEY UPDATE), sshkeys round-trip guard - Sync README/readmes/docs/wrangler.toml/serverless.yml; add optional vars
- Rename >10-char vars: MAX_UPLOAD_SIZE->MAX_UPLOAD, MAX_PART_SIZE->MAX_UPPART, CDN_URL->ASSET_URLS, SEED_SOURCE_ALLOWED_HOSTS->ALLOW_SEED, MYSQL_PASSWORD->MYSQL_PASS, MYSQL_DATABASE->MYSQL_NAME, CF_ACCOUNT_ID->CF_ACCOUNT, CF_KV_NAMESPACE_ID->CF_KV_UUID, CF_API_TOKEN->CF_API_KEY - Remove DATABASE_URL alias; keep MYSQL_URL - Rewrite .env.example to match real variables - Beautify wrangler.toml: uniform separators, concise comments, add kv_namespaces id - Sync README/readmes/docs/PR migration guide
- Remove docs/storage-architecture.md and docs/PR-config-unification.md from git (kept on disk, now gitignored) - Align driver comments and simplify ASSET_URLS example in wrangler.toml
- ensureEncryptionSecret now re-reads the key after writing, retrying with exponential backoff until it is actually visible to other instances. This fixes 'setup succeeds but password auth fails' on EdgeOne/Cloudflare KV, caused by eventual-consistency write propagation delay. - Adopt an existing different key if a concurrent setup wins the race. - Add isEncryptionReady() and expose it via /public/init_status as 'ready'. - Add regression coverage with a delayed KV.
Adds GET /public/env_check (unauthenticated, never echoes secrets) so the initialization page can show, before any account exists: - config: DB_FORMAT / DB_DRIVER as configured, plus the resolved values - storage: availability, memory fallback flag, health, platform - jwt: whether the signing/encryption key is readable from its real source - ready: combined verdict (storage usable AND key ready) - issues: actionable list with code / level / message / docUrl Readiness treats in-memory storage as NOT ready on serverless (writes would vanish) and flags a configured-but-unreachable driver (e.g. KV proxy 401) as STORAGE_UNHEALTHY instead of silently reporting success. Also adds scripts/env-check.mjs to dump the check locally, and covers the endpoint with tests (unready env, healthy env, no secret leakage, and the serverless-with-key-but-no-storage case).
flyfeel
pushed a commit
to flyfeel/openList-worker
that referenced
this pull request
Sep 12, 2026
fix: 勾选记住登录后 token 应持久化到 localStorage
Issue #46 - auto detection could reject usable deployments: - Detect in order mysql -> d1 -> kv -> blob -> do -> cfkv. `do` was missing from the candidate list, so a Cloudflare Worker bound only to Durable Objects reported "no storage available" and refused to initialize. - Only probe mysql when MYSQL_URL / MYSQL_HOST is configured. MySQL needs a network connection; probing it unconditionally added a TCP attempt to every auto-detection on edge runtimes. - In-memory fallback stays forbidden on Workers / EdgeOne / ESA, so data can never silently land in RAM. Issue #34 - one-click deploy could not bind resources: - Replace wrangler.toml with wrangler.jsonc, declaring NO storage bindings. Declaring them forced the deploy button to provision all of them, and a second deploy from the source repo conflicted with the already-created resources (the ids live in the generated repo, not here). - Guide binding from the console instead, with per-option instructions in the file and in scripts/deploy.js. - Add package.json `cloudflare.bindings` descriptions so the deploy form explains JWT_SECRET / DB_DRIVER / DB_FORMAT at the point of entry. - Default vars: DB_FORMAT=map, DB_DRIVER=auto. Also rewrite scripts/test-deploy.js (which still parsed wrangler.toml and would crash) to test the wrangler output parsers, and cover the new detection order in the regression suite.
…ler.jsonc Secret retention: - Add .dev.vars.example (names only, values empty). The Deploy to Cloudflare button reads it (or .env.example) to generate inputs for the required secrets; the values entered there are stored as Worker Secrets. - Align .env.example with the same variable set. - Worker Secrets survive redeploys - they are only removed by an explicit `wrangler secret delete` - so an empty template value never clobbers a configured secret. In contrast, `vars` are overwritten on every deploy, which is exactly what we want for DB_FORMAT / DB_DRIVER defaults. - Ignore real .dev.vars files while keeping the *.example templates tracked. wrangler.jsonc: - Port every variable and comment that used to live in wrangler.toml, all optional ones commented out by default (MAX_UPLOAD, MAX_UPPART, ASSET_URLS, ALLOW_SEED), plus the KV / D1 / DO binding templates. - Document the auto-detection order: mysql -> d1 -> kv -> blob -> do -> cfkv. Verified with `wrangler deploy --dry-run` (wrangler 4.118.0): only ASSETS plus the three vars are bound; no storage bindings are declared.
… default Mirror the full variable set from the retired wrangler.toml so the file stays the single reference for what can be configured: - secrets: JWT_SECRET, ADMIN_PASS, ALLOW_URLS - optional: MAX_UPLOAD, MAX_UPPART, ASSET_URLS, ALLOW_SEED - KV REST API: CF_KV_UUID, CF_ACCOUNT, CF_API_KEY - MySQL: MYSQL_URL, or MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASS / MYSQL_NAME All are commented out except ENVIRONMENT / DB_FORMAT / DB_DRIVER, so they never override anything until the operator opts in. The same names are declared (values empty) in .dev.vars.example / .env.example for the deploy button. Verified with `wrangler deploy --dry-run`: only ASSETS plus the three active vars are bound.
Both are KV-semantic drivers. A local KV binding is more direct and faster than the Cloudflare REST API, so it should win when both are available. New order: mysql -> d1 -> kv -> cfkv -> blob -> do Updates the inline comments, wrangler.jsonc, README and storage-architecture docs, and adds a regression assertion that kv beats cfkv when both are present. Also drops the retired wrangler.toml (replaced by wrangler.jsonc) and rebuilds the EdgeOne bundle.
Addresses findings from the code review of this branch. P1 (major) - internal call used a truncated secret: - The X-Internal-Call header was compared against the first 16 characters of JWT_SECRET, cutting entropy from 256 to 64 bits. Passing that check also bypassed the admin-role enforcement entirely, so a guessed value granted unrestricted KV read/write/delete. - Now the full secret is required on both sides (_kv-proxy.js, json.ts, driver/kv.ts). P2 (major) - kv-list pagination could repeat pages: - If the platform's cursor is not "the last key of this page", the loop kept re-fetching the same page until the 1000-iteration guard. - Adds repeated-page detection (same first key as the previous page stops pagination) and de-duplicates the returned key set. P3 (major) - proxy origin was trusted as-is: - __requestOrigin derives from the request Host, so a forged Host could point the Node side at an attacker server, leaking the (now full) JWT_SECRET via the X-Internal-Call header and allowing forged KV responses. - Adds sanitizeProxyOrigin(): only http/https, must parse with a hostname, and request-derived origins must be HTTPS unless localhost. Used by both json.ts and driver/kv.ts. Minor: - env_check now redacts error text (first line, credentials masked, truncated) since the endpoint is unauthenticated. - driver/kv.ts get() distinguishes signature errors (TypeError) from real failures so network/auth errors are no longer swallowed as "key not found". - format/sql.ts save() switches from "DELETE all + INSERT all" to "UPSERT each row + DELETE ... NOT IN (keys)", removing the empty-table window and avoiding clobbering concurrent writes from the Go backend. - package.json gains test:store / test:regress / test:deploy / env:check. - Documents the concurrency limits of map/key and the constraints of sharing the sql database with the Go backend. Regression suite grows to 71 assertions, including the truncated-secret rejection and origin-validation matrix.
The code-review report slipped into the previous commit because .gitignore only listed storage-architecture.md and PR-config-unification.md by name. Broaden the rule to docs/code-review-*.md so all local notes follow the same untracked policy; files stay on disk.
The env_check issues were too verbose to read in the setup UI: the JWT one ran to four lines of explanation, and the storage one dumped NO_STORAGE_MESSAGE (an eleven line, terminal oriented configuration guide) straight into the page. Reduce each to a single actionable sentence and let the docUrl link carry the detail. The storage config error no longer echoes the raw backend message, which also keeps internal DSNs and hostnames off this unauthenticated endpoint.
This was
linked to
issues
Sep 12, 2026
Closed
…e checks
Follow-up to the code review of this branch. Removes duplicated proxy
implementations, tightens variable naming and makes the storage readiness
checks share a single source of truth.
KV proxy de-duplication (was two independent implementations):
- store/json.ts::createProxyBinding re-implemented the exact same HTTP
proxy protocol as store/driver/kv.ts (same /kv-get|kv-put|kv-delete|kv-list
endpoints, same X-Internal-Call header). Both paths are live at runtime:
business data goes through kvDriver, while the JWT secret, revoked-token
blacklist and login-failure counters go through json.ts. Any drift between
them would silently make the two subsystems disagree.
- createProxyBinding now delegates to kvDriver and only adapts the
Driver string[] list() contract to the binding {name,key}[] shape.
- sanitizeProxyOrigin moves to a new store/proxy.ts so neither module has to
import the other; json.ts still re-exports it for existing callers.
Variable naming:
- MYSQL_URL -> MYSQL_URLS (the code read MYSQL_URLS in hasMysqlConfig but
mysql.ts read MYSQL_URL, so following the error message produced a
deployment that reported "configured" yet could not connect).
- EDGE_KV_BASE_URL -> EO_KV_URLS, and it is now declared in both example
templates plus wrangler.jsonc (it was previously code-only, so users hit
the runtime error before ever seeing the variable name).
Remove dead branches and misleading names:
- isEdgeOneNodeEnv() was `getKvBinding(env) === null` and every call site was
already guarded by `if (kv) return`, so it was always true. Deleted it and
the two now-unreachable branches it guarded (the "KV binding not found"
throw and the mode:"unknown" health result).
- isAvailable() and health() each issued their own __health__ probe with
different verdicts on HTTP 401. Both now share probeProxy(); isAvailable
stays lenient (401 still means the proxy is deployed, only the secret is
wrong) while health stays strict (401 is not usable), preserving the
existing behaviour that /env_check depends on.
Single source of truth for storage readiness:
- Dropped the configErrorCache WeakMap from getStoreConfigError. It was
redundant: getStorageBackend already caches driver resolution per env
fingerprint, and checkProxyConfig is a pure synchronous env read. It also
never worked as documented, since a Worker gets a fresh env object per
request, so the single-reference fast path always missed.
- isPersistentStorageAvailable and getStoreConfigError now share
isPersistentStatus(), so /env_check, init_status and the 503 middleware
can no longer reach contradictory conclusions.
- getStoreConfigError keeps reporting the actionable "JWT_SECRET is missing"
hint first when DB_DRIVER=kv is explicit.
Other:
- keycodec: KEY_SEP is no longer exported (internal only); encodeKeyPart and
decodeKeyPart stay exported because the regression suite uses them to
assert round-trip encoding.
- db.ts: corrected the comment claiming signature and field encryption share
one key. They share the JWT_SECRET env var when set, but fall back to two
separate KV slots, so without an explicit secret they are different values.
- .gitignore: ignore .tmp-dryrun/ (dry-run output written by the deploy
constraint test); the existing rule only covered .wrangler-dryrun/.
Verified: tsc --noEmit clean, test:regress 71/71, test:store 9/9,
test:deploy 4/4, healthz 6/6. The three test:server failures (F-11 password
reset, F-11 random password, CAS codec field names) are pre-existing and
reproduce identically on the parent commit.
Follow-up hardening after reviewing the previous refactor. Three findings,
all reproduced before fixing.
GetStoreConfigError could return null for an unavailable backend:
- The function decided availability via isPersistentStatus() but then looked
up the reason in a chain (configError -> memory -> healthError) that could
exhaust without a match. A driver reporting { available: false } without an
error string fell through to the final `return null`, so the 503 middleware
let the request proceed against a backend that was known to be unusable.
- Added a terminal fallback derived from the driver name, so every path that
isPersistentStatus() rejects also produces an error string. Verified that
getStoreConfigError and isPersistentStorageAvailable now agree in all
probed cases (empty env, serverless without storage, explicit kv without a
secret, explicit d1 without a binding).
HTTP 401 from the KV proxy surfaced as an unactionable error:
- isAvailable() treats 401 as "proxy deployed" so the kv driver gets selected
during auto-detection, while health() treats it as unusable. The combination
is intended (selectable vs. actually usable), but the resulting message was
the raw "HTTP 401" even though the real cause is a JWT_SECRET mismatch with
the Edge Functions serving the proxy.
- getStoreConfigError now also inspects the resolved driver, not just an
explicit DB_DRIVER=kv. When the kv driver is selected in proxy mode and the
probe comes back 401, it explains that the deployment's JWT_SECRET does not
match the proxy's and suggests checking EO_KV_URLS, which is what an
operator actually needs to fix.
kvDriver.get() could return undefined instead of null:
- The proxy branch trusted { value } from the Edge Function, but kv-get returns
{ error } with HTTP 500 on failure, leaving data.value undefined. The Driver
contract is string | null, and callers that test `=== null` would have missed
it. Now normalised explicitly.
Also documented why sanitizeProxyOrigin does not block private/metadata IPs:
string-based checks cannot stop DNS rebinding (which needs resolution the edge
runtime cannot reliably do), would not protect against an attacker who already
controls Host, and would break legitimate internal deployments that point
EO_KV_URLS at a private origin.
Verified: tsc --noEmit clean, test:regress 71/71, test:store 9/9, healthz 6/6.
jyxjjj
previously approved these changes
Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EdgeOne KV 代理 + 存储驱动加固 + 配置精简
一、摘要
一句话概括
让 OpenList-TSWorker 在 EdgeOne Node 云函数上可用(此前完全不可用),同时修正存储驱动的若干正确性缺陷、精简并统一配置变量。
修了什么
sshkeys数据丢失、密钥就绪竞态、CF 绑定声明JWT_SECRET/public/env_check环境自检 + 前端三步初始化向导不修什么(刻意保留)
map格式(宽表)保留 —— 删的是「宽表塞进 SQL 表」的旧实现,不是宽表本身memory驱动保留 —— 仅本地/容器允许,serverless 一律拒绝JWT_SECRET的加密共存 —— 未拆分为独立加密密钥(见「设计取舍」)二、背景
2.1 平台架构边界(本 PR 的起点)
EdgeOne 有两种运行时,KV 绑定只注入其中一种:
cloud-functions/[[default]].jsnode:internal/process/task_queues因此在 EdgeOne Node 云函数上:
context.env.KV永远拿不到 → 报KV binding not found2.2 相关 Issue
2.3 部署机制限制
Cloudflare 官方文档确认:
根因(#34 用户反馈「二次部署找不到已创建的 KV」):首次部署时 Cloudflare 把生成的 id 回填到了**「新建的 Git 仓库」**,而非源仓库。二次从源仓库部署时 id 仍为空 → 冲突。
三、修改方案
3.1 KV HTTP 代理(新能力)
方案:Node 云函数经 HTTP 调用 Edge Function 代理来访问 KV。
触发条件:显式
DB_DRIVER=kv且无原生 binding 且存在请求来源。三层鉴权(
_kv-proxy.jsauthorize()):X-Internal-Call常量时间比对密钥前 16 位exp+nbf+ admin 角色)强制
HS256,拒绝alg=none降级攻击。密钥不缓存:KV 密钥可能被 Node 侧随时写入/轮换,任何模块级缓存(哪怕带 TTL)都会让 Edge 实例持有旧值导致鉴权失败 → 每次直读
env.JWT_SECRET→ 回退 KV。前置条件:
checkProxyConfig()在代理模式强制要求JWT_SECRET(≥16 字符),缺失直接 503,避免「静默不可用」。3.2 Serverless 禁止内存存储
双层防护:
autoDetectDriver()resolveDriver()memory驱动isServerlessRuntime()仅依据运行时特征(不依赖用户配置):EDGEONE_BLOB、EdgeOne全局、TENCENTCLOUD_SCF_FUNCTIONNAMEWebSocketPair、caches.defaultESA_BLOB、ESA全局3.3 检测顺序修正
blob → cfkv → kv → d1(缺do和mysql)mysql → d1 → kv → cfkv → blob → do设计要点:
mysql仅在显式配置MYSQL_URL/MYSQL_HOST时参与探测 —— 网络连接,无条件尝试会给每个请求增加 TCP 建连开销kv优先于cfkv—— 本地 binding 比 REST API 更直接修复的真实缺陷:CF Worker 仅绑 DO 时报「no storage available」拒绝初始化(
doDriver不在候选列表),尽管 DO 完全可用。3.4 绑定探测加固
问题:EdgeOne Node 云函数注入名为
KV的绑定,但它走 RESP/Redis 协议(TCP socket),非 Web KV API。原代码接受任何真值 → 调用put/get抛cannot find the collection by name。方案:探测必须校验接口形态
kv/blobget()且(put()或set())d1prepare()doidFromName()同时修复:
(env && env[key]) || g[key]形式在env[key]存在时短路 →globalThis上可用的绑定永远探测不到。现改为 env 与globalThis独立检查。所有属性访问包
try/catch(抛异常的 getter 不得让探测崩溃)。3.5 KV-safe 键名
问题:EdgeOne KV 限制键名只能含
[A-Za-z0-9_]。DB_FORMAT=key原先用:分隔并内嵌 UUID(含-)→ 每次写入被拒。方案:新增
keycodec.ts_(本身合法,无需转义)xHH0(避免users_与表前缀混淆)无需反向解析:主键值取自记录 JSON,键名只作存储地址。
3.6 列式表重构 + 删除宽表死代码
背景:旧版
store/d1.ts、store/mysql.ts、store/kv.ts是「SQL 表壳 +dataJSON 列」的宽表实现 —— Go 后端读不了,与「共享物理库」目标冲突。方案:删除这三个旧实现,统一走新的
driver/+format/架构;schema.ts采用列式表(每字段独立成列),表名对齐 Go 的 GORM:settingsx_setting_itemsstoragesx_storagesusersx_userssharesx_sharing_dbsmetasx_metaspluginsx_plugins(TS 独有)3.7
sshkeys表数据丢失修复问题:TS 侧 SSH 公钥存在
user.ssh_keys,从不写顶层db.sshkeys;Go 后端存在独立的x_ssh_public_keys。若sshkeys参与往返,sqlFormat.save()会DELETE FROM x_ssh_public_keys再写空数组 → 清空 Go 的公钥。方案:拆为两个列表
TABLE_NAMESload/save往返DDL_TABLE_NAMESsshkeys),保证与 Go 共享库结构一致3.8 MySQL 方言修复
问题:
format/sql.ts用 SQLite 专属的INSERT OR REPLACE,MySQL 上必报语法错误。方案:新增
upsertSql()按driver.name分支INSERT OR REPLACEON DUPLICATE KEY UPDATE3.9 加密密钥就绪竞态(修复「初始化成功但密码认证失败」)
故障链路(KV 最终一致性):
方案:
ensureEncryptionSecret写入后主动回读确认,读不到则指数退避重试(最多 6 次,总等待约 1.9s);读到并发的不同密钥时采纳它,保持加解密对称。配套:
getEncryptionKey()只读不生成(瞬时失败不轮换密钥)isEncryptionReady()(绕过缓存直查真实来源),经/public/init_status暴露ready3.10 一键部署与 Secret 保留
方案:
wrangler.toml→wrangler.jsonc,刻意不声明任何存储绑定。{ "vars": { "ENVIRONMENT": "production", "DB_FORMAT": "map", "DB_DRIVER": "auto" } // ▶ KV / D1 / DO / Blob+MySQL 全部以注释形式提供模板 }选用
.jsonc原因:Wrangler v3.91.0+ 支持、可写注释、官方文档示例均用 jsonc。Secret 声明机制(官方确认):
.dev.vars.example/.env.example,识别需用户填写的 Secretwrangler deploy覆盖或删除,仅wrangler secret delete才移除与
vars的分工:JWT_SECRET等敏感项.dev.vars.exampleDB_FORMAT/DB_DRIVERwrangler.jsonc的vars3.11 环境自检接口
新增 免鉴权 的
GET /public/env_check(初始化页未登录就需要),绝不回显密钥或 DSN 原文。就绪判定规则:
storage.availablehasDriver && !isMemory && !hasConfigError && driverHealthyjwt.readyisEncryptionReady()(绕过缓存直查真实来源)ready两个易漏边界(已修复):memory 不算可用;驱动「配置齐全但不可达」(如 KV 代理 401)报
STORAGE_UNHEALTHY。3.12 前端三步初始化向导
严格完成判定:仅当
ready=true才进入完成态;超时显示「仍在同步」+ 重试,不谎报成功。Go 兼容:面板仅对 TS Worker 渲染;Go 侧跳过就绪等待(强一致存储,且无
ready字段)。四、改动文件
4.1 新增(9)
functions/_kv-proxy.jsfunctions/kv-get/index.jsfunctions/kv-put/index.jsfunctions/kv-delete/index.jsfunctions/kv-list/index.jssrc/backend/internal/model/store/keycodec.tswrangler.jsonc.dev.vars.examplescripts/env-check.mjsscripts/_regress.mjs4.2 删除(5)
wrangler.tomlwrangler.jsonc替代src/backend/internal/model/store/d1.tssrc/backend/internal/model/store/mysql.tssrc/backend/internal/model/store/kv.tsscripts/d1-schema.sqlschema.ts已脱节的过时脚本4.3 主要修改(按模块)
存储核心
store/backend.tsstore/schema.tsTABLE_NAMES/DDL_TABLE_NAMES拆分、固定x_前缀store/types.tsEnvContext;删StoreDriver、TABLE_EXTRA_COLUMNSstore/json.tsDB_JSON_BACKENDstore/format/sql.tsstore/format/key.tsstore/driver/kv.tsget()、绑定名收敛store/driver/{blob,d1,do,mysql,cfkv}.ts服务层
internal/model/db.tsisEncryptionReady()、删DATABASE_JSONserver/public.ts/public/env_check、init_status.readyserver/middlewares.tsCRON_SECRET合并入JWT_SECRETserver/router.tsALLOWED_ORIGINS→ALLOW_URLSserver/auth.tsADMIN_PASSWORD→ADMIN_PASSserver/{assets,fs,seed,task}.tsindex.tsSTORAGE_CONFIG_ERROR测试(5)
store.test.ts、storage.test.ts、healthz.test.ts、crypto_security.test.ts、default_credentials.test.ts脚本/配置/文档(14 + 12 readmes)
scripts/{deploy,test-deploy,security-check}.js、package.json、.env.example、.gitignore、serverless.yml、esa-entry.ts、middleware.js、cloud-functions/[[default]].js、README.md+ 12 个readmes/*.md五、配置变化
5.1 删除的变量(5)
ENCRYPTION_SECRETJWT_SECRETCRON_SECRETJWT_SECRETDATABASE_JSONTABLE_PREFIXx_DB_JSON_BACKENDDB_DRIVERDATABASE_URLMYSQL_URL5.2 重命名的变量(11)
ADMIN_PASSWORDADMIN_PASSALLOWED_ORIGINSALLOW_URLSMAX_UPLOAD_SIZEMAX_UPLOADMAX_PART_SIZEMAX_UPPARTCDN_URLASSET_URLSSEED_SOURCE_ALLOWED_HOSTSALLOW_SEEDMYSQL_PASSWORDMYSQL_PASSMYSQL_DATABASEMYSQL_NAMECF_ACCOUNT_IDCF_ACCOUNTCF_KV_NAMESPACE_IDCF_KV_UUIDCF_API_TOKENCF_API_KEY5.3 绑定名收敛
EDGEONE_KV/EO_KV/KV/CF_KV/DATABASE_KV/EDGEONE_KV_NAME/KV_NAMESPACE/KV_NAMEKVDO_BINDING/DODOSCF_FUNCTIONNAME/TENCENTCLOUD_SCF_FUNCTIONNAMETENCENTCLOUD_SCF_FUNCTIONNAME5.4 最终变量清单
JWT_SECRETDB_DRIVERautoDB_FORMATmapADMIN_PASSALLOW_URLSMAX_UPLOAD26214400MAX_UPPART16777216ASSET_URLSALLOW_SEEDMYSQL_URL/MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASS/MYSQL_NAMEmysql驱动时CF_ACCOUNT/CF_KV_UUID/CF_API_KEYcfkv驱动时5.5 存储格式与驱动对照
map(默认)openlist_configkeyusers_1sqlx_users驱动检测顺序:
mysql → d1 → kv → cfkv → blob → do六、迁移方法
6.1 【必做】密钥变量合并
若你设置了
ENCRYPTION_SECRET:JWT_SECRETENCRYPTION_SECRETJWT_SECRET改为原ENCRYPTION_SECRET的值,删除ENCRYPTION_SECRET6.2 【必做】删除废弃变量
CRON_SECRET?cron_secret=<JWT_SECRET>DATABASE_JSONTABLE_PREFIXx_(非x_前缀需迁移表名,见 6.5)DB_JSON_BACKENDDB_DRIVERDATABASE_URLMYSQL_URL6.3 【必做】变量重命名
见 §5.2 的 11 项对照表,逐项替换。
6.4 【按需】KV / DO 绑定名
绑定名固定为
KV与DO。若用了自定义绑定名,请改为KV/DO。6.5 【按需】表前缀迁移(仅
DB_FORMAT=sql且自定义过前缀)6.6 【按需】EdgeOne Node 云函数 + KV 用户
JWT_SECRET(≥16 字符)—— 代理鉴权依赖它DB_DRIVER=blob(EdgeOne Blob 零配置)6.7 【按需】从
wrangler.toml迁到wrangler.jsoncwrangler.jsonc替换wrangler.toml,并不再声明任何存储绑定wrangler.toml中声明了[[kv_namespaces]]等:wrangler.jsonc(或继续用wrangler.toml,二者不冲突)JWT_SECRET等已配置的 Secret 会保留6.8 迁移速查表
JWT_SECRETENCRYPTION_SECRETJWT_SECRET改为原值,删除ENCRYPTION_SECRETCRON_SECRET?cron_secret=<JWT_SECRET>ADMIN_PASSWORDADMIN_PASSALLOWED_ORIGINSALLOW_URLSTABLE_PREFIXx_需迁移表名DATABASE_JSONDB_JSON_BACKENDDB_DRIVERMYSQL_DSN/SQL_DSN/DATABASE_URLMYSQL_URLKV/DOwrangler.toml声明绑定JWT_SECRET,KV 绑到边缘函数6.9 【验证】首次部署清单
Secret 保留机制依赖 Cloudflare Deploy 按钮行为,需一次真实部署确认:
.dev.vars.example的 7 个输入项JWT_SECRET,执行部署JWT_SECRET显示为 Secret 类型(值不可见)JWT_SECRET仍存在且未被清空第 4 步是核心验证点。
七、设计取舍
JWT_SECRET兼作加密密钥。拆分需数据迁移,且用户须管理两个密钥;先用合并方案,风险见 §6.1map格式memory驱动mysql需显式配置才探测kv优先于cfkvwrangler.jsonc而非.json八、验证结果
回归套件覆盖重点
do;仅绑 D1 →d1;KV+D1 →d1;KV+CF_REST →kv;无 MYSQL 配置不选 mysql;serverless 无存储抛错isEncryptionReadyenv_check响应不含密钥原文已知预存问题:
default_credentials.test.ts有 2 个用例失败,经git stash验证在本分支改动前即已失败(getDb按 env 对象缓存导致的测试隔离问题),与本 PR 无关。九、提交清单
17ec8cf13094df3365fc739ad759d8849415750563cbe59e0b5af83c8774c7ed35d7e0bdb378d十、关联 PR
OpenList-Workerfeat/edgeone-kv-proxyOpenList-Frontendfeat/init-storage-progressOpenList-Docsdocs/worker-env-rename