ed2e42e007
D19「擁有目錄不擁有內容物」遷移的讀取斷點。密文住 cypher per-script secrets、 D1 只存 secret_ref、auth worker 讀不到 cypher 的 secrets → cypher 先取值塞 payload。 WASM 端(T7 fallback 骨架,零行為改變可獨立部署驗): - auth_static_key/main.go + auth_service_account/main.go:Input 加 resolved_secrets, 解密處改「有 resolved 就用、沒有才 fallback 舊 kv_get+crypto_decrypt」。default 等於舊碼。 - tinygo build 兩支通過,copy 到 .component-builds/*/component.wasm。 TS 端(T6 主路徑): - auth-dispatcher.ts 新增 resolveSecretsFromNewHome:查 D1 拿 secret_ref → secret_get(env[ref], T4) 取明文 → 組 map(取不到的 name 缺席,不放空字串) → 更新 last_used_at。tryAuthDispatch + resolveCredentialRefs 都塞 resolved_secrets。 - rule 02 §2.2 對齊:只查 ref/取值/塞字串,不解密不展開模板不組 JWT。 驗證:cypher-executor + cli tsc exit 0;vitest 41/42(新增 auth-dispatcher.test.ts 6 案例全過, 剩 1 pre-existing 無關失敗)。待 leo21c 部署驗 WASM 端到端(不由本任務部署,acr update 硬綁 GitHub codeload=mistakes #23)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
/**
|
||
* credential-store-migration T6/T7(方案 A,D19)測試:auth-dispatcher 的
|
||
* `resolveSecretsFromNewHome`——「查 D1 拿 secret_ref → secret_get(env[ref]) 取值 → 組 map」。
|
||
*
|
||
* 覆蓋 T6 主路徑 + T7 雙讀分流的判準:
|
||
* - D1 有 ref 且 secret 存在(新家有值)→ name 進 map(payload 會含 resolved_secrets[name])
|
||
* - D1 無 ref(從未回填)→ name 缺席 map(讓 WASM fallback 舊 KV,T7)
|
||
* - D1 有 ref 但 secret 不存在(新家還沒寫值)→ secret_get 回 null → name 缺席(不放空字串!)
|
||
* - 取到值的 name 更新 last_used_at
|
||
*
|
||
* secret_get 的實作是 host function `env[ref]`(wasi-shim),故測試把 CRED_* 值放進傳入的
|
||
* env 物件模擬「per-script secret 已注入」,用真實 D1 binding 存目錄(同 credentials.test.ts)。
|
||
*/
|
||
import { describe, it, expect, beforeEach } from 'vitest';
|
||
import { env } from 'cloudflare:test';
|
||
import type { Bindings } from '../src/types';
|
||
import { resolveSecretsFromNewHome } from '../src/actions/auth-dispatcher';
|
||
|
||
const API_KEY = 'test-tenant-t67';
|
||
|
||
async function insertRefRow(name: string, secretRef: string): Promise<void> {
|
||
await env.CREDENTIALS_DB
|
||
.prepare(
|
||
`INSERT INTO credentials (api_key, name, service, sensitivity, secret_ref, created_at, last_used_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, NULL)`,
|
||
)
|
||
.bind(API_KEY, name, null, 'standard', secretRef, Math.floor(Date.now() / 1000))
|
||
.run();
|
||
}
|
||
|
||
async function clearRows(): Promise<void> {
|
||
await env.CREDENTIALS_DB.prepare(`DELETE FROM credentials WHERE api_key = ?`).bind(API_KEY).run();
|
||
}
|
||
|
||
/** 把 CRED_* secret 值疊到真實 env 上(模擬 per-script secret 已注入 runtime)。 */
|
||
function envWithSecrets(secrets: Record<string, string>): Bindings {
|
||
return { ...(env as unknown as Record<string, unknown>), ...secrets } as unknown as Bindings;
|
||
}
|
||
|
||
describe('resolveSecretsFromNewHome (T6/T7 方案 A)', () => {
|
||
beforeEach(clearRows);
|
||
|
||
it('D1 有 ref 且新家有值 → name 進 map(T6 命中新家)', async () => {
|
||
await insertRefRow('openai_key', 'CRED_OPENAI_KEY_ABCDEF01');
|
||
const testEnv = envWithSecrets({ CRED_OPENAI_KEY_ABCDEF01: 'sk-live-123' });
|
||
|
||
const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['openai_key']);
|
||
|
||
expect(resolved).toEqual({ openai_key: 'sk-live-123' });
|
||
});
|
||
|
||
it('D1 無 ref(從未回填)→ name 缺席 map(走 WASM fallback 舊 KV,T7)', async () => {
|
||
// D1 沒有這個 name 的 row
|
||
const testEnv = envWithSecrets({ CRED_ANYTHING: 'unused' });
|
||
|
||
const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['never_migrated']);
|
||
|
||
expect(resolved).not.toHaveProperty('never_migrated');
|
||
expect(resolved).toEqual({});
|
||
});
|
||
|
||
it('D1 有 ref 但新家沒值 → secret_get 回 null → name 缺席(不放空字串)', async () => {
|
||
await insertRefRow('notion_token', 'CRED_NOTION_TOKEN_ABCDEF01');
|
||
// env 裡沒有 CRED_NOTION_TOKEN_ABCDEF01 → env[ref] 為 undefined → secret_get 回 null
|
||
const testEnv = envWithSecrets({});
|
||
|
||
const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['notion_token']);
|
||
|
||
// 關鍵:缺席,不是 { notion_token: '' }(空字串會讓 WASM 誤判命中用空值)
|
||
expect(resolved).not.toHaveProperty('notion_token');
|
||
expect(resolved).toEqual({});
|
||
});
|
||
|
||
it('混合:一把有值一把沒 ref → 只有有值的進 map', async () => {
|
||
await insertRefRow('has_value', 'CRED_HAS_VALUE_ABCDEF01');
|
||
const testEnv = envWithSecrets({ CRED_HAS_VALUE_ABCDEF01: 'the-value' });
|
||
|
||
const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['has_value', 'no_ref']);
|
||
|
||
expect(resolved).toEqual({ has_value: 'the-value' });
|
||
});
|
||
|
||
it('空 names → 空 map(零開銷)', async () => {
|
||
const resolved = await resolveSecretsFromNewHome(envWithSecrets({}), API_KEY, []);
|
||
expect(resolved).toEqual({});
|
||
});
|
||
|
||
it('取到值後更新 last_used_at(治理面)', async () => {
|
||
await insertRefRow('used_key', 'CRED_USED_KEY_ABCDEF01');
|
||
const before = await env.CREDENTIALS_DB
|
||
.prepare(`SELECT last_used_at FROM credentials WHERE api_key = ? AND name = ?`)
|
||
.bind(API_KEY, 'used_key')
|
||
.first<{ last_used_at: number | null }>();
|
||
expect(before?.last_used_at).toBeNull();
|
||
|
||
const testEnv = envWithSecrets({ CRED_USED_KEY_ABCDEF01: 'v' });
|
||
await resolveSecretsFromNewHome(testEnv, API_KEY, ['used_key']);
|
||
|
||
const after = await env.CREDENTIALS_DB
|
||
.prepare(`SELECT last_used_at FROM credentials WHERE api_key = ? AND name = ?`)
|
||
.bind(API_KEY, 'used_key')
|
||
.first<{ last_used_at: number | null }>();
|
||
expect(typeof after?.last_used_at).toBe('number');
|
||
});
|
||
});
|