SDD: RAG Portal 多人授權(#24 #25)— 純設計文件,待審不 merge #49

Merged
Leo merged 4 commits from sdd-portal-auth into main 2026-07-14 03:32:01 +00:00
4 changed files with 289 additions and 0 deletions
+1
View File
@@ -239,6 +239,7 @@ if [[ "$FILE_PATH" == *"docs/3-specs/"* ]]; then
"docs/3-specs/resumable-workflow" # richblack 確認新建(可恢復工作流)
"docs/3-specs/workflow-discovery" # 2026-06-27 總管 issue #8 交辦新建(工作流 description slot + search_workflow,北極星入口缺口)
"docs/3-specs/thin-shell-alignment" # 2026-06-27 總管 issue #11 交辦新建(CLI/MCP 薄殼漂移全面盤點 + 防複發機制)
"docs/3-specs/portal-auth" # 2026-07-13 總管 issue #24/#25 交辦新建(RAG Portal 多人授權,rag-wave1 T1/T2
)
IN_KNOWN=false
for K in "${KNOWN_SDDS[@]}"; do
@@ -0,0 +1,167 @@
# portal-auth — DesignRAG Portal 多人授權)
> 狀態:**定案**2026-07-14 總管審過+leo 裁 D-4=A/D-8=adminPR #49
> 建立:2026-07-13 | 最後更新:2026-07-14
> 對應:Gitea #24T1 Portal 拆分)+ #25T2 多用戶登入);rag-wave1 design §2/§3/§8
> 姊妹檔:requirements.md(範圍與驗收)、tasks.md(實作切分)
---
## 0. 現況地基(讀 code 核實過,設計都踩在這上面)
| 地基 | 位置 | 現況 |
|---|---|---|
| Console 單人登入 | `cypher-executor/src/routes/console-auth.ts` | 全站一組 email/password 存 KV `console:credentials`salt3 輪 SHA-256);session KV `console_sess:{token}` TTL 30 天;登入後前端拿固定租戶字串 `CONSOLE_TENANT` 直打 `/kbdb/*` |
| KBDB 萬用表 | `kbdb/migrations/0001_base.sql` | 三表(entries/templates/entry_values)永不 ALTERrecordtemplateslotsslot 值是 `entry_type='value'` 的 entry`owner_id` 隔離租戶;records API 有 create/updatePATCH/get/by-template |
| 查詢 API | `kbdb/src/routes/entries.ts``cypher-executor/src/routes/kbdb-proxy.ts` | keywordD1 LIKEowner_id/entry_type filter);semanticVectorize metadata filterowner_id/entry_type/source 已建 index);graphkbdb-graph-plugin `/graph/neighbors/:name`**無任何 scope 概念**);proxy 以 `X-Arcrun-API-Key` 當 owner_id 強制注入 |
| MCP OAuth | `mcp/OAUTH.md`PR#15 | access_tokenKV, hash key, TTL)→ 解出**綁定 namespace**owner secret 在 CF Secrets |
| 品牌/裁剪 | `console.ts` #47/#48 | `CONSOLE_BRAND` 字樣覆蓋;`CONSOLE_PROFILE=rag` 裁成 search/card/workflows/settings 四頁、落地搜尋——**是 config 裁剪不是 auth** |
| source 溯源 | `entry-crud.ts` | `metadata_json.$.source`ingest envelope URI)可 json_extract 過濾,且已進 Vectorize metadata index |
## 1. 架構總覽
```
同仁瀏覽器 ─► /portal(登入殼+搜尋+設定+admin帳號頁)─┐
│ cypher-executor(同一顆 worker
owner/導入者 ─► /consoleowner secret,現狀不動)────────┤ └─ /portal/data/* server-side enforce
客戶自家 AI ─► /mcpPR#15 OAuth,第一波不動)────────────┘ (owner_id+庫 filter 注入)
KBDB workerD1 萬用表+Vectorize
kbdb-graph-plugingraph 模式)
```
### 決策 D-1Portal 形態=cypher-executor 的 `/portal` 路由(非獨立 worker)【建議,待總管確認】
- **選項 A(建議)**:同 worker 新增 `routes/portal.ts``routes/portal-auth.ts``routes/portal-data.ts`
- 選項 B:獨立 worker。
- **理由**:Portal 查詢鐵律是「走既有 cypher/kbdb API、不開新資料路徑」——獨立 worker 得複製一整套 KBDB proxy`KBDB_INTERNAL_TOKEN` 持有面,多一個部署單位、多一個 secret 面;同 worker 重用 `kbdbBase()` 慣例零複製。企業實例本來就是「獨立部署的一套 cypher-executor」(帳號=環境模型),worker 級隔離已存在。
- Mira 實例零影響:`/portal` 端點在未 bootstrap 任何 portal_user 時只回登入殼+「尚未啟用」,不影響既有頁面。
- UI 重用:portal HTML 重用 console 的樣式系統與搜尋/卡片 view 的 render 片段(實作時抽共用 helper),但**獨立 HTML 殼**——不是在 console 上加 if,避免動到「Admin Console 現狀不動」鐵律。
## 2. 用戶模型(KBDB 萬用表,零新表)
### 2.1 template`portal_user`seed`created_by='system'`
| slot | 內容 | 備註 |
|---|---|---|
| `email` | 登入帳號(存小寫) | |
| `display_name` | 顯示名 | |
| `status` | `active` / `disabled` | 停用=翻這個 slotupdateRecord PATCHD6 慣例同 deprecate-via-status-slot |
| `role` | `user` / `admin` | admin=多「管帳號+管權限」兩件事 |
| `password_hash` | KDF 輸出(見 §4),格式 `pbkdf2-sha256$iters$salt$hash` | 自帶演算法前綴,未來換 KDF 可共存漸進遷移 |
| `libraries` | JSON array 字串,例 `["general","finance"]`;特殊值 `["*"]`=全庫 | 可查庫集合(見 §3) |
| `created_at` / `updated_at` | ISO 時間 | 萬用表 entries 本有時間欄,slot 冗餘存一份供 admin 頁直讀 |
### 2.2 決策 D-2:帳號資料的 owner_id 系統子 namespace `{tenant}::portal`【建議,重要】
- **問題(盤現況發現的真風險)**:record 的 slot 值是 `entry_type='value'` 的 entries、content 就是值本身;keyword 搜尋是 `content LIKE`、只按 owner_id 過濾(`entry-crud.ts` searchEntries 核實)。若 portal_user 存在 `owner_id=CONSOLE_TENANT` 底下,**同仁的 email、甚至密碼雜湊會出現在搜尋結果裡**。
- **解**portal_user(含 §3 的 portal_library)一律寫 `owner_id = "{CONSOLE_TENANT}::portal"`。既有租戶查詢面(`/kbdb/*`、Portal 搜尋、MCP)都以 `CONSOLE_TENANT` 過濾 → 物理上撈不到帳號資料。零 KBDB 改動,純 caller 端約定。
- 附帶盤出的既有小缺口:`record-crud.ts` updateRecord 的「grow 新 slot」路徑建 entry 時**沒帶 owner_id**line 127)——對本設計影響小(getRecord 按 record_id 取不受影響),但 P2 實作時順手補上(帶 record 既有 owner_id),避免孤兒 entry。
### 2.3 email O(1) 查找(登入用)
record 本身沒有可索引的 head——另建一筆 head entry`entry_type='portal_user'``page_name=email``content=record_id``owner_id={tenant}::portal`。登入時走既有 indexed 查詢 `GET /entries?page_name={email}&entry_type=portal_user&owner_id={tenant}::portal` 一發命中。仍是 entries 表、零新表。(不用 KV 做 email index:帳號→record 映射是長效資料,進 KV 違鐵律。)
## 3. 「庫」是什麼(本 SDD 最核心的一題)
### 3.1 現況盤點:KBDB 語境裡可當「庫」的既有維度
| 候選 | 現況 | 當庫的問題 |
|---|---|---|
| `owner_id`namespace | 租戶隔離主鍵,D1Vectorize 都有 index | 語意是「租戶」不是「庫」;一庫一 namespace 會把「跨庫搜尋」變成 N 次查詢,且 proxy 強制單一 owner_id 注入、graph/ingest/MCP 全要跟著改,既有 45 萬筆資料要搬家 |
| `metadata_json.$.source` | ingest envelope 的檔案級 URI`logseq://vault/foo.md`),D1 json_extractVectorize index 都可過濾 | 粒度是「單檔」不是「庫」;Vectorize filter 是等值比對,做不了前綴匹配 |
| `page_name` / `tags_json` | D1 可查 | 語意已被佔用(頁名/標籤),Vectorize 沒 index |
| `parent_id`project 樹) | project→workflow 樹 | 是任務樹不是知識分區 |
**結論:現況沒有現成的「庫」維度**——source 太細、owner_id 太粗。庫是新概念,但可以不動 schema 地長出來。
### 3.2 決策 D-3:庫=`metadata_json.$.library`ingest 蓋章的一級 metadata 欄位)【建議,待總管/leo 裁——本 SDD 最大單一決策】
- **定義**:庫=知識條目上的 `library` 標記(如 `general`/`finance`/`hr`),**ingest 寫入時蓋章**ingest 端按「收集資料夾/repo → 庫名」的對照表蓋;對照表是 arcrun-rag 包的 ingest config,不歸本 repo)。
- **儲存**`metadata_json.$.library`——json_extract 可查(同 source 的 #5.1 先例),**表不變**。
- **semantic**Vectorize upsert metadata 加 `library` 欄+建 metadata index(同 owner_id/source 先例)。既有向量要重推才進 indexembed.ts line 117 已知特性)——用既有 `reindex` backfill 機制補。
- **庫目錄(admin 頁要列庫)**template `portal_library`slots`name`/`display_name`/`description`/`status`),owner_id 同 `{tenant}::portal`。零新表。
- **未蓋章的舊資料**:視同 `library="general"`(查詢端 fallbackjson_extract 為 NULL → 歸 general)。回填 job 非必要(fallback 已覆蓋),可列第二波清潔工。
- **放棄的選項**owner_id 子 namespace 當庫(3.1 表列問題);source 前綴當庫(Vectorize 做不了前綴)。
### 3.3 查詢怎麼 enforceserver-side,非前端藏)
- **KBDB base 擴充**P1):`/entries/search``/entries``library` 參數(**可多值**,逗號分隔):
- keywordD1):`(json_extract(metadata_json,'$.library') IN (?,…) OR (json_extract(metadata_json,'$.library') IS NULL AND 'general' IN (?,…)))`
- semanticVectorize):filter `{ library: { $in: [...] } }`Vectorize v2 metadata filter 支援 `$in`**實作時以 wrangler/文件核實**,若版本不支援 → fallback:每庫一次 topK 查詢、按 score 合併——庫數小,成本有界)
- **Portal 查詢面**P3):前端**只打 `/portal/data/*`**server 端從 session→user record 取 `libraries`,注入 `owner_id=CONSOLE_TENANT``library=<集合>` 後轉發 KBDB。
- **關鍵差異(vs 現行 console**console 登入後把 `CONSOLE_TENANT` 交給前端當 API key 直打 `/kbdb/*`**Portal 絕不下發租戶字串**——portal_user 拿到租戶字串就能繞過庫 filter 直打 `/kbdb/search`。前端只有 portal session tokenenforce 全在 server。
- `["*"]`(全庫)=不注入 library filter,只注入 owner_id。
### 3.4 graph 模式的庫權限(已知薄弱點,明寫)
kbdb-graph-plugin 的 `/graph/neighbors/:name` 只吃節點名,**無 owner/庫概念**(proxy 也只驗有 key)。三元組是跨文件萃取的衍生物,逐邊回查來源 entry 的 library=N+1 且語意模糊(一條邊可能來自多庫文件)。
- **決策 D-4(定案:A 粗閘,leo 2026-07-14**graph 模式=**粗閘**——只對「擁有 graph 來源庫權限」的用戶開放(portal_library 標記哪個庫是圖譜來源,預設 `general`;無權者搜尋頁不顯示 graph 模式、`/portal/data/graph/*` 回 403)。逐節點/逐邊細粒度過濾列第二波。已知殘餘(誠實記):客戶把機密文件餵進圖萃取時,粗閘擋不住節點名本身——導入文件要寫明「機密庫預設不進圖萃取來源」。
## 4. 密碼與登入
### 4.1 決策 D-5KDFPBKDF2-SHA256WebCrypto 原生,600k 迭代)【建議;issue 字面寫 bcrypt/argon2,偏離需總管點頭】
| 選項 | 評估 |
|---|---|
| **PBKDF2-SHA256 600k iters(建議)** | Workers `crypto.subtle.deriveBits` **原生**、零依賴;OWASP 現行建議值;console-auth 已有「登入雜湊寫在 cypher TS」先例(rule 2.2 限的是 workflow credential 原語,不是 console/portal 登入) |
| argon2id | 記憶體硬化更強,但 Workers 無原生,要引 WASM 模組(新依賴+冷啟成本);Workers 128MB 記憶體下參數也開不大 |
| bcrypt | 純 JS 實作燒 CPUWorkers CPU limit),無原生 |
- 格式 `pbkdf2-sha256$600000$<salt_b64>$<hash_b64>``password_hash` slot(見 D-6);驗證用常數時間比對(同 PR#15 慣例)。
- 順帶:**不動 console-auth 既有 3 輪 SHA-256**Admin Console 現狀不動鐵律);portal 是新面、直接用對的。
### 4.2 決策 D-6:雜湊放 slotKBDB),不放 CF Secrets【建議=定案傾向強】
- CF Secrets 是**部署期靜態**`wrangler secret put`),admin 在 runtime CRUD 用戶時**物理上寫不進去**——多用戶動態資料放 Secrets 不可行。
- KV 放長效帳號資料違「KV 只留暫存」。
- slotKBDB D1 長效儲存,符合三層 by 用途(credential-storage-three-tier:長期→D1)。雜湊非明碼、不可逆;外洩風險靠強 KDF+D-2 子 namespace(搜不到)緩解。
### 4.3 登入/Session 流程
- `POST /portal/login {email,password}` → head entry 查 record → status=active → KDF 驗證 → 發 `portal_sess:{token}`KV `SESSIONS_KV`,值=`{record_id}`TTL `PORTAL_SESSION_TTL` 預設 **7 天**;issue 要求「短效」,比 console 30 天緊)。
- **每個 `/portal/data/*` 請求都回讀 user record**session 只存 record_id,權限/狀態以 record 為唯一真相源)→ **停用/改權限即時生效**,不需 session 反向索引、不需等 TTL。停用時 admin API 同時盡力刪已知 sessionbest-effort),但正確性不依賴它。
- 登入失敗節流(建議含,可裁):KV 計數 `portal_lockfail:{email}`,5 次失敗鎖 15 分鐘(TTL 自然過期)。公開登入面的最低保險,工程量半天內。
- 改自己密碼:`POST /portal/me/password {current,new}`(驗舊密);admin 重設他人:`POST /portal/admin/users/:id/reset-password`(回一次性新密碼,要求首登改密=slot 加 `must_change:true`——**可裁**,第一波可簡化為 admin 口頭轉交新密碼)。
### 4.4 決策 D-7ownerconsole secret)與 portal admin 的關係=**並存**【建議】
- owner**超級管理員**Admin Consoleowner secret,機器/部署層);portal admin=**業務管理員**(管同仁帳號與庫權限)。不取代:Admin Console 現狀不動是 #24 鐵律,且 owner secret 是安裝期人閘、portal admin 是 runtime 角色,層次不同。
- **Bootstrap**`POST /portal/admin/bootstrap` 需帶 **console owner session**(重用 console-auth 的 `validateConsoleSession`)→ 建第一個 role=admin 的 portal_user。不引入新 secret、不開放無閘註冊。安裝器(rag-wave1 §6)把這步寫進一條龍。
## 5. API 面(全掛 cypher-executor`/portal` 前綴)
| Method/Path | auth | 作用 |
|---|---|---|
| `GET /portal` | 無(回登入殼) | Portal HTMLbrand`CONSOLE_BRAND` |
| `POST /portal/login` / `POST /portal/logout` | — / session | 登入/登出 |
| `GET /portal/session` | session | 驗 session+回 `{display_name, role, libraries}`(前端據此渲染;**不回租戶字串**) |
| `POST /portal/me/password` | session | 改自己密碼 |
| `GET /portal/data/search?q=&mode=` | session | 三模式查詢(server 注入 owner_idlibrary;回應含 source 溯源欄) |
| `GET /portal/data/entries/:id` | session | 卡片詳頁(**逐筆驗 library**entry 的 library 不在用戶集合 → 404——防拿 id 直讀越庫) |
| `GET /portal/data/graph/neighbors/:name` | sessionD-4 粗閘 | graph 模式 |
| `GET /portal/data/workflows` | session(見 D-8 | 工作流顯示(唯讀 list+最近執行;**不開 trigger** |
| `POST /portal/admin/bootstrap` | console owner session | 建第一個 admin |
| `GET/POST /portal/admin/users``PATCH /portal/admin/users/:id`status/role/libraries/reset-password | sessionrole=admin | 帳號 CRUD+權限 |
| `GET/POST/PATCH /portal/admin/libraries` | sessionrole=admin | 庫目錄登記 |
## 6. 頁面(leo 2026-07-13 頁面級拍板為綱)
- **一般用戶=兩頁**:搜尋頁(三模式切換+結果 source 溯源+卡片詳頁)+設定頁(改密碼、看自己的角色與可查庫、主題切換)。
- **admin 多一頁**:帳號管理(同仁列表/新增/停用/重設密碼+每帳號勾選可查庫)+庫目錄管理。掛 Portal 內 admin-only nav 項(**非** console settings 擴充——console 不動鐵律;且受眾不同:console 是 owner、這頁是 portal admin)。
- **決策 D-8(定案:adminleo 2026-07-14):工作流頁 admin 可見、一般用戶不顯示**——「工作流頁可以顯示(誠實系統狀態)」與「一般用戶只要兩頁」取交集;要改全員可見=config `PORTAL_SHOW_WORKFLOWS=all` 一行。唯讀、不開 triggertrigger 是 owner/console 的事)。
- Mira 專屬頁(駕駛艙/分流台/專案管理/自動開發)**一律不進**。
## 7. 設定與部署
- `[vars]``PORTAL_SESSION_TTL`(預設 604800)、`PORTAL_SHOW_WORKFLOWS``admin`/`all`/`off`,預設 admin);沿用 `CONSOLE_TENANT`/`CONSOLE_BRAND`。無新 secretbootstrap 走 console owner session)。
- 部署:gated leo21c wrangler 直推(B 類流程);Vectorize `library` metadata index 建立+`reindex` backfill 是部署步驟(寫進 PR 的部署清單與 rag-wave1 安裝器)。
- Mira 實例:不 bootstrap 即不啟用;`CONSOLE_PROFILE` 與 Portal 互不干涉(#48 是 console 的裁剪、本 SDD 是新面)。
## 8. 不做清單(第二波以後,明寫)
SSO/AD/SAML、Google OAuth2fast-follow)、per-user MCP token、審計 log、忘記密碼 email 自助、graph 逐節點細粒度過濾、舊資料 library 回填 job(fallback 已覆蓋)、密碼策略進階(複雜度規則/輪換)。
## 9. MCP 第二波預告(只預告,不實作)
PR#15 的 access_token KV record 現存「綁定 namespace」→ 擴成 `{namespace, libraries}``/authorize` 同意頁讓 owner 勾庫集合;partner-auth 解 token 後把 libraries 帶進 MCP 工具的 KBDB 呼叫(同 §3.3 filter)。P1 的 KBDB library filter 地基即為此鋪路——第二波只動 mcp/,不再動 kbdb。
## 10. 風險
| 風險 | 緩解 |
|---|---|
| Vectorize `$in` filter 版本支援不確定 | P1 第一件事實測;fallback 每庫 fan-out 合併(設計已含) |
| graph 模式洩漏機密庫節點名 | D-4 粗閘+可收緊成 admin-only;第二波細粒度 |
| 既有向量不重推就 filter 不到(embed.ts 已知特性) | 部署清單強制 reindex backfill+驗收抽查 semantic 命中 |
| Portal 時程拖住 pilot | requirements 降級方案(rag profile+共享唯讀連結+IP 白名單),啟用回 leo 裁 |
| 帳號資料進搜尋結果 | D-2 子 namespace(物理隔離)+P2 驗收明測「搜 email 搜不到」 |
@@ -0,0 +1,62 @@
# portal-auth — RequirementsRAG Portal 多人授權)
> 狀態:草稿(待總管/leo 審)
> 建立:2026-07-13 | 最後更新:2026-07-13
> 對應交辦:Gitea `Leo/Arcrun` **#24**Portal 拆分,rag-wave1 T1)+ **#25**(多用戶+登入+sessionrag-wave1 T2
> 需求上游:`Leo/arcrun-rag` `system-dev/docs/3-specs/rag-wave1/design.md` §2/§3/§8issue 留言 191/192/194/195leo 2026-07-12、07-13 兩輪拍板)
---
## 一句話說明
企業 RAG 實例的 **Arcrun RAG Portal**:多人帳號(email+密碼登入)+庫級查詢權限(每帳號綁「可查哪些庫」,server-side enforce)+admin 管帳號管權限——全部用 Arcrun 既有機制(KBDB 萬用表、KV TTL session、既有查詢 API),零新 D1 表。
## 背景
- 現行 console 是「單一 owner 帳密」(console-auth.ts:全站一組 email/password,登入只為擋外人看頁面,後端仍用固定租戶字串 `CONSOLE_TENANT` 打 KBDB)。企業產品要發帳號給多位同仁,且財務/機密庫非人人可查——單人模型不夠。
- #47/#48 已完成品牌與頁面裁剪地基(`CONSOLE_BRAND` 覆蓋字樣、`CONSOLE_PROFILE=rag` 裁成搜尋落地 4 頁),但那是「config 裁剪」不是授權——本 SDD 補上多人授權。
## User Stories
- **US1(一般用戶)**:我用 admin 發給我的 email+密碼登入 Portal,只看得到兩頁:**搜尋頁**keyword/semantic/graph 三模式+來源溯源)+**設定頁**(改自己密碼、看自己權限)。我只搜得到「我被授權的庫」的內容。
- **US2admin**:我除了 US1 的兩頁,多「帳號管理」:新增/停用/重設同仁帳號、設定每個帳號可查哪些庫。停用立即生效(該用戶既有 session 失效)。
- **US3owner/導入者)**Admin Consoleowner secret)照舊不動;我能用 owner 身份 bootstrap 第一個 Portal admin。
- **US4(第二波預告,不實作)**:客戶自家 AI 走 MCP 查知識庫時,token 也綁庫集合(PR#15 地基擴充)。
## 驗收標準(合併 #24/#25 驗收)
1. leo21c(或企業實例)上 `/portal` 可開;未登入只見登入殼。
2. 三模式查詢(keyword/semantic/graph)與結果來源溯源可用;頁面零 Mira 字樣(Arcrun 品牌,`CONSOLE_BRAND` 可覆蓋)。
3. admin 新增的用戶能登入查詢;**只查得到被授權的庫**server-side filter 驗證:直接 curl Portal 查詢 API 帶該用戶 session 也繞不過,不是前端藏)。
4. 停用用戶登入被拒,且既有 session 立即失效。
5. KBDB **無新 D1 表**(D6 萬用表鐵律);密碼抽查非明碼(雜湊)。
6. Admin Consoleowner secret)行為一字不變;Mira 實例(`CONSOLE_PROFILE` 未設)零影響。
## 範圍
### In Scope(第一波)
- portal_user 用戶模型(KBDB 萬用表)+帳號狀態(active/disabled)+角色(user/admin)。
- email+密碼登入、KV TTL session、登出、改密碼。
- 「庫」的定義與登記(見 design §3)+每帳號綁可查庫集合+查詢 API server-side enforce。
- Portal 頁面:登入殼、搜尋頁(三模式+溯源)、設定頁;admin 加帳號管理頁;工作流頁顯示(見 design §6 裁量)。
- owner bootstrap 第一個 admin 的流程。
### Out of Scope(明寫不做,第二波以後)
- **SSO / AD / SAML**。
- **Google OAuth2 登入**fast-follow,不進第一波)。
- **per-user MCP tokenMCP 庫級 scope**(第二波;design §9 只留擴充預告)。
- **審計 log**(誰查了什麼)。
- 忘記密碼 email 自助重設(第一波由 admin 重設)。
- Mira 生活面頁(駕駛艙、分流台、專案管理、自動開發)——**一律不進企業版**(leo 2026-07-13 拍板)。
- 私人筆記(Portal 只顯示公司公用庫,rag-wave1 §-1.2 硬切割)。
## 鐵律(上游規定,本 SDD 不越)
- 查詢一律走既有 cypher/kbdb API**不開新資料路徑**。
- **零新 D1 表**D6);session/暫存才准 KV;密碼永不明碼。
- Admin Console 現狀不動——拆分是「加一個面」不是重構。
- B 類平台維護流程:本 SDD 總管審 → 實作 PR → component-pr-review-standard 逐條 → 總管 review → gated 部署 leo21c。
## 風險與降級(rag-wave1 §8 對接)
- Portal/認證是框架級新面積=第一波最大單件。**降級方案**:若 pilot 時程被拖住(判準:pilot 日前 P2/P3 未過總管審),先用「`CONSOLE_PROFILE=rag` console+共享唯讀連結(單一 token)+Cloudflare WAF IP 白名單」頂替登入,用戶管理延後——**啟用與否回 leo 裁**。
@@ -0,0 +1,59 @@
# 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
- [ ] `/entries/search``/entries``library` 多值參數(D1 json_extract INNULL→general fallback
- [ ] embed upsert metadata 加 `library` 欄;semanticSearch filter 支援 library$in 或 fan-out
- [ ] Vectorize `library` metadata index 建立步驟+reindex backfill 寫進部署清單
- [ ] cypher `kbdb-proxy` 透傳 `library` 參數(供 owner/admin 面用;portal 面走 P3 的注入,不經這)
- [ ] 測試:D1 filter 單元測、NULL fallback、多值、semantic filtermock VECTORIZE
- **驗收**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_user``portal_library`init-seed 慣例)
- [ ] PBKDF2-SHA256 雜湊模組(600k iters、常數時間比對、`pbkdf2-sha256$…` 格式)
- [ ] `POST /portal/admin/bootstrap`console owner session 閘)
- [ ] login/logout/session/改密碼(KV `portal_sess:`、TTL var、每請求回讀 record 驗 status
- [ ] admin 用戶 CRUDlibraries 授權+庫目錄 CRUDrole=admin 閘)
- [ ] 登入失敗節流(5 次/15 分鐘,KV TTL
- [ ] 順手修 `record-crud.ts` updateRecord grow 路徑漏 owner_iddesign §2.2 附帶)
- [ ] 測試:bootstrap 閘、登入對錯、停用即拒(session 立即失效)、role 閘、雜湊格式、`{tenant}::portal` 子 namespace 隔離(**搜 email 搜不到**
- **驗收**=#25 驗收):新增用戶能登入;停用登入被拒+既有 session 失效;KBDB 無新表;密碼抽查非明碼
- **工程量**:中(約 1 個 CC 工作天)
## P3 — `/portal` UI:登入殼+搜尋頁+設定頁+scope enforcedesign §1/§3.3/§5/§6)|觸碰:`cypher-executor/`
- [ ] `/portal` HTML 殼(重用 console 樣式/搜尋 view 抽共用 helper`CONSOLE_BRAND` 品牌;零 Mira 字樣)
- [ ] 未登入只見登入殼;登入後兩頁:搜尋(keyword/semantic/graph 三模式+source 溯源+卡片詳頁)+設定(改密碼/看自己權限/主題)
- [ ] `/portal/data/*` server-side enforcesession→record→注入 `owner_id``library`**前端絕不下發租戶字串**
- [ ] 卡片詳頁逐筆驗 library(越庫 id 直讀 → 404
- [ ] graph 粗閘(D-4:無 graph 來源庫權限 → 模式不顯示+API 403)
- [ ] 測試:curl 帶 user session 直打 data API 驗 filter 繞不過(=#24 驗收 3 的 server-side 證明)
- **驗收**=#24 驗收):leo21c `/portal` 可開;未登入只見登入殼;三模式+溯源可用;A 用戶(僅 general)搜不到 finance 內容——UI 與 curl 雙驗
- **工程量**:大(1–1.5 個 CC 工作天,UI 是最大件)
## P4 — admin 頁+工作流顯示(design §6)|觸碰:`cypher-executor/`
- [ ] 帳號管理頁(admin-only nav):列表/新增/停用/重設密碼+每帳號庫權限勾選
- [ ] 庫目錄管理(登記/停用庫)
- [ ] 工作流頁(唯讀 list+最近執行;`PORTAL_SHOW_WORKFLOWS` 預設 admin;不開 trigger
- **驗收**:admin 全流程「發帳號→授庫→同仁登入查詢→停用」在 UI 走通;一般用戶看不到 admin 頁與(預設下)工作流頁
- **工程量**:中(約 1 個 CC 工作天)
---
## 第二波(不在本 SDD 動工範圍,掛號)
- MCP token 綁庫集合(design §9PR#15 擴充,只動 `mcp/`
- graph 逐節點/逐邊細粒度過濾
- Google OAuth2 fast-follow、審計 log、忘記密碼 email 自助、舊資料 library 回填清潔工