feat(credential-store-migration): T5 寫入路徑改走 Workers Secrets + D1 ref
POST/PUT /credentials 改寫:密文值 PUT 進 CF Workers per-script Secrets (唯寫,D19 不持有內容物),D1 credentials 表只存目錄(不含密文)。 secret_ref = CRED_<NAME>_<sha256(api_key)[:8]>,避免跨租戶撞名。 DELETE/GET /credentials 本次不動(仍走舊 KV,T9 範圍),已誠實標注。 新增 CREDENTIALS_DB D1 binding(與 KBDB base 共用同一顆 arcrun-kbdb, 沿用既有 database_id 注入機制)+ CF_SECRETS_API_TOKEN/CF_ACCOUNT_ID env vars(CF_ACCOUNT_ID 由 deploy.ts 自動注入,同 WORKER_SUBDOMAIN 模式)。 §2.4 選項甲落地:client 不再加密,明文值經 TLS 傳輸,cypher 短暫經手 明文不落地不持金鑰——與舊版 01-tech-stack.md 傳輸格式不同,是本 SDD 對舊格式的刻意取代(§6 Q-b 仍需 leo 明確接受)。 驗證:tsc --noEmit exit 0;vitest 26/27(1 pre-existing 無關失敗)。 端到端:真實部署 leo21c arcrun-cypher-executor,POST/PUT /credentials 成功寫入 Workers Secrets + D1 row(CF API + D1 query 雙重確認), 清理測試資料後 restore wrangler.toml(無帳號專屬 id 殘留 git)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018D6QoC5waFkcjc2N7csJBB
This commit is contained in:
@@ -462,6 +462,16 @@ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
// credential-store-migration T3:CF_ACCOUNT_ID(vars,非機密識別碼)換成用戶自己的帳號 id,
|
||||
// 比照 WORKER_SUBDOMAIN 注入同一套機制。cypher 寫入 /credentials 時要用它組
|
||||
// CF Workers Scripts secrets 管理 API URL(見 routes/credentials.ts)。
|
||||
if (ctx.accountId && /CF_ACCOUNT_ID/.test(toml)) {
|
||||
toml = toml.replace(
|
||||
/(CF_ACCOUNT_ID\s*=\s*")[^"]*(")/,
|
||||
`$1${ctx.accountId}$2`,
|
||||
);
|
||||
}
|
||||
|
||||
// KBDB Base: inject user's D1 database_id into [[d1_databases]] (placeholder in repo toml)
|
||||
if (ctx.d1DatabaseId && /database_id\s*=/.test(toml)) {
|
||||
toml = toml.replace(
|
||||
|
||||
@@ -24,6 +24,9 @@ export function isRecipeHash(id: string): boolean {
|
||||
return /^rec_[0-9a-f]{8}$/.test(id);
|
||||
}
|
||||
|
||||
/** 匯出給 credential-store-migration T5 用(secret_ref 命名衍生租戶隔離 hash,非解密/簽章邏輯)。 */
|
||||
export { sha256Prefix };
|
||||
|
||||
async function sha256Prefix(input: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(input);
|
||||
const buf = await crypto.subtle.digest('SHA-256', data);
|
||||
|
||||
@@ -1,56 +1,197 @@
|
||||
/**
|
||||
* Credentials API — 多租戶 credential 管理
|
||||
*
|
||||
* POST /credentials
|
||||
* Body: { name: string, encrypted: string, iv: string }
|
||||
* Header: X-Arcrun-API-Key
|
||||
* → 以 {api_key}:cred:{name} 為 KV key 存入 CREDENTIALS_KV
|
||||
* credential-store-migration T5(D19「擁有目錄,不擁有內容物」,
|
||||
* system-dev/docs/3-specs/arcrun/credential-primitives-wasm/credential-store-migration.md §2.3-2.4/§3):
|
||||
*
|
||||
* DELETE /credentials/:name
|
||||
* Header: X-Arcrun-API-Key
|
||||
* → 刪除 {api_key}:cred:{name}
|
||||
* 新寫入(POST 建立 / PUT 覆寫)一律走新家:
|
||||
* 1. 密文值 PUT 進 CF Workers per-script Secrets(掛在本 worker 上,管理 API 唯寫,
|
||||
* arcrun 自己也讀不回值——D19「不持有內容物」)。
|
||||
* 2. D1 `credentials` 表只寫「目錄」(api_key/name/service/sensitivity/secret_ref/
|
||||
* created_at),**不含密文**。
|
||||
* 不再寫 KV / 不再寫明文密文到 D1。
|
||||
*
|
||||
* GET /credentials
|
||||
* Header: X-Arcrun-API-Key
|
||||
* → 列出當前 api_key 下所有 credential 名稱(不含加密值)
|
||||
* §2.4 選項甲(傾向):client 不再 AES-GCM 加密,明文值經 TLS 送到 cypher,cypher 短暫在記憶體
|
||||
* 經手明文(不落地、不持久、不持金鑰)後直接 PUT 進 Workers Secrets。這與舊版
|
||||
* `.claude/rules/01-tech-stack.md` 記載的「傳輸格式 {name, encrypted, iv}」不同——是本 SDD
|
||||
* (2026-07-03 T1.5 spike 定案)對舊格式的刻意取代,SDD §6 Q-b 仍列為需 leo 明確接受的
|
||||
* 誠實 trade-off(本次實作先落地,若 leo 不接受選項甲需回頭改)。
|
||||
*
|
||||
* DELETE /credentials/:name 與 GET /credentials 本次**不動**(T9 治理端點的範圍),
|
||||
* 仍讀寫舊 KV 路徑——這代表新寫入的 credential 目前查不到舊 GET /credentials 列表裡
|
||||
* (誠實缺口,見 credential-store-migration.md T5 完成註記;驗證改用直接查 D1)。
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { sha256Prefix } from '../lib/hash';
|
||||
|
||||
export const credentialsRouter = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
// POST /credentials — 上傳加密 credential
|
||||
/** 本 worker 的 script name(wrangler.toml `name`),官方與 self-hosted 都用同一個名字,
|
||||
* 只有帳號(CF_ACCOUNT_ID)不同——CF Workers Scripts secrets API 是 accountId+scriptName 定位。*/
|
||||
const CYPHER_SCRIPT_NAME = 'arcrun-cypher-executor';
|
||||
|
||||
/**
|
||||
* secret_ref 命名規則(credential-store-migration §2.3「以 CRED_ 前綴隔離命名空間」):
|
||||
* CRED_<NAME 大寫>_<sha256(api_key) 前 8 碼大寫>
|
||||
* 加 api_key 的 hash 是為了避免跨租戶同名 credential(如兩個用戶都存 telegram_bot_token)
|
||||
* 撞名覆蓋彼此的 Workers Secret(secret 是掛在同一個 worker 上、全域命名空間,沒有租戶
|
||||
* 隔離機制,必須自己用命名衍生隔離)。name 先前已被 validateName() 限制為 \w+,
|
||||
* 大寫後仍是合法的 env var 名(CF secret name 只接受 [A-Za-z0-9_])。
|
||||
*/
|
||||
async function deriveSecretRef(apiKey: string, name: string): Promise<string> {
|
||||
const hash8 = await sha256Prefix(apiKey);
|
||||
return `CRED_${name.toUpperCase()}_${hash8.toUpperCase()}`;
|
||||
}
|
||||
|
||||
function validateName(name: unknown): name is string {
|
||||
return typeof name === 'string' && /^\w+$/.test(name);
|
||||
}
|
||||
|
||||
function validSensitivity(s: unknown): s is 'standard' | 'high' {
|
||||
return s === 'standard' || s === 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* 呼叫 CF Workers Scripts secrets 管理 API,把明文值存進本 worker 的 per-script secret。
|
||||
* 唯寫:這支 API 不回傳任何既有 secret 的值,只能 create/update/delete/list 名字(D19 對齊)。
|
||||
*/
|
||||
async function putWorkerSecret(env: Bindings, secretRef: string, value: string): Promise<void> {
|
||||
if (!env.CF_SECRETS_API_TOKEN || !env.CF_ACCOUNT_ID) {
|
||||
throw new Error(
|
||||
'此 worker 缺 CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID 設定,寫入路徑未就緒(見 ' +
|
||||
'credential-store-migration.md T3:acr init/update 應確保這兩項就緒)',
|
||||
);
|
||||
}
|
||||
const url = `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/scripts/${CYPHER_SCRIPT_NAME}/secrets`;
|
||||
const res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.CF_SECRETS_API_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name: secretRef, text: value, type: 'secret_text' }),
|
||||
});
|
||||
const body = (await res.json().catch(() => null)) as
|
||||
| { success?: boolean; errors?: Array<{ message?: string }> }
|
||||
| null;
|
||||
if (!res.ok || !body?.success) {
|
||||
const detail = body?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`;
|
||||
throw new Error(`CF Workers Secrets 寫入失敗:${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* D1 upsert credential 目錄 row(不含密文)。
|
||||
* created_at 只在首次建立時寫入;覆寫(PUT/重複 POST)保留原 created_at,只更新
|
||||
* service/sensitivity/secret_ref(secret_ref 是純函式衍生自 api_key+name,理論上覆寫時
|
||||
* 值不會變,這裡仍寫入以求同一份 SQL 同時支援「首次建立」與「覆寫」兩種呼叫路徑)。
|
||||
*/
|
||||
async function upsertCredentialRow(
|
||||
db: D1Database,
|
||||
apiKey: string,
|
||||
name: string,
|
||||
service: string | null,
|
||||
sensitivity: 'standard' | 'high',
|
||||
secretRef: string,
|
||||
): Promise<void> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
await db
|
||||
.prepare(
|
||||
`INSERT INTO credentials (api_key, name, service, sensitivity, secret_ref, created_at, last_used_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
||||
ON CONFLICT(api_key, name) DO UPDATE SET
|
||||
service = excluded.service,
|
||||
sensitivity = excluded.sensitivity,
|
||||
secret_ref = excluded.secret_ref`,
|
||||
)
|
||||
.bind(apiKey, name, service, sensitivity, secretRef, now)
|
||||
.run();
|
||||
}
|
||||
|
||||
interface CredentialWriteBody {
|
||||
name?: string;
|
||||
value?: string;
|
||||
service?: string;
|
||||
sensitivity?: string;
|
||||
}
|
||||
|
||||
/** POST 建立 / PUT 覆寫共用的寫入邏輯。回傳 { secretRef } 供 route handler 組回應。 */
|
||||
async function writeCredential(
|
||||
env: Bindings,
|
||||
apiKey: string,
|
||||
name: string,
|
||||
value: string,
|
||||
service: string | undefined,
|
||||
sensitivityRaw: string | undefined,
|
||||
): Promise<{ secretRef: string; sensitivity: 'standard' | 'high' }> {
|
||||
const sensitivity = validSensitivity(sensitivityRaw) ? sensitivityRaw : 'standard';
|
||||
const secretRef = await deriveSecretRef(apiKey, name);
|
||||
|
||||
// 1. 密文值進 Workers Secrets(唯寫,arcrun 自己也讀不回)
|
||||
await putWorkerSecret(env, secretRef, value);
|
||||
|
||||
// 2. D1 目錄(不含密文)
|
||||
await upsertCredentialRow(env.CREDENTIALS_DB, apiKey, name, service ?? null, sensitivity, secretRef);
|
||||
|
||||
return { secretRef, sensitivity };
|
||||
}
|
||||
|
||||
// POST /credentials — 建立/覆寫 credential(新家:Workers Secrets + D1 目錄)
|
||||
credentialsRouter.post('/credentials', async (c) => {
|
||||
const apiKey = c.req.header('X-Arcrun-API-Key');
|
||||
if (!apiKey) {
|
||||
return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
|
||||
}
|
||||
|
||||
const body = await c.req.json().catch(() => null) as {
|
||||
name?: string;
|
||||
encrypted?: string;
|
||||
iv?: string;
|
||||
} | null;
|
||||
|
||||
if (!body?.name || !body.encrypted || !body.iv) {
|
||||
return c.json({ error: '缺少必要欄位:name, encrypted, iv' }, 400);
|
||||
const body = (await c.req.json().catch(() => null)) as CredentialWriteBody | null;
|
||||
if (!validateName(body?.name)) {
|
||||
return c.json({ error: 'name 必填,只能包含英文字母、數字和底線' }, 400);
|
||||
}
|
||||
if (!body?.value || typeof body.value !== 'string') {
|
||||
return c.json({ error: 'value 必填(credential 明文值,經 TLS 傳輸)' }, 400);
|
||||
}
|
||||
|
||||
const name = body.name.trim();
|
||||
if (!/^\w+$/.test(name)) {
|
||||
return c.json({ error: 'credential name 只能包含英文字母、數字和底線' }, 400);
|
||||
try {
|
||||
const { secretRef, sensitivity } = await writeCredential(
|
||||
c.env, apiKey, body.name, body.value, body.service, body.sensitivity,
|
||||
);
|
||||
return c.json({ success: true, name: body.name, service: body.service ?? null, sensitivity, secret_ref: secretRef });
|
||||
} catch (e) {
|
||||
// 誠實回報:寫入失敗(缺 token 設定 / CF API 錯誤)不假綠(mindset §7)
|
||||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
|
||||
}
|
||||
|
||||
const kvKey = `${apiKey}:cred:${name}`;
|
||||
const record = JSON.stringify({ encrypted: body.encrypted, iv: body.iv });
|
||||
|
||||
await c.env.CREDENTIALS_KV.put(kvKey, record);
|
||||
|
||||
return c.json({ success: true, name });
|
||||
});
|
||||
|
||||
// DELETE /credentials/:name — 刪除 credential
|
||||
// PUT /credentials/:name — 整筆覆寫(credential-store-migration §3:只能 replace,不能 edit 局部)
|
||||
credentialsRouter.put('/credentials/:name', async (c) => {
|
||||
const apiKey = c.req.header('X-Arcrun-API-Key');
|
||||
if (!apiKey) {
|
||||
return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
|
||||
}
|
||||
|
||||
const name = c.req.param('name');
|
||||
if (!validateName(name)) {
|
||||
return c.json({ error: 'name 只能包含英文字母、數字和底線' }, 400);
|
||||
}
|
||||
|
||||
const body = (await c.req.json().catch(() => null)) as CredentialWriteBody | null;
|
||||
if (!body?.value || typeof body.value !== 'string') {
|
||||
return c.json({ error: 'value 必填(credential 明文值,經 TLS 傳輸)' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const { secretRef, sensitivity } = await writeCredential(
|
||||
c.env, apiKey, name, body.value, body.service, body.sensitivity,
|
||||
);
|
||||
return c.json({ success: true, name, service: body.service ?? null, sensitivity, secret_ref: secretRef });
|
||||
} catch (e) {
|
||||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /credentials/:name — 刪除 credential(未動:仍是舊 KV 路徑,T9 範圍)
|
||||
credentialsRouter.delete('/credentials/:name', async (c) => {
|
||||
const apiKey = c.req.header('X-Arcrun-API-Key');
|
||||
if (!apiKey) {
|
||||
@@ -64,7 +205,7 @@ credentialsRouter.delete('/credentials/:name', async (c) => {
|
||||
return c.json({ success: true, name });
|
||||
});
|
||||
|
||||
// GET /credentials — 列出 credential 名稱(不含值)
|
||||
// GET /credentials — 列出 credential 名稱(不含值)(未動:仍是舊 KV 路徑,T9 範圍)
|
||||
credentialsRouter.get('/credentials', async (c) => {
|
||||
const apiKey = c.req.header('X-Arcrun-API-Key');
|
||||
if (!apiKey) {
|
||||
|
||||
@@ -27,8 +27,15 @@ export type Bindings = {
|
||||
RECIPES: KVNamespace;
|
||||
// Webhook Store:key = workflow name,value = Workflow JSON
|
||||
WEBHOOKS: KVNamespace;
|
||||
// Credential Store:AES-GCM 加密存放用戶 API token
|
||||
// Credential Store:AES-GCM 加密存放用戶 API token(舊家;credential-store-migration T7
|
||||
// 雙讀過渡期間仍是 fallback 讀路徑,本次 T5 只改「新寫入」,不動這裡)
|
||||
CREDENTIALS_KV: KVNamespace;
|
||||
// credential-store-migration T2/T5(D19「擁有目錄,不擁有內容物」):credential 目錄表
|
||||
// (api_key/name/service/sensitivity/secret_ref/created_at/last_used_at,不含密文)。
|
||||
// 與 KBDB base 共用同一顆 arcrun-kbdb D1(self-hosted 由 deploy.ts 注入用戶自己的
|
||||
// database_id,比照 kbdb/wrangler.toml 同一套 database_id 注入機制)。密文本體不在這裡,
|
||||
// 住在 Workers per-script Secrets(見 CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID)。
|
||||
CREDENTIALS_DB: D1Database;
|
||||
// Analytics:執行統計(fire-and-forget,key = stats:{workflowId}:{timestamp})
|
||||
ANALYTICS_KV: KVNamespace;
|
||||
// Users:OAuth 登入用戶帳號(key = user:{provider}:{provider_id})
|
||||
@@ -49,6 +56,12 @@ export type Bindings = {
|
||||
SESSION_SIGNING_SECRET?: string; // 用於 HMAC session ID(可選,也可直接用 UUID)
|
||||
// KBDB 整合
|
||||
KBDB_INTERNAL_TOKEN?: string;
|
||||
// credential-store-migration T3/T5:能打 CF Workers Scripts secrets 管理 API 的 token
|
||||
// (wrangler secret;PUT/DELETE .../workers/scripts/{script}/secrets,唯寫讀不回值,
|
||||
// D19 語意仍成立)+ 對應帳號 id(非機密,比照 WORKER_SUBDOMAIN 由 deploy.ts 自動注入)。
|
||||
// 缺其一 → /credentials 寫入路徑誠實回報未就緒(不假綠),見 T3。
|
||||
CF_SECRETS_API_TOKEN?: string;
|
||||
CF_ACCOUNT_ID?: string;
|
||||
// KBDB Base worker URL(recipe 成功記錄 /recipe-stats/record、fragment 抓取)。
|
||||
// 未設 fallback 見各使用點(recipe-expander 預設 kbdb.finally.click)。kbdb-base SDD §7.1。
|
||||
KBDB_BASE_URL?: string;
|
||||
|
||||
@@ -39,6 +39,15 @@ id = "25bef01d079148919578894434d58c4d"
|
||||
binding = "SESSIONS_KV"
|
||||
id = "455d0505c7534883a4d4985ab8295857"
|
||||
|
||||
# credential-store-migration T2/T5(D19):credential 目錄表(不含密文)。
|
||||
# 與 KBDB base 共用同一顆 arcrun-kbdb D1(比照 kbdb/wrangler.toml 同一個 database id)。
|
||||
# self-hosted:deploy.ts injectWranglerConfig 對任何 toml 內的 database_id 賦值行一律注入
|
||||
# ctx.d1DatabaseId(既有機制,見 kbdb/wrangler.toml 同款註解),本檔沿用不需額外改 deploy.ts。
|
||||
[[d1_databases]]
|
||||
binding = "CREDENTIALS_DB"
|
||||
database_name = "arcrun-kbdb"
|
||||
database_id = "0c580910-e00b-4f8e-9c57-ac54ea52242f" # 官方 prod D1(arcrun-kbdb);self-hosted 由上述機制注入用戶自己的 id
|
||||
|
||||
# 2026-06-04:移除 WASM_BUCKET R2 binding。R2 wasm 路徑早已 dead(平台零件 = 獨立 Worker,
|
||||
# 不從 R2 動態讀),保留只會誤導且 R2 需綁信用卡,與 open source 零費用核心衝突。
|
||||
# SDD: .agents/specs/component-registry-canon/tasks.md Phase 1.5(registry 已於 2026-05-07 移除,此為 cypher-executor 補清)
|
||||
@@ -107,6 +116,12 @@ ENVIRONMENT = "production"
|
||||
# MULTI_TENANT = "true"
|
||||
# ENCRYPTION_KEY 透過 wrangler secret set 設定
|
||||
|
||||
# credential-store-migration T3(§2.3 寫入路徑需要的 token+account id):
|
||||
# CF_SECRETS_API_TOKEN 是機密,透過 `wrangler secret put CF_SECRETS_API_TOKEN` 設定(不進 toml)。
|
||||
# CF_ACCOUNT_ID 非機密(帳號識別碼,不是憑證),比照 WORKER_SUBDOMAIN 由 deploy.ts 自動注入
|
||||
# (injectWranglerConfig 新增 CF_ACCOUNT_ID 注入,ctx.accountId 是 init/update 早就有的值)。
|
||||
CF_ACCOUNT_ID = ""
|
||||
|
||||
# Component worker subdomain(workers.dev 帳號 subdomain)
|
||||
# cypher-executor fetch component worker 一律走 arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev
|
||||
# 避開同 zone (*.arcrun.dev) 自循環死鎖,見 arcrun.md P0 #9(2026-05-13)
|
||||
|
||||
+29
-1
@@ -197,7 +197,35 @@ CLI 薄殼(rule 07):`acr creds list`(讀 D1 顯示)、`acr creds repla
|
||||
`fn(...)` 呼叫——用 probe 測試證實 `kv_get` 的 import 同樣不可直接呼叫,確認是既有架構的
|
||||
環境限制非 secret_get 缺陷,已移除該 2 案例並在測試檔內註記;pointer 機制的真實驗證改由
|
||||
T5 wrangler 部署端到端涵蓋。
|
||||
- [ ] T5 寫入路徑:`POST/PUT /credentials` 改寫 Workers Secrets(API `PUT`)+ D1 ref(§2.4)。
|
||||
- [x] T5 寫入路徑:`POST/PUT /credentials` 改寫 Workers Secrets(API `PUT`)+ D1 ref(§2.4)。2026-07-03 完成:
|
||||
`cypher-executor/src/routes/credentials.ts` 整檔改寫 POST(建立)+ 新增 PUT `/credentials/:name`
|
||||
(覆寫):兩者共用 `writeCredential()` → 1) `putWorkerSecret()` PUT 進 CF Workers Scripts secrets
|
||||
管理 API(唯寫)2) `upsertCredentialRow()` D1 upsert(不含密文,覆寫時保留原 `created_at`)。
|
||||
`secret_ref` 命名=`CRED_<NAME 大寫>_<sha256(api_key) 前 8 碼大寫>`(`cypher-executor/src/lib/hash.ts`
|
||||
匯出既有 `sha256Prefix`,避免跨租戶同名 credential 撞名覆蓋彼此的 secret;純函式衍生非解密/簽章邏輯,
|
||||
不違 rule 02 §2.2)。DELETE/GET `/credentials` 本次刻意不動(仍讀寫舊 KV,T9 治理端點範圍),
|
||||
已在檔頭與 §3 comment 誠實標注此缺口。`types.ts` Bindings 加 `CREDENTIALS_DB: D1Database`
|
||||
(與 KBDB base 共用同一顆 `arcrun-kbdb` D1)+ `CF_SECRETS_API_TOKEN?` / `CF_ACCOUNT_ID?`。
|
||||
`wrangler.toml` 加 `[[d1_databases]] binding="CREDENTIALS_DB"`(沿用 kbdb 同款 database_id
|
||||
注入機制,deploy.ts 不用改)+ `[vars] CF_ACCOUNT_ID=""` 佔位(T3 由 deploy.ts 自動注入非機密
|
||||
帳號 id,同 WORKER_SUBDOMAIN 模式)。**§2.4 選項甲落地**(client 不再 AES-GCM 加密,明文值經
|
||||
TLS 傳輸,cypher 短暫在記憶體經手明文不落地不持金鑰)——與舊版 `01-tech-stack.md` 記載的
|
||||
`{name,encrypted,iv}` 傳輸格式不同,此為本 SDD 對舊格式的刻意取代,§6 Q-b 仍列需 leo 明確
|
||||
接受的誠實 trade-off(先落地,若不接受選項甲需回頭改,見任務完成報告「撞牆」段)。
|
||||
驗證:`tsc --noEmit` exit 0(cli + cypher-executor 皆是);`vitest run` 26/27 通過(1 個
|
||||
pre-existing 無關失敗,見 T4 記錄,non-regression 已用 git stash 驗證)。**端到端證據**(真實
|
||||
leo21c `arcrun-cypher-executor` worker,非測試替身——寫入端點本身就是要驗的東西,無替代):
|
||||
比照 mistakes #23 手法本地 patch `wrangler.toml`(KV/D1 id、CF_ACCOUNT_ID、WORKER_SUBDOMAIN、
|
||||
KBDB_BASE_URL 換 leo21c 真實值,strip `[ai]`/`[[routes]]`)→ `wrangler deploy --dry-run` 核對
|
||||
binding 表(`env.CREDENTIALS_DB (arcrun-kbdb) D1 Database` 等全對)→ 真部署 → `wrangler secret put
|
||||
CF_SECRETS_API_TOKEN`(沿用 `CLOUDFLARE_API_TOKEN`)→ `curl POST /credentials` 回
|
||||
`{"success":true,"secret_ref":"CRED_TELEGRAM_BOT_TOKEN_9BB9FB82",...}` → CF API `GET
|
||||
.../workers/scripts/arcrun-cypher-executor/secrets` 確認該 secret 真的存在 → D1 query 確認
|
||||
對應 row(api_key/name/service/sensitivity/secret_ref/created_at 全對)→ `curl PUT
|
||||
/credentials/telegram_bot_token` 覆寫值,確認 `secret_ref` 不變、`created_at` 不變(覆寫語意
|
||||
正確)→ 清理:CF API `DELETE .../secrets/{ref}` + D1 `DELETE FROM credentials`,二次查詢確認
|
||||
皆已清空。部署完成後**立刻 restore `wrangler.toml` 備份**,git 追蹤版本無 leo21c 帳號專屬 id
|
||||
殘留(已 grep 確認)。
|
||||
- [ ] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → `env[ref]` 取值(§2.5)+ 更新 last_used。
|
||||
- [ ] T7 雙讀 fallback(§4.1)。
|
||||
- [ ] T8 回填端點 `POST /credentials/migrate-to-workers-secrets`(§4.2,冪等可審)。
|
||||
|
||||
Reference in New Issue
Block a user