Files
Arcrun/system-dev/docs/3-specs/portal-auth/tasks.md
T
Claude (總管雲端) eaaf894b23 portal: 來源回溯 gitea:// 顯示為可點超連結(PORTAL_SOURCE_WEB_BASE 可選 var)
- 新增可選 var PORTAL_SOURCE_WEB_BASE:設了之後 Portal 卡片詳頁「來源回溯」與
  AI 問答出處列把 gitea://<path>#n 渲染成 {base}/<path> 超連結(新分頁開啟);
  未設時行為一字不變(Mira 零影響)。
- 安全:href 過 esc()、路徑逐段 encodeURIComponent、rel=noopener;
  只認 gitea:// 前綴,其他 scheme 一律純文字。
- 驗證:tsc 0 錯誤;portal-auth/portal-data 測試 48/48 綠;
  渲染後 client JS 語法檢查通過。
- 來源:leo RAG demo 客戶測試回饋(2026-07-18)「來源回溯沒辦法點擊」。
- 部署(leo 閘):uncle6 重部 cypher 時帶
  --var PORTAL_SOURCE_WEB_BASE:"https://git.uncle6.me/Leo/arcrun-rag-demo-knowledge/src/branch/main"
2026-07-18 06:01:26 +00:00

14 KiB
Raw Blame History

portal-auth — Tasks(實作切分,每階段可獨立 PR)

狀態:全部未動工——本 SDD 待總管/leo 審後才開工(B 類流程:SDD 審 → 實作 PR → component-pr-review-standard → 總管 review → gated 部署 leo21c 對應:Gitea #24/#25design.md 各 §

依賴鏈:P1 → P2 → P3 → P4(P2 不依賴 P1 可並行起,但 P3 要兩者都齊)。


P1 — KBDB「庫」filter 地基(design §3.2/§3.3)|觸碰:kbdb/

  • 開工第一件事:實測 Vectorize metadata filter $in 支援與否(決定主路徑 vs fan-out fallback
    • 核實結論(2026-07-14支援,走主路徑 $in,不需 fan-out fallback。證據:① 官方文件 developers.cloudflare.com/vectorize/reference/metadata-filtering/ 明列 8 運算子 $eq/$ne/$in/$nin/$lt/$lte/$gt/$gte,「For $in and $nin, filter object values can be arrays of string, number, boolean, or null values」;② 本 repo @cloudflare/workers-types@4.20260702.1 原生 typing VectorizeVectorMetadataFilterCollectionOp = "$in" | "$nin"index.d.ts:15782), wrangler 4.98.0。限制:metadata filtering 只對 2023-12-06 之後建的 index 有效(本專案 index 皆是)。 本機無 Vectorize runtime$in線上實跑併入部署排練驗(見部署清單)。
  • /entries/search/entrieslibrary 多值參數(D1 json_extract INNULL→general fallback
    • 實作註:SQL 用 COALESCE(json_extract(metadata_json,'$.library'),'general') IN (…)——與 design §3.3 的 OR 形狀語意完全等價(單組佔位符較簡)。本機 miniflare+local D1 十案例實跑全過(PR 附證據)。
  • embed upsert metadata 加 library 欄;semanticSearch filter 支援 library$in 主路徑fan-out 不需)
    • 實作註:Vectorize 端在寫入時正規化(未標記→'general')——Vectorize filter 做不了 COALESCE, 寫入端蓋章後查詢端單純 $in;與 D1 查詢端 fallback 語意對齊。
  • Vectorize library metadata index 建立步驟+reindex backfill 寫進部署清單
    • 部署清單(待雲端排練驗——本機無 Vectorize runtimesemantic 路徑 code 完成、線上實跑未驗)
      1. wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name library --type string
      2. POST /embed/backfill {"reindex":true} 分批到 remaining=0index 只收建立後 upsert 的向量)
      3. 驗收抽查:semantic 帶 library= 過濾命中/不帶行為不變
      4. 掛號:cli/src/lib/deploy.ts ensureVectorizeMetadataIndexes() 待補 library(cli/ 屬 P1 派工範圍外「只動 kbdb」,隨 P3 或部署 PR 補——kbdb/wrangler.toml 註解已標)
  • cypher kbdb-proxy 透傳 library 參數(供 owner/admin 面用;portal 面走 P3 的注入,不經這)
    • ⚠️ 範圍註(2026-07-14):P1 派工紅線「只動 kbdb、不碰 cypher-executor」與本項矛盾——照窄範圍 執行,本項順延(一行 query 透傳,隨 P3 動 cypher 時一併)。
    • 已隨 P3 補上(2026-07-14 P3 PR):/kbdb/search/kbdb/entries 透傳白名單各加 library
  • 測試:D1 filter 單元測、NULL fallback、多值、semantic filtermock VECTORIZE
    • kbdb/tests/library-filter.test.ts 11 項(SQL 形狀/route 解析/向後相容/降級仍 enforce/$in 構造/ 寫入端正規化)+既有 6 項全綠(17/17);tsc exit 0。
  • 驗收curl /entries/search?q=&library=finance 只回 finance+未標記條目歸 general 可驗;semantic 同
  • 工程量:小-中(0.5–1 個 CC 工作天)

P2 — portal_user 模型+認證 APIdesign §2/§4)|觸碰:cypher-executor/(新 route 檔)

  • seed template portal_userportal_libraryinit-seed 慣例)
    • 實作註(2026-07-14):種子資料 cypher-executor/src/lib/portal-seeds.ts*-seeds.ts 慣例類別); 冪等 ensure 在 routes/portal.ts ensurePortalTemplates(),掛進 POST /init/seed(回應加 portal_templates 區塊)+ bootstrap 前自動 ensure(沒跑過 seed 也能 bootstrap)。
  • PBKDF2-SHA256 雜湊模組(600k iters、常數時間比對、pbkdf2-sha256$… 格式)
    • cypher-executor/src/lib/portal-auth.tsWebCrypto deriveBitsverify 從儲存格式解析 iterations → 換 KDF/參數可共存漸進遷移;constantTimeEqual 同 mcp PR#15 實作)。
  • POST /portal/admin/bootstrapconsole owner session 閘)
  • login/logout/session/改密碼(KV portal_sess:、TTL var、每請求回讀 record 驗 status
    • PORTAL_SESSION_TTL 進 wrangler.toml [vars]types.ts(預設 604800)。
  • admin 用戶 CRUDlibraries 授權+庫目錄 CRUDrole=admin 閘)
    • 安全註:admin 操作目標 record 需過「email head entry 指回同 record_id」成員驗證(防拿任意 record_id 改到不相干 KBDB record);列表/回應一律剝除 password_hash;未帶密碼建帳號 → server 產一次性密碼隨回應回傳一次(不落地明碼)。
  • 登入失敗節流(5 次/15 分鐘,KV TTL)
  • 順手修 record-crud.ts updateRecord grow 路徑漏 owner_iddesign §2.2 附帶)
    • 修法:首查 JOIN entries 帶回 record 既有 owner_idgrow 的新 entry 沿用。回歸測試 kbdb/tests/record-grow-owner.test.ts3 項:帶 owner/原無 owner 維持 null/非 grow 不 INSERT)。
  • 測試:bootstrap 閘、登入對錯、停用即拒(session 立即失效)、role 閘、雜湊格式、{tenant}::portal 子 namespace 隔離(搜 email 搜不到
    • 單元:cypher-executor/tests/portal-auth.test.ts 20 項(fetchMock 假 KBDB host,絕不外連); cypher 全套 133/134(唯一失敗=executor「不存在的零件」pre-existingstash 複驗);kbdb 20/20 兩包 tsc exit 0。端到端:本機雙 worker(隔離 persist33/33 全綠,含搜尋隔離實測 (租戶 ns 搜 email/雜湊 0 筆+對照組 leo::portal 搜得到=非假綠)與明碼落地抽查 0 筆。
  • 驗收(=#25 驗收):新增用戶能登入;停用登入被拒+既有 session 失效;KBDB 無新表;密碼抽查非明碼
  • 工程量:中(約 1 個 CC 工作天)

P3 — /portal UI:登入殼+搜尋頁+設定頁+scope enforcedesign §1/§3.3/§5/§6)|觸碰:cypher-executor/

  • /portal HTML 殼(重用 console 樣式/搜尋 view 抽共用 helperCONSOLE_BRAND 品牌;零 Mira 字樣)
    • 實作註(2026-07-14):routes/portal-ui.ts 單檔 HTML+原生 JSconsole 同形態)。UI 重用取捨= 複製 console 樣式子集而非抽共用模組——抽共用要動 console.ts(違「console 現狀不動」鐵律), 代價=樣式雙份、改版要兩邊同步(PR 說明誠實記)。零 Mira 字樣由測試機械斷言。
  • 未登入只見登入殼;登入後兩頁:搜尋(keyword/semantic/graph 三模式+source 溯源+卡片詳頁)+設定(改密碼/看自己權限/主題)
    • graph 模式按 session graph_allowed 顯示(D-4);工作流頁 admin 額外可見(D-8, PORTAL_SHOW_WORKFLOWS 預設 admin,唯讀不開 trigger)——前端藏只是 UX,真閘在路由層。
  • /portal/data/* server-side enforcesession→record→注入 owner_idlibrary前端絕不下發租戶字串
    • routes/portal-data.ts(安全核心):caller 自帶 owner_id/library 一律被 server 值靜默覆蓋 (e2e 攻擊實測);["*"]=不注 library;HTML 殼經測試機械斷言零租戶字串/零 X-Arcrun-API-Key/不打 /kbdb/*。
  • 卡片詳頁逐筆驗 library(越庫 id 直讀 → 404
    • 雙重驗證:owner_id=CONSOLE_TENANT(防拿跨租戶 id 直讀)+library∈用戶集合(NULL→general); 越庫/跨租戶/不存在同一句 404(不洩存在性,e2e 逐字比對)。
  • graph 粗閘(D-4:無 graph 來源庫權限 → 模式不顯示+API 403)
    • 來源庫=portal_library graph_source='true' 集合(seed 補 slotensurePortalTemplates 冪等 grow 舊 template 缺的 slotsadmin libraries PATCH 可標記/取消),沒有任何標記→預設 ['general'](D-4 定案);無權 403、前端模式鈕不顯示;停用庫不算來源。
  • 測試:curl 帶 user session 直打 data API 驗 filter 繞不過(=#24 驗收 3 的 server-side 證明)
    • 單元:cypher-executor/tests/portal-data.test.ts 21 項(fetchMock 假 host,絕不外連); cypher 全套 154/155(唯一失敗=executor「不存在的零件」pre-existingstash 複驗); kbdb 20/20;兩包 tsc exit 0。端到端:本機隔離雙 worker9790/9791demo 8787/8788 不碰) 47/47 全綠——含越庫 id 直讀 404、攻擊參數被覆蓋、graph 403→授 general 後放行、 admin 全庫+跨租戶不可見、權限 PATCH 即時生效、HTML 殼斷言+inline JS node --check、 改密碼全鏈。wrangler deploy --dry-run 打包過。
  • 驗收=#24 驗收):leo21c /portal 可開;未登入只見登入殼;三模式+溯源可用;A 用戶(僅 general)搜不到 finance 內容——UI 與 curl 雙驗
    • 本機端到端已全數驗過(上項證據);leo21c 線上面=部署後驗gated wrangler 直推,B 類流程; semantic 線上路徑仍依 P1 部署清單的 Vectorize indexreindex)。
  • 工程量:大(1–1.5 個 CC 工作天,UI 是最大件)

P4 — admin 頁+工作流顯示(design §6)|觸碰:cypher-executor/

  • 帳號管理頁(admin-only nav):列表/新增/停用/重設密碼+每帳號庫權限勾選
    • 實作註(2026-07-14):portal-ui.ts 加「管理」頁(v-adminnav 只對 role=admin 顯示, 真閘=/portal/admin/* role 閘 403)。新增未帶密碼 → server generatePassword 一次性 密碼只在畫面顯示一次otpbox,關閉即逝;server 只存 hash);每帳號庫勾選含 ["*"] 全庫選項(勾選連動停用個別庫框);可勾清單=庫目錄啟用中 ∪ general ∪ 該帳號 既有授權(目錄外舊授權不憑空消失)。admin API P2 已交付不重做;本波補 last-admin 鎖死保護(總管派工明定,design 未明寫——PATCH 停用/降級「最後一個 active admin」→ 409;只在目標是 active admin 且 patch 會使其失格時多打一次 list)。
  • 庫目錄管理(登記/停用庫)
    • 同頁第二區:登記(name/display_name/description)+停用/啟用+ graph_source 圖譜來源標記(D-4,P3 已有 API,本波補 UI)。
  • 工作流頁(唯讀 list+最近執行;PORTAL_SHOW_WORKFLOWS 預設 admin;不開 trigger)
    • P3 已交付/portal/data/workflows routev-workflows 頁+D-8 閘),P4 沿用零改動。
  • 驗收:admin 全流程「發帳號→授庫→同仁登入查詢→停用」在 UI 走通;一般用戶看不到 admin 頁與(預設下)工作流頁
    • 單元:tests/portal-admin.test.ts 14 項(鎖死保護含「另一 admin 是 disabled 不算數」 「停用一般 user 不多打 list」;一次性密碼/reset 明碼不進 KBDB bodyHTML 殼加 admin 頁後 仍零租戶字串/零 /kbdb//零 API key/零 Mira)。cypher 全套 168/169(唯一失敗=executor 「不存在的零件」pre-existing);kbdb 20/20;兩包 tsc 0dry-run 打包過;inline JS node --check 過(2 段)。
    • 端到端:本機隔離雙 worker9790/9791demo 8787/8788 不碰)41/41 全綠——bootstrap→ admin 建帳號拿一次性密碼→新 user 登入→勾庫後同一 session 搜尋範圍即時變化1 筆→2 筆、 越庫 404→200)→停用 session 立即失效→重設密碼舊密 401 新密 200→last-admin 409(含降級 即時生效:原 admin session 打 admin API 變 403)→一般 user 打 admin API 403。
    • leo21c 線上面=部署後驗(gated wrangler 直推,B 類流程)。
  • 工程量:中(約 1 個 CC 工作天)

BugfixP1-P4 交付後)

  • PBKDF2 600k → 100kT6-cloud 部署抓到的框架蟲,2026-07-14,分支 fix-pbkdf2-cf-limit CF Workers 正式 runtime PBKDF2 上限 100,000 iterations600k 時 crypto.subtle.deriveBits 直接拒絕 → /portal/admin/bootstrap 真雲 500uncle6 實撞,證據 arcrun-rag docs/manual/uncle6-deploy-record.md);miniflare 無此限制=本機測試全綠假象。 修:portal-auth.ts PBKDF2_ITERATIONS 100_000+註明平台封頂;design D-5 已加修訂註。 verify 相容性:迭代數從儲存值解析,補測試「600k 舊 hash 仍通」(miniflare 造);誠實邊界 =真雲 deriveBits 同樣封頂、600k hash 真雲仍拒,但真雲 bootstrap 從未成功過 → 無存量 hash、 無遷移面。同 PR 順手:.component-builds/http_request/wrangler.tomlglobal_fetch_strictly_public flagself-hosted 同帳號 fetch 1042 正解,Arcrun#33 同族, 官方行為不變原理同 cypher 前例 rules/03 1042 段)。

  • Portal 來源回溯超連結(2026-07-18,任務層小改,分支 feat/portal-source-link 來源=leo RAG demo 客戶測試回饋「來源回溯 gitea:// 沒辦法點」) 新增可選 var PORTAL_SOURCE_WEB_BASEtypes.ts 註解含語意);portal-ui 卡片詳頁「來源回溯」 與 AI 問答出處列在「var 有設+source 為 gitea://」時渲染 <a target=_blank rel=noopener> gitea://#n → {base}/chunk 錨點捨棄、路徑逐段 encodeURIComponent、href 過 esc());未設 var 行為一字不變(Mira 零影響)。驗證:tsc 0、portal-auth/portal-data 測試 48/48 綠、渲染後 client JS new Function 語法檢查過。部署:uncle6 重部+設 var(leo 閘)。

第二波(不在本 SDD 動工範圍,掛號)

  • MCP token 綁庫集合(design §9PR#15 擴充,只動 mcp/
  • graph 逐節點/逐邊細粒度過濾
  • Google OAuth2 fast-follow、審計 log、忘記密碼 email 自助、舊資料 library 回填清潔工