From 6cbc06f833e141f344050ee0bdff5cc5a4ba2440 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Fri, 17 Jul 2026 11:32:43 +0800 Subject: [PATCH] feat: add Web Vault visibility switch --- README.md | 4 +++- README_ZH.md | 4 +++- src/index.ts | 29 ++++++++++------------- src/types/index.ts | 2 ++ src/web-vault-visibility.ts | 47 +++++++++++++++++++++++++++++++++++++ wrangler.kv.toml | 2 +- wrangler.toml | 2 +- 7 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 src/web-vault-visibility.ts diff --git a/README.md b/README.md index 4a9ad3c..c025586 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ - If the site reports a missing `JWT_SECRET`, add it as a **Secret** in Workers settings. In production use a random string of at least 32 characters; do not use temporary or example values. +- To hide the Web Vault, add a text variable named `HIDE_WEB_VAULT` with the value `1` under **Workers settings → Variables and Secrets**. While enabled, server-hosted frontend pages and static assets return `404 Not Found`, while the login, sync, attachment, icon, notification, and other server endpoints used by Bitwarden clients remain available; an already installed or cached PWA can continue using its local frontend. Delete the variable (or change it to anything other than `1`) to restore the server-hosted Web Vault. + - In this flow you hand code to Cloudflare to build and deploy. `wrangler.toml` or `wrangler.kv.toml` in the repo defines binding names; the Worker initializes the D1 schema on first request—no manual SQL upload. @@ -147,4 +149,4 @@ LGPL-3.0 License Star History Chart - \ No newline at end of file + diff --git a/README_ZH.md b/README_ZH.md index f37c192..56fb130 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -78,6 +78,8 @@ - 页面提示缺少 `JWT_SECRET` 时,到 Workers 设置里添加 Secret。正式环境至少使用 32 个字符以上的随机字符串,不要使用临时值或示例值。 +- 如需隐藏 Web Vault,在 Workers 的“设置 → 变量和机密”中添加文本变量 `HIDE_WEB_VAULT`,值设为 `1`。启用后,服务器上的前端页面和静态资源统一返回 `404 Not Found`,Bitwarden 客户端所需的登录、同步、附件、图标、通知等服务端接口仍可使用;已经安装或缓存的 PWA 可以继续使用本地前端。删除该变量(或将值改为非 `1`)即可恢复服务器上的 Web Vault。 + - 这套流程里,用户实际做的是把代码交给 Cloudflare 构建并部署。代码里的 `wrangler.toml` 或 `wrangler.kv.toml` 决定绑定名,Worker 第一次处理请求时会自动初始化 D1 schema,不需要用户上传 SQL。 @@ -146,4 +148,4 @@ LGPL-3.0 License Star History Chart - \ No newline at end of file + diff --git a/src/index.ts b/src/index.ts index 8ef182c..e34d0b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,11 @@ import { handleRequest } from './router'; import { StorageService } from './services/storage'; import { applyCors, jsonResponse } from './utils/response'; import { runScheduledBackupIfDue } from './handlers/backup'; +import { + isBackendRequestPath, + isWebVaultHidden, + webVaultNotFoundResponse, +} from './web-vault-visibility'; let dbInitialized = false; let dbInitError: string | null = null; @@ -19,22 +24,6 @@ function normalizeRequestUrl(request: Request): Request { return new Request(url.toString(), request); } -function isWorkerHandledPath(path: string): boolean { - return ( - path.startsWith('/api/') || - path.startsWith('/identity/') || - path.startsWith('/icons/') || - path.startsWith('/fill-assist/') || - path.startsWith('/notifications/') || - path.startsWith('/.well-known/') || - path === '/v1/assetlinks:check' || - path === '/web-bootstrap' || - path === '/config' || - path === '/api/config' || - path === '/api/version' - ); -} - function addSearchIndexHeaders(request: Request, response: Response): Response { const url = new URL(request.url); const contentType = String(response.headers.get('Content-Type') || '').toLowerCase(); @@ -58,7 +47,7 @@ async function maybeServeAsset(request: Request, env: Env): Promise { void ctx; const normalizedRequest = normalizeRequestUrl(request); + const requestPath = new URL(normalizedRequest.url).pathname; + + if (isWebVaultHidden(env) && !isBackendRequestPath(requestPath)) { + return webVaultNotFoundResponse(normalizedRequest); + } + const assetResponse = await maybeServeAsset(normalizedRequest, env); if (assetResponse) { return applyCors(normalizedRequest, assetResponse, env); diff --git a/src/types/index.ts b/src/types/index.ts index aafb86b..01079b7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,8 @@ export interface Env { ASSETS?: { fetch(input: RequestInfo | URL, init?: RequestInit): Promise; }; + // Set to "1" to return 404 for the Web Vault while keeping client APIs available. + HIDE_WEB_VAULT?: string; // Prefer R2 when available. Optional to support KV-only deployments. ATTACHMENTS?: R2Bucket; // Optional fallback for attachment/send file storage (no credit card required). diff --git a/src/web-vault-visibility.ts b/src/web-vault-visibility.ts new file mode 100644 index 0000000..bf685cc --- /dev/null +++ b/src/web-vault-visibility.ts @@ -0,0 +1,47 @@ +import type { Env } from './types'; + +const BACKEND_PATH_PREFIXES = [ + '/api', + '/identity', + '/icons', + '/fill-assist', + '/notifications', + '/.well-known', + // Compatibility aliases retained for older Bitwarden clients. + '/devices', + '/auth-requests', + '/webauthn', +] as const; + +const BACKEND_EXACT_PATHS = new Set([ + '/v1/assetlinks:check', + '/web-bootstrap', + '/config', + '/accounts/kdf', + '/settings/domains', +]); + +export function isBackendRequestPath(pathname: string): boolean { + const path = pathname.toLowerCase(); + if (BACKEND_EXACT_PATHS.has(path)) return true; + + return BACKEND_PATH_PREFIXES.some((prefix) => ( + path === prefix || path.startsWith(`${prefix}/`) + )); +} + +export function isWebVaultHidden(env: Env): boolean { + return String(env.HIDE_WEB_VAULT || '').trim() === '1'; +} + +export function webVaultNotFoundResponse(request: Request): Response { + const body = request.method === 'HEAD' ? null : 'Not Found'; + return new Response(body, { + status: 404, + headers: { + 'Cache-Control': 'no-store, max-age=0', + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'noindex, nofollow, noarchive, nosnippet', + }, + }); +} diff --git a/wrangler.kv.toml b/wrangler.kv.toml index 8ca8f22..c5e3453 100644 --- a/wrangler.kv.toml +++ b/wrangler.kv.toml @@ -9,7 +9,7 @@ command = "npm run build" binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = false +run_worker_first = true [triggers] crons = [ "*/5 * * * *" ] diff --git a/wrangler.toml b/wrangler.toml index df0f11e..75db16b 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -9,7 +9,7 @@ command = "npm run build" binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = false +run_worker_first = true [triggers] crons = [ "*/5 * * * *" ]