fix(scripts): shadcn-sync 不再把 403/非 JSON 错误响应当成注册表数据缓存 - #3496
Merged
Conversation
…onse `fetchUrl` never inspected `res.statusCode`, and a body that failed `JSON.parse` was resolved as a raw string. A 403 from an egress allowlist therefore resolved as if it were a component, and `fetchRegistry` wrote it to disk unconditionally with a 1h TTL — so one blocked run poisoned every `pnpm shadcn:check` for the next hour, reporting "46 cached, 0 fetched" while never retrying. Two independent defences: - transport: a non-2xx (redirects included) and a 2xx that is not JSON now reject, with the status and a sanitised body excerpt in the message. - semantic: only a response passing `isRegistryEntry` (files[0].content) is written to the cache, and the same check runs on READ — an entry poisoned by an older build is dropped on first contact instead of being trusted until its TTL expires, so recovery is immediate. `--update` semantics are unchanged (still never reads the cache, still refuses to write on a failed fetch); the summary line now reports failed and evicted separately so a fully-blocked run cannot read as a success. `main()` runs only when the file is the process entry point, so the offline tests can import the fetch/cache internals. Refs objectstack-ai/objectstack#5803 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 6, 2026
…ype gate objectui#3494 (PR #3498) adds `tsconfig.scripts.json` with `allowJs: true`, whose `include` glob covers this branch's new test file too. Neither PR can see the other, and the two land green individually while the merge is red — no git conflict, so nothing warns. Two separate problems, both found by running #3498's compilerOptions against this branch: - the `@ts-expect-error` above the `../shadcn-sync.js` import becomes TS2578 once the import has an inferred type. Removed (the sibling test files get the same treatment inside #3498). - `fetchRegistry`'s `get` option was the one destructured option without a default, so it is absent from the function's inferred signature and passing it from a `.ts` caller is TS2353. Fixed at the producer by giving it its real default (`https.get`), which also drops the `get ? { get } : undefined` dance at the call site — not by suppressing it at the consumer. Behaviour is unchanged: production still resolves `https.get`, the tests still inject `http.get`. Verified against a local copy of #3498's config — zero errors in either file; the only remaining diagnostics are the five stale directives that #3498 itself removes. Refs objectstack-ai/objectstack#5803 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes objectstack-ai/objectstack#5803
问题(已在本地复现,与 issue 描述一致)
scripts/shadcn-sync.js的fetchUrl从不检查res.statusCode,而JSON.parse失败的响应体被 catch 后当成功 resolve。于是 egress 拦截返回的403 Forbidden+text/plain正文,被当作组件数据一路流到fetchRegistry——后者又无条件写盘(TTL 1h)。一次网络抖动就毒化了接下来一小时的所有--check。复现(修复前,origin/main,沙箱 egress 拦截):
落盘的缓存条目正文:
{"url":"https://ui.shadcn.com/r/styles/default/calendar.json","fetchedAt":1786022023686, "data":"Host not in allowlist: ui.shadcn.com. Add this host to your network egress settings to allow access."}两条独立防御(按 issue 建议)
fetchUrl非 2xx 直接 reject,错误信息带状态码与净化后的正文摘要(形如HTTP 403 Forbidden from https://ui.shadcn.com/… — Host not in allowlist: …)。2xx 但解析不出 JSON 同样 reject:旧代码 resolve 原始字符串,而所有消费者读的都是data.files?.[0]?.content,undefined让失败静默掉了——这正是 #0.1 说的「宽容消费者」把生产端错误藏起来。重定向刻意不跟随:3xx 落到登录页/中间页是同一类毒药,注册表端点本身是直连 JSON,真要跟随应当是一次显式决定。isRegistryEntry(files[0].content非空)的响应才写缓存。200 但返回{"error":"…"}这类错误信封,依旧原样返回给调用方(--check仍报 "no usable file content"),但不许进磁盘。读侧也校验(选择理由)
同一个
isRegistryEntry在读的时候也跑一遍,不通过就删掉该条目并重新取。只做写侧校验的话,已经被旧版本毒化的 checkout 仍要靠 TTL 熬满一小时才能自愈,期间除了「等」或「知道有--no-cache这个参数」没有出路。读侧校验让恢复立刻发生——这也是实测里 46 条毒条目在第一次运行就被清掉的原因。过期(TTL 到期)但格式正常的条目不算污染,照旧只是重新取,不计入 evicted。未改动的语义
--update仍然不读缓存(写入路径永远看实时数据),失败时仍拒绝写文件。区别只是错误信息现在说的是真正的原因(HTTP 403 …)而不是误导性的 "No files found in registry"。--check的退出码语义保持 fix(components,i18n): Sheet/Dialog 关闭按钮接入 common.close,并让 shadcn 同步机制携带该补丁 (objectstack#5505) #3455 不变:只有 declared-patch 失败才非零,普通 fetch 错误仍是 exit 0。main()加了 entry-point 判断(只在文件本身是进程入口时执行),这样离线测试可以 import 内部函数;这条守卫由一个真正 spawnnode scripts/shadcn-sync.js --list的用例钉住——写错的话pnpm shadcn:check会变成静默 no-op。验证
scripts/__tests__/shadcn-sync-fetch-cache.test.ts(23 例,全离线:本地httpfixture server + 注入get,真 socket / 真状态行 / 真分块)。反向验证(先预测方向再跑):只回退两条防御、保留导出与
isRegistryEntry本身 → 预测 10 红,实测 10 红 13 绿,且绿的正是预测的那批(8 个isRegistryEntry断言全绿 —— 说明谓词本身被独立覆盖,不是幽灵检查;CLI 用例、正常缓存命中、TTL 过期重取、allowCache关闭四例也如预期不受影响)。离线端到端(沙箱 egress 拦截,先把 46 条毒条目放回缓存目录):
健康路径未被误伤(用本地组件内容伪造 46 条格式正确的条目后跑真实 CLI):
无 changeset:纯 CI/tooling 脚本,不面向用户(#3437 先例)。
node scripts/check-control-bytes.mjs通过,两个改动文件另做了控制字符自查(bodySnippet按码位判断,源文件里不含任何控制字符字面量)。