From b131e1b11d7fb7920426cb81599f1ea1d0c10192 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 16:39:09 +0800 Subject: [PATCH 1/4] Fail closed without admin configuration and document SMTP operations --- apps/waterfall-wiki/ACCOUNT.md | 46 ++++++++++++++++++++++++++++++++ apps/waterfall-wiki/src/proxy.ts | 5 +++- compose.yml | 1 + scripts/integration.sh | 2 ++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 apps/waterfall-wiki/ACCOUNT.md diff --git a/apps/waterfall-wiki/ACCOUNT.md b/apps/waterfall-wiki/ACCOUNT.md new file mode 100644 index 0000000..03fbf3f --- /dev/null +++ b/apps/waterfall-wiki/ACCOUNT.md @@ -0,0 +1,46 @@ +# Email accounts + +`/account` offers registration, password login, email verification/resend, and logout. +`/api/account/session` returns the current account; other actions use same-origin POST. +Passwords require at least 12 characters and at most 256 UTF-8 bytes. Only verified +accounts can log in. Verification links expire after 30 minutes and are consumed once. +Session tokens expire after seven days; logout deletes the current token immediately. +Only token SHA-256 digests are stored. Passwords use individually salted scrypt. + +## Runtime configuration + +The existing production deployment reads `/etc/prompt.env` on ces-control. Set its +ownership to root and mode to 0600. The operator should update SMTP_PASSWORD there +through a secure remote session, without putting it in command arguments, repository, +CI output, release.env, or chat. Existing compose deployment reads this file without +copying credentials into the release directory. Do not run `docker compose config` +without `--quiet` against the production environment because interpolation shows secrets. +Docker administrators can inspect container environment; access to Docker is privileged. + +Set ACCOUNT_PUBLIC_ORIGIN to the exact browser origin and SMTP_HOST=smtpdm.aliyun.com, +SMTP_PORT=465, SMTP_USERNAME and SMTP_FROM=noreply@notice.entroia.com. TLS certificate +validation stays enabled. Missing SMTP configuration returns a generic service failure; +resend with the registration password can recover an account whose initial delivery failed. + +Keep AUTH_CONFIG_FILE configured for administrator access. Missing administrator +configuration fails closed. AUTH_DISABLED_FOR_TESTS is false by default and is enabled +only by the disposable legacy integration stack; never enable it in production. +Email account cookies never satisfy administrator authorization. With the current HTTP +acceptance origin use AUTH_COOKIE_SECURE=false; use true after enabling HTTPS. + +Limits persist in PostgreSQL: 10 logins and 3 registration/resend attempts per email per +15 minutes, plus 300 requests per 15 minutes globally. ACCOUNT_TRUST_PROXY defaults +false. Enable it only when a trusted ingress overwrites x-forwarded-for; this changes +the global cap to 60 per trusted client address. Do not enable it on the direct public port. +Limits count failures and successes. Registration/resend always give the same success +response for existing/unknown emails, and login uses a dummy KDF for unknown accounts. +The rate limit itself can reveal recent activity for a known email; it does not reveal +whether an account exists. No password recovery or account deletion is included. + +## Verification + +ces-control runs crypto/origin tests, PostgreSQL verification-token replay races, +expired/revoked sessions, concurrent limit consumption and unverified login rejection. +Production-auth exercises verify that account cookies cannot authorize legacy writes. +Actual SMTP delivery and visual acceptance at http://8.211.154.134:18131/account require +operator runtime configuration and the normal manually approved deployment. diff --git a/apps/waterfall-wiki/src/proxy.ts b/apps/waterfall-wiki/src/proxy.ts index 11b2a4e..65450ad 100644 --- a/apps/waterfall-wiki/src/proxy.ts +++ b/apps/waterfall-wiki/src/proxy.ts @@ -9,7 +9,10 @@ export function proxy(request: NextRequest) { if (path === '/brand-mark.svg' || path === '/login' || path === '/api/auth/login' || path === '/api/auth/logout' || path === '/api/health' || path === '/api/ready' || path.startsWith('/_next/static/')) return NextResponse.next({headers:responseHeaders}); let config; try { config = authConfig(); } catch { return NextResponse.json({error:'登录服务配置不可用'}, {status:503,headers:responseHeaders}); } - if (!config) return NextResponse.next(); + if (!config) { + if (process.env.AUTH_DISABLED_FOR_TESTS === 'true' && process.env.AUTH_CONFIG_FILE === '') return NextResponse.next(); + return NextResponse.json({error:'管理员登录服务尚未配置'}, {status:503,headers:responseHeaders}); + } const bearer = request.headers.get('authorization'); const token = bearer?.startsWith('Bearer ') ? bearer.slice(7) : request.cookies.get(SESSION_COOKIE)?.value; if (validSession(token,config)) { diff --git a/compose.yml b/compose.yml index 5bd9c3d..48f6f12 100644 --- a/compose.yml +++ b/compose.yml @@ -12,6 +12,7 @@ x-waterfall-wiki-environment: &waterfall-wiki-environment CHROMIUM_PATH: /usr/bin/chromium RENDER_ORIGIN: http://waterfall-wiki:3000 AUTH_CONFIG_FILE: ${AUTH_CONFIG_FILE:-} + AUTH_DISABLED_FOR_TESTS: ${AUTH_DISABLED_FOR_TESTS:-false} AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE:-true} ACCOUNT_PUBLIC_ORIGIN: ${ACCOUNT_PUBLIC_ORIGIN:-http://8.211.154.134:18131} ACCOUNT_TRUST_PROXY: ${ACCOUNT_TRUST_PROXY:-false} diff --git a/scripts/integration.sh b/scripts/integration.sh index 857a956..4e65f87 100644 --- a/scripts/integration.sh +++ b/scripts/integration.sh @@ -11,6 +11,8 @@ cleanup(){ code=$?; if [ "$code" -ne 0 ]; then compose logs --tail 160 waterfall trap cleanup EXIT # Random credentials exist only for this disposable stack; no host Node required. docker run --rm --entrypoint node "$WATERFALL_WIKI_IMAGE" -e "const c=require('crypto');const hex=()=>c.randomBytes(24).toString('hex');console.log('POSTGRES_PASSWORD='+hex()+'\nS3_ACCESS_KEY='+hex()+'\nS3_SECRET_KEY='+hex())" > "$TEST_ENV" +# Explicitly isolate legacy unauthenticated integration coverage; production fails closed. +printf '\nAUTH_DISABLED_FOR_TESTS=true\n' >> "$TEST_ENV" compose up -d --no-build --wait --wait-timeout 120 postgres objects compose run --rm --no-deps migrate compose up -d --no-build --wait --wait-timeout 150 waterfall-wiki waterfall-wiki-worker From b157504ca6dc02d145522436a83bc30c5453edeb Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 16:46:50 +0800 Subject: [PATCH 2/4] Keep account session revocation available when rate limited --- apps/waterfall-wiki/ACCOUNT.md | 5 ++++- apps/waterfall-wiki/src/lib/account-handler.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/waterfall-wiki/ACCOUNT.md b/apps/waterfall-wiki/ACCOUNT.md index 03fbf3f..6f49425 100644 --- a/apps/waterfall-wiki/ACCOUNT.md +++ b/apps/waterfall-wiki/ACCOUNT.md @@ -35,7 +35,10 @@ the global cap to 60 per trusted client address. Do not enable it on the direct Limits count failures and successes. Registration/resend always give the same success response for existing/unknown emails, and login uses a dummy KDF for unknown accounts. The rate limit itself can reveal recent activity for a known email; it does not reveal -whether an account exists. No password recovery or account deletion is included. +whether an account exists. Synchronous SMTP latency can still expose a timing side channel; +a durable asynchronous mail queue would be needed to fully decouple delivery timing. +Logout remains available when login/mail limits are exhausted. No password recovery or +account deletion is included. ## Verification diff --git a/apps/waterfall-wiki/src/lib/account-handler.ts b/apps/waterfall-wiki/src/lib/account-handler.ts index 4a7dbbb..1c9c8b9 100644 --- a/apps/waterfall-wiki/src/lib/account-handler.ts +++ b/apps/waterfall-wiki/src/lib/account-handler.ts @@ -14,10 +14,11 @@ export async function POST(request: Request,context: Context) { const {action} = await context.params; if (!['register','resend','verify','login','logout'].includes(action)) return json({error:'不存在'},404); try { + // Revocation remains available even when login or mail limits are exhausted. + if (action === 'logout') { await logoutAccount(request); const r=json({ok:true});r.headers.set('Set-Cookie',accountCookie('',true));return r; } // Use a global cap by default: forwarding headers are untrusted unless the deployment explicitly enables a trusted ingress. const ip = process.env.ACCOUNT_TRUST_PROXY === 'true' ? request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown' : 'global'; if (!await rateLimit('request:'+ip,process.env.ACCOUNT_TRUST_PROXY === 'true' ? 60 : 300)) return json({error:'请求过于频繁,请稍后再试'},429); - if (action === 'logout') { await logoutAccount(request); const r=json({ok:true});r.headers.set('Set-Cookie',accountCookie('',true));return r; } if (Number(request.headers.get('content-length') || 0)>4096) return json({error:'请求过大'},413); const text=await request.text(); if(text.length>4096)return json({error:'请求过大'},413); let body;try{body=JSON.parse(text);}catch{return json({error:'请求格式错误'},400);} From a1984e615a08f5539350e4762e10f3caee14eb37 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 16:49:27 +0800 Subject: [PATCH 3/4] Verify public knowledge reads and protected administrator writes --- apps/waterfall-wiki/tests/auth.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/waterfall-wiki/tests/auth.test.ts b/apps/waterfall-wiki/tests/auth.test.ts index 28132ac..a3b8114 100644 --- a/apps/waterfall-wiki/tests/auth.test.ts +++ b/apps/waterfall-wiki/tests/auth.test.ts @@ -34,14 +34,18 @@ test('proxy protects APIs, data routes, static content and Studio; fails closed' const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE; try{ const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config)); - for(const path of ['/','/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); + for(const path of ['/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path); - for(const path of ['/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); + for(const path of ['/','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'}; assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://wiki.test'}})).status,200); - writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/')).status,503); + assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{cookie:'wiki_account='+ 'a'.repeat(64)}})).status,401); + assert.equal(proxy(new NextRequest('http://wiki.test/guide/welcome',{method:'POST'})).status,401); + writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503); + delete process.env.AUTH_CONFIG_FILE; + assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503,'Missing admin configuration fails closed'); }finally{if(old===undefined)delete process.env.AUTH_CONFIG_FILE;else process.env.AUTH_CONFIG_FILE=old;rmSync(temp,{recursive:true,force:true});} }); From 7372783062700e5cea70774cd47170da1dba18bb Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 16:50:35 +0800 Subject: [PATCH 4/4] Record reference screenshots and anonymous publishing limits --- docs/linked-guidance/product-plan.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/linked-guidance/product-plan.md b/docs/linked-guidance/product-plan.md index 79cd8c8..2be14f8 100644 --- a/docs/linked-guidance/product-plan.md +++ b/docs/linked-guidance/product-plan.md @@ -36,3 +36,11 @@ Hugging Face 可借鉴:owner/name 身份、标签、概览与文件/社区分 - 文件标签:观察到 references、scripts 文件夹和 SKILL.md、共 4 文件;侧栏有 token 预估、下载收藏、作者、安全报告、内容指纹、数字签名与相关推荐。文件树仍将整个包作为单位。 本项目取舍:保留清晰命名空间、列表可扫描性、阅读与源文档切换;将安装栏改为“采用单篇”,将包文件树改为条件链接和反向引用。暂不复制评分、下载热度、认证徽章,避免没有真实数据的信任暗示。当前暖绿原型是独立视觉探索,需完成参考截图与服务器验收后修订。 + +## 第二轮截图补充 + +已在主 chat 逐项截取:文件树与安装侧栏、元数据与签名侧栏、版本历史、手机号登录弹窗、企业作者页、发布访问限制。截图为 Chrome 原生窗口实拍,包含浏览器框;暂未存为仓库图片文件。 + +版本历史为时间线,显示当前版本标记、日期、说明、展开/下载控件;安装右栏在不同标签间保持一致。登录弹窗区分个人/团队,个人有微信和手机号模式,手机号模式包含验证码输入、协议勾选、主按钮;没有执行登录或同意协议。作者页包含头像/名称/认证/简介、关注、横向统计条、左侧排序分类、右侧搜索与作品目录。发布入口 `/dashboard/publish` 匿名访问明确显示“需要登录才能访问”,无法检查其内部编辑器,不假定内部布局。 + +待补:详情评论和评测标签、首页下半屏的独立截图、移动端;插件、付费服务、大赛作为非核心周边功能,不进入本次开发首版。第一版发布使用结构化指导编辑器,支持 XML 校验与预览,不沿用未知的 SkillHub 内部发布流程。