零依赖工具库的演进史:深入解析 Scalar 的 @scalar/helpers 包及其安全加固实践
2026/9/15 9:56:36 网站建设 项目流程

零依赖工具库的演进史:深入解析 Scalar 的 @scalar/helpers 包及其安全加固实践

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

@scalar/helpers是 Scalar 开源 API 平台(REST API 客户端、API 文档渲染、OpenAPI 工具链)内部共享的零依赖工具函数集合,负责为api-referenceapi-clientsidebarjson-magic等下游包提供字符串处理、对象合并、URL 安全、主题管理、测试基础设施等基础能力。本文以该包的 CHANGELOG 为主线,结合仓库源码逐项剖析其从 0.0.2 到 0.11.3 的演进脉络,重点讲解原型污染防护、恶意 URL 过滤、安全侧边栏锚点渲染等安全加固背后的实现原理,以及 slugify、compareVersions、Result/safeRun 等实用 API 的参数与用法,帮助你在自己的项目中直接复用这些经过实战检验的工具函数。

一、包定位:为什么 Scalar 需要一个独立的 helpers 包

@scalar/helpers的 README 第一句话就点明了它的定位:A collection of dependency free helpers(零依赖工具函数集合)。这意味着包内所有工具函数都不依赖第三方 npm 包,纯手写实现。

从源码结构看,packages/helpers/src 目录按领域划分了 15 个子模块:

  • array:数组工具(addToMapArrayisDefinedsortByOrder
  • dom:DOM 与事件工具(isPlainLeftClickscrollToIdfreezeElement
  • errors:错误归一化(normalizeError
  • file:文件格式转换(json2xml
  • formatters:格式化(formatBytesformatMilliseconds
  • general:通用工具(compareVersionsdebouncecreateLimiter、平台检测)
  • http:HTTP 相关(MIME 类型、HTTP 方法/状态码、Cookie 序列化、scalar-headers
  • json:JSON 处理(prettyPrintJson、JSON Pointer 解析)
  • markdown:Markdown 工具(getMarkdownHeadingsrelease-notes
  • node:Node 兼容垫片(node:pathpolyfill)
  • object:对象操作(mergeObjectssetValueAtPathpreventPollution等)
  • playwright:Playwright 测试基础设施(docker.ts
  • regex:变量替换与正则工具
  • string:字符串工具(slugifysluggertruncate、hash 生成)
  • theme:主题系统(color-modeload-css-variables
  • types:类型工具(ResultsafeRun
  • url:URL 工具(isSafeUrlmergeUrlsensureProtocol等)

每个模块都配有对应的*.test.ts单测文件,包内共 80+ 个源码文件,测试与实现一一对应,这为下游包安全复用提供了基础保障。

二、安全加固主线:对抗原型污染(Prototype Pollution)

变更日志中最具分量的安全议题是0.11.0 版本对 diff 工具链的原型污染防护,这也是 Scalar 处理"不可信文档"(远程拉取的 OpenAPI 文档、用户上传的配置文件)时的核心防线。

2.1 攻击面:JSON.parse会把__proto__变成真实属性

0.11.0 的变更说明指出了攻击路径的本质:文档数据来自远程抓取和用户文件,而JSON.parse会把__proto__键解析为对象的自有属性。于是:

apply({}, diff({}, JSON.parse('{"__proto__": {"polluted": "yes"}}')))

这段代码在过去会通过apply把值写入Object.prototype,从而"污染"运行时中所有对象的原型链——任何对象都能读取到被注入的polluted属性,这是典型的原型污染(Prototype Pollution)攻击。

2.2 修复方案:单点维护的危险键清单

修复的核心是在 prevent-pollution.ts 中定义危险键集合:

const PROTOTYPE_POLLUTION_KEYS = new Set(['__proto__', 'prototype', 'constructor'])

并基于它提供两个 API:

  • preventPollution(key, context?)校验式。遇到危险键直接抛出Errorcontext参数用于在错误信息中定位是哪个操作触发的(prevent-pollution.ts)。
  • isPollutionKey(key)过滤式。只返回布尔值不抛错,适合遍历不可信文档键名时把危险键跳过而非拒绝(prevent-pollution.ts)。

两个 API 的设计意图在源码注释中写得很清楚:preventPollution用于"这个键必须被拒绝"的场景,isPollutionKey用于"这个键应当被过滤掉"的场景。

2.3 下游集成:diff/apply 的三重防线

0.11.0 对@scalar/json-magic的 diff 工具链做了系统性加固,具体包括:

  1. diffisKeyCollisionsmergeObjects跳过危险键:不再比较或合并名为__proto__constructorprototype的属性;
  2. merge的 trie 子节点改用 null 原型:即使子节点键名是__proto__,由于Object.create(null)没有原型链,写入也不会污染Object.prototype
  3. apply在任何写入发生前拒绝危险 changeset:见 json-magic/src/diff/apply.ts,apply会先遍历全部变更的路径段,一旦发现isPollutionKey命中的段,立即抛出InvalidChangesDetectedError,且这个校验发生在任何文档写入之前,保证文档不会处于半更新状态。

InvalidChangesDetectedError本身定义在 json-magic/src/diff/apply.ts,是Error的子类,便于调用方用instanceof精确捕获。

2.4 行为取舍:合法属性名的代价

文档如实记录了这次加固带来的行为变化(CHANGELOG 0.11.0 小节):

  • JSON 规范允许、schema 也完全可以描述一个名为constructor的属性,但加固后diff不再单独比较这类属性——单独编辑它不会被报告为变更;
  • 之前 merge 会把它当作冲突浮出,现在则干净地合并;
  • 属性在其父对象整体被新增或更新时仍会随子树一并传输(因为新子树是作为单个值发出的)。

这个取舍与代码库中已有的preventPollution策略保持一致,属于"以可控的行为差异换取运行时安全"的典型安全工程决策。

2.5 与之配套的 0.10.0 安全加固

早在 0.10.0 版本,API Reference 就针对"不可信 OpenAPI 文档"做了一轮安全加固:

  • 文档中取出的链接目标(info.license.urlinfo.termsOfServiceinfo.contact.urlexternalDocs.urlx-scalar-links)以及直接下载链接,全部经过协议白名单校验,文档无法再渲染出点击即执行脚本的javascript:链接,不安全的值回退为纯文本;
  • deepMerge(被导出的createEmptySpecification使用)不再穿透原型链写入;
  • customCss不能再闭合注入的<style>标签(这在 SSR 场景下尤其重要,因为该值会原样进入 HTML 流);
  • 剩余的target="_blank"链接补上了rel="noopener noreferrer"

三、URL 安全:isSafeUrl / sanitizeUrl 的协议白名单实现

0.10.0 引入的 URL 安全能力沉淀为@scalar/helpers/url/is-safe-url,实现在 is-safe-url.ts。核心逻辑非常值得学习:

const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:', 'ftp:', 'ftps:', 'sms:'])

isSafeUrl的处理流程(is-safe-url.ts):

  1. 先剔除 URL 中的ASCII 空白、C0/C1 控制字符——因为浏览器在解析 URL 前会先剥离这些字符,java\tscript:alert(1)不剔除的话能绕过协议检查;
  2. SCHEME_REGEX = /^([a-z][a-z0-9+.-]*):/i提取协议;
  3. 无协议即相对 URL/docs./openapi.json#section//example.com),它继承当前页面的协议,不会携带危险协议,直接放行;
  4. 有协议则检查是否在白名单内。

配套的sanitizeUrl在 URL 不安全时返回undefined,调用方据此直接丢弃href甚至整个链接(is-safe-url.ts),而不是渲染一个攻击者可控协议的链接。

关键启发:做 URL 安全校验时,"先剥离控制字符再解析 scheme" 与"相对 URL 天然安全"是两个容易遗漏但至关重要的细节。

四、侧边栏锚点化:isPlainLeftClick 与可被搜索引擎爬取的导航

0.11.0 中另一项大型改进是将侧边栏导航从 button 渲染为真正的 anchor 链接,涉及@scalar/sidebar@scalar/api-reference@scalar/helpers三个包。@scalar/helpers的贡献是新增了 is-plain-left-click.ts:

export const isPlainLeftClick = (event: MouseEvent): boolean => { return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey }

设计要点(源码注释原文逻辑):

  • SPA 只应该劫持"普通左键单击":带meta/ctrl/shift/alt修饰键或非主键的点击表达的是"新标签页打开"等浏览器原生意图,必须交给浏览器默认行为;
  • isPlainLeftClick只检查按钮和修饰键,不检查event.defaultPrevented——判断"默认行为是否已被阻止"是调用方的职责,两者分离避免了在谓词里测试可变标志带来的顺序依赖问题。

配合@scalar/sidebar新增的getHref回调(返回 URL 时该项渲染为真实链接,唯一的例外是作为分组标签的 tag-group 标题)与@scalar/api-reference新增的 SSR 安全助手makeHrefFromId(实现在 api-reference/src/helpers/id-routing.ts,被 ApiReference.vue 使用),实现了:

  • 路径路由(pathRouting)下,侧边栏包含真正的<a>标签,href 与 push 进 history 的 URL 一致,导航可被搜索引擎爬取和索引
  • 哈希路由 / 哈希基准路径路由下,片段 href 改善了链接语义和新标签页打开行为,但搜索引擎不把片段视为独立 URL——变更日志明确提示:若目标是 URL 发现,请配置pathRouting
  • 侧边栏条目遵循标准链接键盘语义(Enter 激活、Space 滚动页面);
  • SSR 时折叠分组内的链接只在分组展开状态下出现在 HTML 中(例如通过defaultOpenAllTags)。

同时,@scalar/components不需要任何新 API——条目复用了ScalarSidebarItem/ScalarSidebarGroup上已有的button插槽。

五、主题系统:applyColorMode 与 loadCssVariables

5.1 DarkLightMode 与 applyColorMode(0.11.3)

0.11.3 为@scalar/helpers新增了applyColorModeDarkLightMode类型,并被useColorMode使用。实现位于 theme/color-mode.ts:

export type DarkLightMode = 'light' | 'dark' export const applyColorMode = (mode: DarkLightMode, target: HTMLElement = document.body): void => { target.classList.toggle('dark-mode', mode === 'dark') target.classList.toggle('light-mode', mode === 'light') }

源码注释揭示了几个设计决策:

  • DarkLightMode刻意比用户可选的主题模式窄:用户还能选system,但那只是"偏好",必须先用操作系统解析成具体模式才能渲染,所以到达 DOM 的模式永远是lightdark之一;
  • Scalar 主题把明暗两套都做成类名而非媒体查询,切换模式就是交换两个类,applyColorMode同时 toggle 两个类,保证元素永远不会同时携带两个模式类;
  • 默认参数读取document,因此在 SSR 下调用会直接抛错而非静默无效——调用方有责任先确认 DOM 存在。

配套的getSystemColorMode(color-mode.ts)读取系统偏好:无window时返回light(与服务端渲染一致);有window但无matchMedia时返回dark(源码注明这是历史行为保留,实际浏览器都实现了matchMedia)。

5.2 从主题 CSS 解析变量:loadCssVariables(0.8.0)

0.8.0 把loadCssVariablesscalar-app移入@scalar/helpers/theme/load-css-variables。实现在 theme/load-css-variables.ts:

  1. new CSSStyleSheet()+sheet.replace(css)解析 CSS 文本;
  2. 遍历CSSStyleRule,通过getColorModesFromSelectors精确匹配恰好是.light-mode/.dark-mode的选择器(复合选择器如.light-mode .foo不匹配);
  3. 提取--前缀的自定义属性,并用parseVariableValue把颜色值规范化为大写 hex(支持#RGB短格式展开、rgb()/rgba()转换、var()保留待解析);
  4. resolveVariables单趟递归解析var(--x)引用链;
  5. 最终返回{ light: {...}, dark: {...} }两个映射。

这对"把任意 Scalar 主题 CSS 变成可编程的明暗变量表"非常有用,是主题编辑器、动态主题切换类功能的基础。

六、本地化配置与 mergeObjects(0.9.0)

0.9.0 为 API Reference UI 引入了本地化配置,内置英文、俄语、西班牙语、法语、德语、简体中文和阿拉伯语七种语言,阿拉伯语 locale 自动启用RTL 方向;同时更新了共享主题 reset,让文本输入框在 RTL 文档中默认对齐到逻辑起点。

本地化层把翻译覆盖(overrides)合并到内置 locale 时使用的正是新增的mergeObjects深合并助手(object/merge-objects.ts)。它的语义对"配置覆盖默认值"场景做了精确裁剪:

  • 嵌套普通对象递归合并
  • override 中的undefined被跳过——覆盖永远不会清空基准值;
  • 任何非普通对象值(包括数组)整体替换基准值——不做数组合并,这正是配置式合并期待的行为;
  • 基准对象从不被原地修改,返回全新对象。

代码示例:

mergeObjects({ theme: { mode: 'light', accent: '#000' } }, { theme: { accent: '#4f46e5' } }) // => { theme: { mode: 'light', accent: '#4f46e5' } }

七、请求构建的错误即值:Result 与 safeRun

0.8.0 的补丁中有一系列围绕"请求构造失败应当是一等公民"的改动,其底层基础是@scalar/helpersResult类型与safeRun

Result<T, E = string>(types/result.ts)是一个可辨识联合:

type Result<T, E = string> = | { ok: true; data: T } | { ok: false; error: E; message?: string }

错误类型默认是string,但可以传入字符串字面量联合(或任意形状)作为第二泛型参数,配合message字段承载人类可读的详情,消费方可以直接在 toast 和日志里展示。

safeRun(types/safe-run.ts)把任意函数包装成不会抛错的调用:

  • 函数返回 Promise 时返回Promise<Result<T>>,rejection 变成err(message),promise 永不 reject;
  • 同步返回时返回Result<T>
  • 失败统一console.error记录(开发期可见),再映射为err

0.8.0 中buildRequest正是基于这套机制返回带稳定错误码的结果,如MISSING_REQUEST_SERVER_BASEINVALID_REQUEST_FACTORY_URLBUILD_REQUEST_FAILEDresolveRequestFactoryUrl在严格模式下拒绝相对 URL、空 server base、以及仍含未解析{{variable}}占位符的路径;需要刻意省略完整绝对 URL 的场景(如OperationBlock内嵌模态布局、API Reference 的onBeforeRequest钩子)可设置allowMissingRequestServerBase。API Reference 插件路径在预览请求构建失败时记录日志并跳过onBeforeRequest,保证用户钩子永远不会拿到半成品的 fetch 载荷。下游包(api-clientapi-referencescalar-app)解包结果、展示 toast 或日志,并在 URL 合法之前不调用sendRequest

八、字符串与版本工具:slugify、compareVersions、serializeReleaseNotes

8.1 slugify / slugger 的 Unicode 规范化选项(0.8.0 / 0.7.0)

0.8.0(对应 0.7.0 内容)为slugifyslugger新增了normalizationFormstripAccents选项。实现在 string/slugify.ts,完整参数如下:

选项类型默认值说明
allowedSpecialCharsstring''允许额外存活于非单词过滤的字符,如'.'"v1.2.3""v1.2.3"
preserveCasebooleanfalsetrue时保留大小写,默认小写
normalizationForm'NFC' \| 'NFD' \| 'NFKC' \| 'NFKD''NFC'传给String.prototype.normalize()的 Unicode 规范化形式
stripAccentsbooleanfalse去除重音符号,"Crème Brûlée""creme-brulee",优先于normalizationForm

实现细节:stripAccents内部固定走 NFD 分解,把基础字母与组合变音符拆成独立码点后,用/\p{M}/gu一次性删除所有 Unicode 组合标记(slugify.ts);allowedSpecialChars会先做正则转义再编译进字符类,并用Map缓存编译结果(slugify.ts);默认输出限制 255 字符。

8.2 compareVersions:不依赖 semver 的版本比较(0.8.0 / 0.7.0)

compareVersions(general/compare-versions.ts)实现了 "What's new" 功能所需的MAJOR.MINOR.PATCH+ 可选预发布标签比较:

  • 返回负数表示a < b0相等,正数a > b,契约对齐Array.sort
  • 预发布版本(如1.0.0-rc.1低于同版本正式版(符合 semver 规范),避免正式版用户看到 beta 专属条目;
  • +之后的构建元数据在比较前剥离;
  • 预发布标识按段比较,数字标识符低于字母数字标识符。

并附带isVersionLessThanisVersionLessThanOrEqual两个便捷包装。

8.3 serializeReleaseNotes:从 JSON 生成人类可读的发布说明(0.8.0)

serializeReleaseNotes(markdown/release-notes.ts)把结构化的ReleaseNoteEntry(版本、日期、标题、富内容块)序列化为RELEASE_NOTES.md的格式。条目支持段落、三级/四级标题、有序/无序列表、图片、内嵌<video>与链接块;输出在给定 preamble 与条目列表下是确定性的,且始终以单个换行结尾。设计上RELEASE_NOTES.json是唯一事实来源,markdown 只是为人类浏览仓库生成的派生视图(每次发布重新生成,手工编辑会被覆盖)。Scalar 应用内的 "What's new" 弹窗在构建期读取 JSON 结构数据。

九、prettyPrintJson:防浏览器卡死的深共享图美化

0.8.1 修复了"Show Schema"在递归 OpenAPI schema 上展开导致浏览器卡死的问题,0.9.2 进一步让被多次引用的 schema 类型完整展示而非在首次出现后渲染[Circular]

实现在 json/pretty-print-json.ts:

  • replaceCircularDependencies:只折叠当前路径上的循环引用(用祖先栈判断),因此同一个对象出现在两个兄弟位置会被完整展开两次(pretty-print-json.ts);
  • 节点预算上限MAX_EXPANDED_NODES = 100_000(pretty-print-json.ts):展开过程计数,一旦超过预算抛出哨兵错误EXPANSION_LIMIT_EXCEEDEDprettyPrintJson捕获后回退到replaceRepeatedReferences——把所有重复引用都折叠为[Circular]
  • 回退保证了无论共享图如何变形,输出都是线性规模,深共享的递归 schema 也不会冻结标签页。

值得注意:解析真实 JSON 不会产生共享引用,所以对普通解析数据,replaceRepeatedReferences的输出与普通JSON.stringify完全一致。

十、Playwright 测试基础设施:getDockerServer(0.5.3 → 0.11.2)

0.5.3 为@scalar/helpers增加了 Playwright 工具函数,0.11.2 把默认 runner 镜像升级为 Chromium-only 的scalarapi/playwright-runner:1.62.1(与工作区@playwright/test版本对齐,保证客户端与容器说同一套协议)。

实现在 playwright/docker.ts:getDockerServer返回一个 PlaywrightwebServer配置,用 Docker 启动浏览器侧服务:

const server = getDockerServer({ version: '1.62.1', port: 5001 }) // 生成命令: // docker run --name scalar-playwright --rm --platform linux/amd64 // --entrypoint="playwright" --network=host // scalarapi/playwright-runner:1.62.1 // run-server --port 5001 --host 0.0.0.0 --unsafe

源码注释特别解释了--unsafe的必要性:Playwright 始终以x-playwright-launch-options头把配置里的launchOptions转发给服务端,但服务端默认丢弃args等危险字段,除非以--unsafe启动。否则use.launchOptions里的设置在 CI(浏览器直启)生效、本地(走 docker)被静默忽略,同一测试两种渲染结果,本地快照对不上 CI。其余默认值:端口 5001、超时 120s、CI 下不复用已有服务(reuseExistingServer: !process.env.CI)。

十一、请求与代理相关的零散演进

0.8.0 的补丁还包含一批值得留意的细节改动:

  • 请求体内容类型:下拉列表先列内置类型,再列 OpenAPI operation 的额外媒体类型;标签使用 MIME 本质(不带charset);Other选项回归,用于原始 body 且不自动添加Content-Type(用户可手动设置),代码片段避免注入Content-Type: othergetDefaultHeadersfilterDisabledDefaultHeaders@scalar/workspace-store/request-example导出,API 客户端不再维护重复助手;
  • 受限头转发:浏览器会剥离DateDNTReferer等受限头,使用 Scalar 代理或 Electron 环境时,将这三者改写为X-Scalar-*头转发,既让代理带上预期上游头,又不开放完整受限头集合。共享常量定义在 http/scalar-headers.ts(0.5.2 起导出并被请求构建/发送流程消费);
  • 相对 / 非 http URL 的友好错误:API 客户端对相对 URL 或非 http 协议 URL 给出友好提示;
  • 从部分 URL 提取 server:改进了从残缺 URL 中提取服务器地址的能力;
  • setValueAtPath:基于路径数组的嵌套对象写入助手(object/set-value-at-path.ts)。

更早的版本同样体现了"去外部依赖"的主线:0.8.2 用仓库内 ESMserializeCookie助手替换了 CommonJS-only 的cookie依赖(http/serialize-cookie.ts);0.5.0 用本地实现替换了直接 CJS 的mimecurl依赖;0.4.3 用内部助手替换了pretty-bytes依赖;0.3.0 要求 Node >= 22 (LTS);0.1.0 增加了node:pathpolyfill(node/path.ts)并移除所有场景对crypto.subtle的使用;0.2.13 增加了 v1 localStorage 到 v2 IndexedDB 的迁移器。

十二、从版本历程看工具库的收敛策略

纵观 0.0.2 到 0.11.3 的演进,@scalar/helpers呈现三条清晰策略:

  1. 去依赖化cookiemimecurlpretty-bytessemver等外部依赖逐一被手写实现替代(参见 0.5.0、0.8.2、0.8.0 各条目),README 的 "dependency free" 承诺从口号变成了工程现实——减小包体、消除 CJS/ESM 互操作摩擦(0.8.2 明确提到 CommonJS-only 依赖的替换动机);
  2. 共享收敛loadCssVariablesscalar-app迁入(0.8.0)、debounce移入并支持 max wait(0.1.1)、preventPollutionisPollutionKey合并危险键清单(0.11.0)、getDefaultHeaders从 workspace-store 复用(0.8.0)——同一份逻辑只在一个包维护;
  3. 安全纵深:从 URL 白名单(0.10.0)到原型污染三重防线(0.11.0)再到applyColorMode这类 SSR 安全的主题助手(0.11.3),安全考量贯穿版本主线。

如果你的项目也在处理不可信的 JSON/OpenAPI 文档、需要安全的链接渲染、或者正在实现配置合并与 slug 生成,完全可以借鉴甚至直接复用@scalar/helpers的这些实现——所有源码都带完整单测,路径均列于上文各节,可随时深入阅读。

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询