fix(teardown): 中心側補上帳號白名單——原本 central-* 碰得到 leo21c
實測破口(2026-08-14,修補前):
central-plan --account-id 51a01bfa…(leo21c)
→ 一路暢通,列出 deployed:51a01bfa…:leo21c
加 --yes 就會刪掉 leo 的安裝紀錄 ⇒ 他下次重裝撞 Arcrun#120 死結。
根因是兩個問題被當成同一個:
① 動的是「誰的」紀錄 → assertAccountAllowed(帳號側有,中心側沒有)
② 以「誰的身分」動手 → assertWranglerIsCentralAccount(D88 補的那道)
c543aba 的註解寫「這個不對稱現在補齊」,但只補了②。註解說補齊、程式補一半,
後來讀的人會相信註解——所以順手把那段註解改成實測講法。
另修 listAllScripts 沒翻頁(ops-facts 記過的同款坑)。在這支工具裡漏看一顆
worker 有兩層傷害:拆不乾淨,以及「共用資源保護」看不到那個 owner,
把還在用的資源判成沒人用而刪掉。
並補一條已知限制:資源清單是從 worker binding 反推、不掃帳號,
所以沒人綁的孤兒殘骸不會被列也不會被刪(這正是 drill-a/drill-b 不受影響的原因,
但代價是拆完重裝可能撞 Arcrun#123,要自己再列一次帳號)。
驗證(youlin,全唯讀,未執行 apply):
- plan:6 顆 worker + 10 個資源(8 顆 yuga3bse KV/yuga3bse-db/embed-m3),
drill-a、drill-b 不在清單
- leo21c 帳號側、中心側 兩條路徑皆 exit 1 拒絕
- youlin 中心側仍正常:staging 通道有 deployed:1129efd7…:youlin-hsieh-dev 一筆
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,13 @@
|
||||
* (是 → 一併排入拆除;不是 → 照紅線問人再決定要不要動它),**拆完後如果選了半殘留,
|
||||
* 要有心理準備下一步的 fail-closed 檢查會擋下重裝**。
|
||||
*
|
||||
* - 🔴 **只拆「有人綁著的」,看不見孤兒資源**:資源清單是從「要刪的 worker 的 binding」反推的,
|
||||
* 不是掃帳號。所以**沒有任何 worker 綁著的殘骸**(例:上一次安裝到一半死掉留下的 KV/D1)
|
||||
* 本工具**不會列、也不會刪**。這個取捨是刻意的——它同時是「不誤刪」的來源
|
||||
* (2026-08-14 youlin 上的 `arcrun-rag-drill-a`/`drill-b` 兩顆演練用 D1 因此原封不動)——
|
||||
* 但代價是:拆完之後帳號上可能仍留著同名殘骸,重裝時撞 Arcrun#123(同名資源已存在)。
|
||||
* ⇒ 拆完要重裝之前,**自己再列一次帳號**確認沒有同租戶前綴的孤兒。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/teardown-instance.mjs plan --account-id <id> --api-token <token> [--keep w1,w2]
|
||||
* node scripts/teardown-instance.mjs apply --account-id <id> --api-token <token> [--keep w1,w2] --yes
|
||||
@@ -161,11 +168,25 @@ function scanKnownWorkerNames() {
|
||||
|
||||
// ── 二、帳號側 discovery ─────────────────────────────────────────────────────
|
||||
|
||||
/** 列出帳號上所有 worker script 名字。 */
|
||||
/**
|
||||
* 列出帳號上所有 worker script 名字。
|
||||
*
|
||||
* 🔴 一定要翻頁。漏看一顆 worker 在這支工具裡是**兩層**傷害:
|
||||
* ① 那顆是實例的一部分卻沒被刪 ⇒ 拆不乾淨;
|
||||
* ② 更糟——那顆若綁著某顆 KV/D1,「共用資源保護」就看不到這個 owner,
|
||||
* 於是把還在用的資源判成「只有要刪的 worker 綁著」而**刪掉別人在用的東西**。
|
||||
* (同款坑 ops-facts 記過:`cf-resource-api.mjs` 寫死 per_page=100 又不翻頁。)
|
||||
*/
|
||||
async function listAllScripts(api) {
|
||||
const { ok, result, error } = await api.cfRaw('/workers/scripts?per_page=100');
|
||||
if (!ok) throw new Error(`列 worker scripts 失敗:${error}`);
|
||||
return /** @type {Array<{id:string}>} */ (result).map((s) => s.id);
|
||||
const names = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const { ok, result, error } = await api.cfRaw(`/workers/scripts?per_page=100&page=${page}`);
|
||||
if (!ok) throw new Error(`列 worker scripts 失敗(page ${page}):${error}`);
|
||||
const batch = /** @type {Array<{id:string}>} */ (result) ?? [];
|
||||
for (const s of batch) names.push(s.id);
|
||||
if (batch.length < 100) break;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +334,15 @@ async function applyTeardown(plan, { accountId, apiToken }) {
|
||||
// 靠操作者自己每次手動 `wrangler whoami` 核對——那個核對動作是對的,只是長錯地方
|
||||
// (長在人身上)。失敗模式是最壞的一種:wrangler 的 OAuth 登入態如果哪天換成別的帳號,
|
||||
// 會**靜默**對錯的帳號動中心 KV,沒有任何東西會叫。帳號側(youlin)有 ALLOWED_ACCOUNT_IDS
|
||||
// 白名單擋,中心側原本完全沒有——這個不對稱本身就是設計缺口,現在補齊。
|
||||
// 白名單擋,中心側原本完全沒有——這個不對稱本身就是設計缺口。
|
||||
//
|
||||
// ⚠️ 2026-08-14 稍晚訂正:上面那句原本寫「現在補齊」,**但當時只補了一半**——
|
||||
// 補的是「以誰的身分動手」(wrangler 身分),沒補「動的是誰的紀錄」(accountId 白名單)。
|
||||
// 實測證明破口是真的:`central-plan --account-id 51a01bfa…`(leo21c)一路暢通,
|
||||
// 直接列出 `deployed:51a01bfa…:leo21c`,再加 `--yes` 就會刪掉 leo 的安裝紀錄。
|
||||
// ⇒ 現在 planCentralCleanup() 兩道都跑:assertAccountAllowed + assertWranglerIsCentralAccount。
|
||||
// 📌 教訓:**「補齊了」這句話要用實測講,不要用註解講**——註解說補齊,程式只補一半,
|
||||
// 而後來讀的人(包括我)會相信註解。
|
||||
// assertWranglerIsCentralAccount() 在任何中心 KV 讀寫前跑,核對 wrangler 現在的 OAuth
|
||||
// 登入態的帳號清單裡有沒有 CENTRAL_ACCOUNT_ID,沒有就直接丟錯、不放行。
|
||||
|
||||
@@ -384,6 +413,16 @@ function wranglerKvDelete(namespaceId, key) {
|
||||
}
|
||||
|
||||
function planCentralCleanup(accountId) {
|
||||
// 🔴 兩道,缺一不可,而且問的是**不同的問題**:
|
||||
// ① assertAccountAllowed(accountId):「我要清掉的是**誰的**紀錄」——
|
||||
// 中心 KV 裡每個帳號一筆,`deployed:<accountId>:`。少了這道,
|
||||
// `central-apply --account-id <leo21c 的 id>` 會清掉 leo21c 的安裝紀錄,
|
||||
// leo 下次重裝就會撞上 Arcrun#120 的死結。**帳號側擋得住 leo21c,中心側原本擋不住。**
|
||||
// (2026-08-14 實測:`central-plan --account-id 51a01bfa…` 直接列出了
|
||||
// `deployed:51a01bfa…:leo21c`,一路暢通。)
|
||||
// ② assertWranglerIsCentralAccount():「我現在**以誰的身分**在動中心 KV」(D88 那道)。
|
||||
// ①問對象、②問身分。原本只有②,於是「白名單」只保護了帳號側的一半路徑。
|
||||
assertAccountAllowed(accountId);
|
||||
assertWranglerIsCentralAccount();
|
||||
const prefix = `deployed:${accountId}:`;
|
||||
const plan = {};
|
||||
|
||||
Reference in New Issue
Block a user