t181 修正:/portal/daemon/extract 認證改用 X-Arcrun-API-Key(帳密行不通)

【我自己的設計錯誤】前一版用帳密認證,但查證 daemon 實際行為後發現行不通:
**密碼只在連線精靈當下用過就丟、不落地**(config.json 沒有密碼欄,刻意的安全設計,
wiki 記為「密碼零落地」),而背景萃取是每輪自動跑的 ⇒ 根本拿不到密碼。

【改法】沿用 daemon 送卡片上雲時本來就帶的 `X-Arcrun-API-Key`(=namespace,
collector/direct.go:355)⇒ 同一把憑證、同一個身份模型,不必為此新增任何儲存。
不符即 401(租戶隔離)。

測試四則全過(沒帶 key 401/key 錯 401/缺參數 400/
**回歸守衛:錯誤訊息不得出現 gemini_api_key 或 credential**)。
全檔 38 passed,唯一 failed 與 tsc 的 1190 行 'auto' 皆為基準線既有、與本次無關。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-04 14:37:16 +08:00
parent d728071a6a
commit 60f481938d
2 changed files with 26 additions and 45 deletions
+17 -31
View File
@@ -820,46 +820,32 @@ describe('/portal/admin/ai + /portal/daemon/report-capabilitiest131', () =
// 「52 檔全滅還要把金鑰傳給別人才查得出原因」三種災難。
describe('POST /portal/daemon/extractt181Workers AI 萃卡,免金鑰)', () => {
const USER_EMAIL = 'daemon@example.com';
const USER_PW = 'unit-test-pw-1';
const USER_RECORD = 'rec_daemon_extract';
// 認證=X-Arcrun-API-Key(=namespacewrangler.test.toml CONSOLE_TENANT=leo),
// **不是帳密**:daemon 密碼不落地(連線精靈用完即丟),背景萃取拿不到密碼。
const KEY = { 'X-Arcrun-API-Key': 'leo' };
function mockEmailLookup(email: string, recordId: string | null) {
const needle = new URLSearchParams({ page_name: email }).toString();
fetchMock
.get(KBDB)
.intercept({
path: (p: string) => p.startsWith('/entries?') && p.includes(needle) && p.includes(encodeURIComponent(NS)),
method: 'GET',
})
.reply(200, { success: true, entries: recordId ? [{ content: recordId }] : [], count: recordId ? 1 : 0 });
}
it('沒帶 API Key → 401', async () => {
const res = await json('POST', '/portal/daemon/extract', { page_name: 'x', text: 'y' });
expect(res.status).toBe(401);
});
it('API Key 錯 → 401(租戶隔離)', async () => {
const res = await json('POST', '/portal/daemon/extract',
{ page_name: 'x', text: 'y' }, { 'X-Arcrun-API-Key': 'someone-else' });
expect(res.status).toBe(401);
});
it('缺 page_name 或 text → 400(不打 AI、不假裝成功)', async () => {
const res = await json('POST', '/portal/daemon/extract', { email: USER_EMAIL, password: USER_PW });
const res = await json('POST', '/portal/daemon/extract', {}, KEY);
expect(res.status).toBe(400);
const d = (await res.json()) as { error?: string };
expect(String(d.error)).toContain('page_name');
});
it('缺帳密 → 400', async () => {
const res = await json('POST', '/portal/daemon/extract', { page_name: 'x', text: 'y' });
expect(res.status).toBe(400);
});
it('帳密錯 → 401(認證與 daemon/config 同一把)', async () => {
mockEmailLookup(USER_EMAIL, USER_RECORD);
mockGetRecord(USER_RECORD, adminValues({ email: USER_EMAIL, password_hash: storedHash }));
const res = await json('POST', '/portal/daemon/extract', {
email: USER_EMAIL, password: 'wrong-password', page_name: 'x', text: 'y',
});
expect(res.status).toBe(401);
});
// 🔴 回歸守衛:這條路**不得**要求任何 GeminiAPI 金鑰。
// 若哪天有人把它改回打 Google,這則會因為錯誤訊息提到 credential/gemini 而紅。
// 🔴 回歸守衛:這條路**不得**要求任何 Gemini/API 金鑰——免金鑰正是它存在的理由。
// 若哪天有人把它改回打 Google,錯誤訊息會出現 credential/gemini_api_key ⇒ 這則會紅。
it('錯誤訊息不得要求任何金鑰(免金鑰是本端點存在的理由)', async () => {
const res = await json('POST', '/portal/daemon/extract', { email: USER_EMAIL, password: USER_PW });
const res = await json('POST', '/portal/daemon/extract', {}, KEY);
const raw = await res.text();
expect(raw).not.toContain('gemini_api_key');
expect(raw).not.toContain('credential');