Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
149ed72
ci: 新增 verify 工作流、统一 Node 版本,并修掉 OSV 报告的依赖漏洞
cakkl Sep 12, 2026
d29c7bf
fix(security): 显式隔离 JWT 用途,并放行解密 worker 的 CSP
cakkl Sep 12, 2026
2ce81e6
fix(backend): 把备份租约的读改写放进同一事务
cakkl Sep 12, 2026
a1e8fb6
fix(backend): 补上 NAT64 的 SSRF 绕过,并确认写入确实生效
cakkl Sep 12, 2026
9c2d730
fix(backend): 补齐 migrations 的 schema 漂移,并为兜底提权记录审计事件
cakkl Sep 12, 2026
c0381a9
refactor: 移除私有的批量删除文件夹端点
cakkl Sep 12, 2026
beeeccf
fix(backup): 「尚未配置」的备份目标不再被当成错误
cakkl Sep 12, 2026
1947b4c
fix(webapp): 修复暗色对比度、无障碍名称与布局溢出
cakkl Sep 12, 2026
c2a8dfb
docs(security): 补充管理员引导与角色分配说明
cakkl Sep 12, 2026
659bbfc
fix(backend): 导入先校验后写入,避免失败时留下半截数据
cakkl Sep 12, 2026
f341e6d
perf(backend): 新增清理索引并消除全表扫描与串行 IO
cakkl Sep 12, 2026
74b547b
fix(backup): 修正本地导出的附件内联与体积预算
cakkl Sep 12, 2026
701b170
fix(webapp): 修正网站地址栏主机的裁剪
cakkl Sep 12, 2026
8efe2a6
test: 引入跑真实 SQL 的测试基础设施与 200 项用例
cakkl Sep 12, 2026
3eae45a
docs: 补上测试相关的贡献指南与 PR 检查项
cakkl Sep 12, 2026
ceed600
chore(ci): 加 gitleaks 配置,排除测试夹具中的假凭据误报
cakkl Sep 12, 2026
3081ce7
test: 消除两处 semgrep 误报(改代码,而非加抑制注释)
cakkl Sep 13, 2026
e495ef7
Merge pull request #1 from cakkl/Developer
cakkl Sep 13, 2026
fb293fc
chore(ci): 假凭据按值加入 gitleaks 白名单,并给 Dependabot 加 cooldown
cakkl Sep 13, 2026
3defc3c
feat(logs): 新版本启动时在日志中心记录一条版本事件
cakkl Sep 13, 2026
4891322
fix(demo): 演示站日志中心与真实程序对齐(动作名/分类/级别/元数据)
cakkl Sep 13, 2026
8341a49
fix(backup): 导出侧补 db.json 体积预检,恢复侧去掉重复拷贝并按批写入
cakkl Sep 13, 2026
530352c
chore(security): 清理 CodeQL/Semgrep 告警:尾部量词正则改循环、删死代码、补落盘断言
cakkl Sep 13, 2026
82ff017
chore(deps): 依赖与 Cloudflare 版本升到最新(档 1)
cakkl Sep 13, 2026
8c9b299
Merge pull request #2 from cakkl/Developer
cakkl Sep 13, 2026
4a58bbf
Merge pull request #3 from cakkl/chore/deps-upgrade
cakkl Sep 13, 2026
a98a657
fix(logs): 日志中心分页显示真实总数,不再每页 +limit
cakkl Sep 13, 2026
ad12c19
Merge pull request #4 from cakkl/fix/log-center-pagination
cakkl Sep 13, 2026
a1ae136
fix(a11y): 键盘焦点不可见、4 处对比度不足、弹窗按钮尺寸声明从未生效
cakkl Sep 13, 2026
19f0949
Merge pull request #5 from cakkl/fix/a11y-and-dialog-metrics
cakkl Sep 13, 2026
bb22a81
chore(deps): @simplewebauthn/server 13.3.3 → 14.0.2(评估型升级)
cakkl Sep 13, 2026
fcb384e
Merge pull request #6 from cakkl/chore/simplewebauthn-14
cakkl Sep 13, 2026
1608b1f
i18n: 新增 TOTP 验证按钮与密钥不可用提示文案、cipher.key 错误映射,恢复确认框补 2FA 回退警告
cakkl Sep 15, 2026
8816df9
fix(totp): 区分「已启用」与「可用」,非法密钥在启用时即被拒绝、登录时保持 fail-closed
cakkl Sep 15, 2026
ec3b03d
fix(webapp): 设置页显示服务端真实 TOTP 密钥,并修正弹窗的验证、启用与停用路径
cakkl Sep 15, 2026
dfbe82a
fix(backup): 恢复进度上报改为尽力而为,不再「恢复成功却报 500」或覆盖原始失败原因
cakkl Sep 15, 2026
d5f25d7
fix(compat): 不再向客户端宣称支持邮箱验证;cipher.key 报文修正归因与建议
cakkl Sep 15, 2026
1a5cac7
chore(deps): allowScripts 白名单跟随已安装的 esbuild/workerd 版本
cakkl Sep 15, 2026
4acc109
fix(audit): 审计日志记录操作者邮箱行内快照,恢复/删用户后不再永久丢失
cakkl Sep 15, 2026
05c8d08
fix(backup): 进度上报统一改为尽力而为,导出/远端路径不再可能被上报失败拖垮
cakkl Sep 15, 2026
74ff70d
Merge pull request #7 from cakkl/fix/totp-restore-and-audit-hardening
cakkl Sep 15, 2026
2f0f27e
chore(webapp): 删除 3 处未使用的类型导入(死声明)
cakkl Sep 15, 2026
56b4001
chore(tsconfig): 打开 noUnusedLocals,把死声明交给编译器而不是 CodeQL
cakkl Sep 15, 2026
32dedbf
Merge pull request #8 from cakkl/chore/dead-declarations
cakkl Sep 15, 2026
676ffd7
fix(backup): 远端请求补分档超时,目的地不可达时给出可读错误
cakkl Sep 17, 2026
354a0a7
fix(yubikey): Yubico 两处外发请求补超时,并抽出共用的超时封装
cakkl Sep 17, 2026
d025cd2
fix(scripts): 加固 ensure-kv.cjs —— 不再猜命名空间、写回后校验、可测
cakkl Sep 18, 2026
3c8dd77
fix(webapp): 补暗色下 .input-icon-btn 的禁用态颜色(被 dark.css 基色规则反压)
cakkl Sep 18, 2026
10732ba
test(security): 给「错误原文回给客户端」加源码护栏(docs/TODO 第 4 条)
cakkl Sep 18, 2026
07cc114
fix(admin): 修掉「系统可能没有任何可用管理员」的三处漏洞(docs/TODO 第 5 条)
cakkl Sep 18, 2026
8f0abe6
fix(backup): 备份失败原因不再被重试循环吃掉(docs/TODO 第 18 条)
cakkl Sep 18, 2026
6451573
fix(backup): 计划任务因租约被跳过时留下痕迹(docs/TODO 第 19 条)
cakkl Sep 18, 2026
a58633a
fix(webapp): 管理端 4 个邀请码端点不再吞掉服务端文案(docs/TODO 第 20 条)
cakkl Sep 18, 2026
ce79f87
fix(webapp): 前端 API 不再吞掉服务端文案(41 处,docs/TODO 第 21 条)
cakkl Sep 18, 2026
787130a
feat(webapp): 备份中心展示「上次失败」(docs/TODO 第 22 条)—— 后端一直在下发 runtime(redact…
cakkl Sep 18, 2026
5561660
fix(demo): 演示数据补一个「上次成功过、之后一直失败」的目标
cakkl Sep 18, 2026
c87baa0
fix(webapp): 备份地点详情只保留「上次失败」,并修好它与下一块的间距
cakkl Sep 18, 2026
7d9a5eb
fix(webapp): 备份列表/详情只在「最后一次尝试是失败的」时才显示上次失败
cakkl Sep 18, 2026
83e5280
refactor(backup): 远端超时消息的形状改为单一来源
cakkl Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@

- [ ] I read `CONTRIBUTING.md`.
- [ ] Schema changes, if any, updated both runtime schema and `migrations/0001_init.sql`.
- [ ] Schema changes, if any, bumped `STORAGE_SCHEMA_VERSION` in `src/services/storage.ts`.
- [ ] Persistent data changes, if any, updated backup export/import or documented why backup is not needed.
- [ ] User-facing text changes, if any, updated all locale files.
- [ ] Bitwarden client compatibility was considered for sync/API shape changes.
- [ ] No secrets, tokens, private deployment values, or real vault data are included.

## Checks

- [ ] `npx tsc -p tsconfig.json --noEmit`
- [ ] `npx tsc -p webapp/tsconfig.json --noEmit`
- [ ] `npm run i18n:validate`
- [ ] `npm run build`
- [ ] `npm run verify` — type checks for all four tsconfigs, `npm test`, `npm run i18n:validate`, `npm run build`
- [ ] New or changed tests: `npm test` was run at least twice (some cleanup paths are gated on `Math.random()`, so one green run can be luck)

## Notes

Expand Down
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ updates:
day: "monday"
time: "05:00"
timezone: "Asia/Shanghai"
# 新版本发布后先等 7 天再提 PR(cooldown):避免上游一天连发几个补丁就刷出一串 PR,
# 也避免刚发布、尚未被广泛验证的版本立刻进仓。
cooldown:
default-days: 7
open-pull-requests-limit: 5
groups:
npm-minor-and-patch:
Expand All @@ -26,6 +30,10 @@ updates:
day: "monday"
time: "05:10"
timezone: "Asia/Shanghai"
# 同上。本生态的 open-pull-requests-limit 是 0,即不会开出版本更新 PR,
# 所以这条 cooldown 目前不影响实际行为,加它只为两个生态配置保持一致。
cooldown:
default-days: 7
open-pull-requests-limit: 0
groups:
github-actions:
Expand Down
13 changes: 8 additions & 5 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ on:
branches:
- "**"

permissions:
contents: read
actions: read
security-events: write
packages: read
# 权限一律在 job 级声明:工作流级声明会让所有 job(含未来的新 job)共享同一套权限,
# 而 security-events: write 只有上传分析结果的那一步需要。
permissions: {}

jobs:
analyze:
name: CodeQL Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
packages: read
security-events: write

strategy:
fail-fast: false
Expand Down
34 changes: 5 additions & 29 deletions .github/workflows/security-extra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,35 +48,11 @@ jobs:
upload-sarif: true
fail-on-vuln: true

pnpm-audit:
name: pnpm audit
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 22

- name: Run pnpm audit
shell: bash
run: |
if [ ! -f pnpm-lock.yaml ]; then
echo "pnpm-lock.yaml not found, skip pnpm audit."
exit 0
fi

corepack enable
corepack prepare pnpm@10 --activate
pnpm audit --audit-level=high
# 说明:这里原本还有一个 `pnpm audit` job,但本仓库使用 npm(仅有
# package-lock.json,无 pnpm-lock.yaml),该 job 的守卫 `if [ ! -f pnpm-lock.yaml ]`
# 必然直接 exit 0 —— 永远不执行任何审计。实测上方 osv job 的
# `scan source --recursive` 会扫到 package-lock.json(321 个包),
# 覆盖范围等价,故移除该冗余 job。

semgrep:
name: Semgrep CE Scan
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/sync-global-domains.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ on:
default: "main"
type: string

permissions:
contents: write
pull-requests: write
# 权限一律在 job 级声明:工作流级声明会让所有 job 共享同一套过宽权限。
permissions: {}

jobs:
sync-global-domains:
runs-on: ubuntu-latest
# 需要建分支/提交(contents)并创建 PR(pull-requests)——
# 这也是本文件的 checkout **不能**加 persist-credentials: false 的原因(与 verify.yml 相反)。
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 22
# 与 verify.yml、Cloudflare Workers Builds 共用同一版本来源(.nvmrc)。
node-version-file: '.nvmrc'

- name: Sync generated Bitwarden domains
env:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Verify

# 说明:本仓库已有一套测试与校验脚本(类型检查 / i18n 对齐 / 通知与 WebAuthn
# 安全测试 / 构建),但在本次补充前,CI 中没有任何一个 workflow 执行它们 ——
# 测试写了却无人自动运行,回归不会被拦截。此外 `pnpm audit` 步骤因项目使用 npm
# (仅有 package-lock.json)而被守卫跳过。此 workflow 补上“验证”这一环。
#
# 另:scripts/security-audit-*.mjs 三个安全回归脚本此前在 CI、npm scripts 与
# 文档中均无引用(死资产),现已通过 `test:security-audit` 纳入 `npm test`。
#
# 本地复现:npm run verify

on:
push:
branches: [main]
pull_request:

# 仅需读取仓库内容
permissions:
contents: read

jobs:
verify:
name: Type check, i18n, tests, build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
# 本 job 不需要 git push,因此不把 GITHUB_TOKEN 写进 .git/config。
# 否则 npm ci 期间任何依赖包的 postinstall 脚本都能读到该令牌。
# 与 codeql.yml / security-extra.yml 保持一致。
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
# 版本来源单一化:.nvmrc 同时被本 workflow 与 Cloudflare Workers Builds
# 的构建镜像读取(官方文档:NODE_VERSION / .nvmrc / .node-version)。
# 24.18.0 是 Cloudflare 构建镜像预装的版本,因此三方零分叉、零下载。
node-version-file: '.nvmrc'
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify (typecheck + i18n + tests + build)
run: npm run verify
32 changes: 32 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# gitleaks 配置:只用于排除「刻意写死的假凭据」误报 —— 测试夹具里的自述式 JWT 密钥,
# 以及 Web 端 UI 演示数据里的假 SSH 私钥。
#
# 为什么需要配置级白名单,而不是改代码或加行内注释:
# - `scripts/lib/test-harness.ts` 定义的测试用 JWT 密钥,取值是自述式字符串
# `test-jwt-secret-at-least-32-characters-long`:变量名含 "secret"、熵值 3.602
# 高于阈值,于是熵值类规则 `generic-api-key` 必然命中;另有 3 个测试文件把同一
# 字面量写死在本地,合计 4 处(均出自提交 8efe2a6)。
# - `webapp/src/lib/demo.ts` 的演示密码库里有一条名为 "Production SSH key" 的假条目:
# `notes` 字段自己就写着 `Fake SSH key material for UI preview.`,所谓私钥的主体
# 是字面量 `DEMO-PRIVATE-KEY`。`private-key` 规则只认 `-----BEGIN/END ... PRIVATE
# KEY-----` 装甲头、不校验中间是否为合法 base64,因此必然命中(2026-06-23 首次报出)。
# - gitleaks 的扫描单位是「**本次 push 范围内新增的行**」(日志可见它执行的是
# `git log -p -U0 --no-merges --first-parent <range>`)。因此即使后续提交删掉
# 或改写这些行,只要 8efe2a6 仍在该范围内(例如合并进 main 的那次 push),
# 行内 `gitleaks:allow` 注释也无能为力 —— 只有配置级白名单能稳定生效。
#
# 白名单按**那一个字面值**匹配,不是按目录、也不是按文件:
# 测试目录或演示数据里若真的混进其它凭据(哪怕同一个文件),其它取值与其它规则仍会照常报出。
#
# 注意:本文件是在 gitleaks 8.24.3(CI 中 gitleaks-action 自动安装的版本)下验证的。
# 升级 gitleaks 大版本时需复核 `[allowlist]` 的写法(新版本已改用 `[[allowlists]]`)。

[extend]
useDefault = true

[allowlist]
description = "刻意写死的假凭据:测试用自述式 JWT 密钥 + UI 演示数据里的假 SSH 私钥"
regexes = [
'''test-jwt-secret-at-least-32-characters-long''',
'''DEMO-PRIVATE-KEY''',
]
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.21.0
62 changes: 62 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,63 @@ For new locales, update:
- `webapp/src/lib/i18n/locales/*`
- `scripts/i18n-utils.cjs`

### Tests

`npm test` runs the whole suite. It needs no external services and no network: the
D1 and R2 bindings are backed by in-process implementations, so the tests execute
**real SQL** against the real schema (including the real shadow tables and the
final swap used by restore).

```sh
npm test
```

Files worth knowing about when adding a test:

- `scripts/lib/test-harness.ts` — shared fixture. Start from
`createSchemaDatabase()` / `insertUser()` instead of building a database by hand;
it also resets the process-scoped statics that would otherwise stop a second
database from getting a schema.
- `scripts/lib/d1-sqlite.ts` — a `D1Database` on Node's built-in `node:sqlite`.
- `scripts/lib/r2-memory.ts` — in-memory attachment bucket.
- `scripts/lib/sql-recorder.ts` — records the statements that were run. Two
counters, do not mix them up: `queries` counts prepared statements (use it for
query-plan assertions) while `roundTrips` counts database round trips (use it
for N+1 assertions). `batch([...])` is N prepares but **one** round trip.
- `scripts/lib/register-cloudflare-stub.mjs` — handler tests must be started as
`tsx --import ./scripts/lib/register-cloudflare-stub.mjs`, because
`cloudflare:workers` cannot be resolved under Node. This is already wired into
every `test:*-handler` script; copy one of them when adding another.

Two rules that exist because each has already produced a false result:

- **Run the suite at least twice** before calling a change green. Some cleanup
paths are gated on `Math.random()` and only run on a fraction of requests, so a
single passing run can be luck. When testing such a path, pin `Math.random` for
the duration of the test rather than hoping the path fires.
- **Never assert that two timestamps differ.** Calling `new Date().toISOString()`
twice within the same millisecond returns the same value. Pin a baseline
timestamp and assert that the new value is greater.

## Recommended Checks

Before opening a pull request, run the same command CI runs:

```sh
npm run verify
```

which is:

```sh
npm run typecheck # tsconfig.json, webapp/, tsconfig.scripts.json, tsconfig.webapp-tests.json
npm run i18n:validate
npm test
npm run build
```

Narrower runs while iterating.

For most backend or shared changes:

```sh
Expand All @@ -126,6 +181,13 @@ npx tsc -p webapp/tsconfig.json --noEmit
npm run build
```

For changes under `scripts/` (tests and tooling):

```sh
npx tsc -p tsconfig.scripts.json --noEmit
npm test
```

For documentation-only changes:

```sh
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ npm run deploy

# Optional: KV mode
npm run deploy:kv
# The first deploy pins the KV namespace id into wrangler.kv.toml (commit that change).
# If the account already has a namespace with a *similar* title, the script stops and asks
# you to choose explicitly instead of guessing (a wrong guess sends attachments elsewhere):
# node scripts/ensure-kv.cjs --id <32-hex> # reuse one of the candidates
# node scripts/ensure-kv.cjs --force-new # create a new namespace anyway

# Local development
npm run dev
Expand Down
5 changes: 5 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ npm run deploy

# 可选:KV 模式
npm run deploy:kv
# 首次部署会把账号里的 KV 命名空间 id 写回 wrangler.kv.toml(记得把这次改动一并提交)。
# 若账号里已有「标题相近但名字不完全一致」的命名空间,脚本会停下来让你显式选择,
# 而不是替你猜(猜错会把附件写进另一个库):
# node scripts/ensure-kv.cjs --id <32 位 hex> # 复用其中一个
# node scripts/ensure-kv.cjs --force-new # 确实要新建

# 本地开发
npm run dev
Expand Down
39 changes: 39 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Use GitHub Private Vulnerability Reporting instead:
3. Click **Report a vulnerability**.
4. Submit the report privately.

Direct link (replace the owner if this file is reused by another fork):
<https://github.com/cakkl/nodewarden/security/advisories/new>

NodeWarden is independent from Bitwarden. Please do not report NodeWarden-specific issues to the official Bitwarden team.

## What to Include
Expand Down Expand Up @@ -60,6 +63,42 @@ We aim to acknowledge valid private reports within 72 hours, investigate the iss

Please do not publicly disclose vulnerability details before a fix or mitigation is available.

## Administrator Bootstrap and Role Assignment

NodeWarden has no separate setup step or setup token. Administrator privilege is
assigned as follows.

**First account.** The first account that registers on an instance is granted the
`admin` role, and the instance is then marked as registered. This is recorded in
the security audit log as `user.register.first_admin`.

**Later accounts.** After the first account exists, registration requires an
invite code (`Invite code is required`, HTTP 403) and the new account gets the
default `user` role. These are recorded as `user.register.invite`. Registration
can therefore not be used to obtain administrator privilege on an existing
instance.

**Recovery when no administrator exists.** If an instance ends up with no account
holding the `admin` role — for example the last administrator account was deleted
— the database bootstrap promotes the **earliest-created** account back to `admin`
on its next schema initialization. This is a system action with no actor, and is
recorded in the security audit log as `user.bootstrap.admin_promoted`.

Two properties of that recovery path are worth knowing:

* **Administrator accounts are not protected against deletion.** NodeWarden does
not block deleting the last administrator. The bootstrap exists so an instance
cannot become permanently unmanageable, but it is not a substitute for
administrative care on a multi-user instance.
* **The account that regains the role is selected by account creation time, not
by trust.** On a multi-user instance, make sure you intend to delete an
administrator account before doing so.

The bootstrap only runs when the runtime schema is initialized or re-initialized
(for example after a schema version change), not on every request. Operators who
need tighter control over administrator assignment should edit the `users.role`
column directly and treat the bootstrap as a recovery mechanism only.

## Supported Versions

Security fixes are generally provided for the latest release and the latest code on the default branch.
Expand Down
Loading