本卡屬 FR-167(母卡 CM-3241),卡 P2-1,階段 P2 更新站、批 1。只改 update-center(新 repo)。依賴:無。
更新站的第一版:一個 Cloudflare Worker(TypeScript),客戶主系統每天來問「有沒有新版」、按下載時來拿網址、升完來回報。要先驗客戶帶來的授權檔是不是我們授權中心簽的,再看主版號對不對、有沒有被列入拒絕名單。包放 R2,依主版號分資料夾。
新 repo 從零;判定邏輯(合法安裝、拒絕名單、主版號、必經路徑)要一次做對且有真資料守衛
new(新 repo,TS):模組分法照 License Center(routes/service/repo);驗章用 WebCrypto 原生 Ed25519;授權檔格式與 kid 機制 reuse jedi-license-runtime public_keys.py 的定義(公鑰表複製成 TS 常數,守衛測試用授權中心真簽的檔對照);型別用 @cloudflare/workers-types;測試用 vitest+@cloudflare/vitest-pool-workers
repo:~/Projects/Billows/Audit-Manager/update-center
新開:update-center/(package.json、tsconfig.json、wrangler.toml 綁既有 bucket `guidant-ai-release` 與 D1、src/index.ts、src/routes/client.ts、src/releases.ts(含 r2_key 拼法 `v<major>/<version>/<檔名>`,只在這裡拼)、src/licenses.ts、src/denylist.ts、migrations/0001_init.sql、test/、.env.example、README.md)
細節在 design.md §5 對應段(~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-167-2610-online-update-channel/design.md,開 design.html 看導覽);本段只列本卡要做到的點:
npm create cloudflare@latest 選 TS Worker 骨架;wrangler.toml 綁 D1(UPDATE_DB)、R2(bucket guidant-ai-release,binding RELEASES);migrations/0001_init.sql 六張表照 design §5.4。src/licenses.ts:授權檔格式與 kid 查表照 jedi-license-runtime public_keys.py(公鑰表複製成 TS 常數,PEM→raw Ed25519 轉 WebCrypto importKey);verifyLicense() 回 {license_id, fingerprint, product_major(缺=1), valid},不看 expires_at。src/releases.ts:r2Key(major, version, filename) 唯一拼 key 的地方;check() 條件:授權驗過 ∧ 不在 denylist ∧ major 相同 ∧ version > 現行 → 回 signed_manifest+path[](必經版本鏈由 min_base_version 遞推);對該 fingerprint 開的草稿也回。POST /v1/download-url:R2 預簽 15 分;寫 release_download_log。POST /v1/report:寫 instance_version_reports(無客戶名)。test/fixtures/ 放授權中心真簽的 DEV 授權檔(不含客戶名;跟首腦要或自己用 LC DEV 簽一份);偽造檔 401、denylist 403、主版號不同 none。本機 wrangler dev:帶 DEV 真授權檔打 check 回已發布清單+簽章;過期授權檔照回;主版號不同回 none;拒絕名單回 403;偽造授權檔 401;download-url 回 R2 簽名網址 15 分有效;report 寫 instance_version_reports 不含客戶名;守衛測試用授權中心真簽出來的檔,驗不過擋部署
對照雛形:~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-167-2610-online-update-channel/prototype/index.html(上方「跟著走」)。