From c9391e36401388e638068f9273bcc799a85b137a Mon Sep 17 00:00:00 2001 From: PiEgg Date: Wed, 23 Sep 2026 17:40:54 +0800 Subject: [PATCH 1/8] feat: add browser SDK for PicGo Cloud --- .github/workflows/ci.yml | 21 + .gitignore | 7 + LICENSE | 21 + README.md | 163 ++++ eslint.config.js | 11 + examples/basic.html | 46 ++ package.json | 33 + pnpm-lock.yaml | 1608 +++++++++++++++++++++++++++++++++++++ rolldown.config.ts | 7 + scripts/browser-smoke.mjs | 134 ++++ src/api-error-codes.ts | 4 + src/client.ts | 30 + src/errors.ts | 38 + src/http.ts | 355 ++++++++ src/index.ts | 9 + src/media.ts | 146 ++++ src/types.ts | 99 +++ src/upload/storage.ts | 105 +++ src/upload/task.ts | 385 +++++++++ src/upload/transport.ts | 179 +++++ src/upload/types.ts | 74 ++ tests/browser/smoke.html | 8 + tests/browser/smoke.js | 47 ++ tests/client.test.ts | 336 ++++++++ tests/storage.test.ts | 72 ++ tests/transport.test.ts | 296 +++++++ tests/upload.test.ts | 783 ++++++++++++++++++ tsconfig.build.json | 12 + tsconfig.json | 14 + vitest.config.ts | 3 + 30 files changed, 5046 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 examples/basic.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 rolldown.config.ts create mode 100644 scripts/browser-smoke.mjs create mode 100644 src/api-error-codes.ts create mode 100644 src/client.ts create mode 100644 src/errors.ts create mode 100644 src/http.ts create mode 100644 src/index.ts create mode 100644 src/media.ts create mode 100644 src/types.ts create mode 100644 src/upload/storage.ts create mode 100644 src/upload/task.ts create mode 100644 src/upload/transport.ts create mode 100644 src/upload/types.ts create mode 100644 tests/browser/smoke.html create mode 100644 tests/browser/smoke.js create mode 100644 tests/client.test.ts create mode 100644 tests/storage.test.ts create mode 100644 tests/transport.test.ts create mode 100644 tests/upload.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4cd5341 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b6c976 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +*.tgz +.DS_Store +.env* +!*.example diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..702e290 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PicGo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3854eeb --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# @picgo/cloud-sdk + +面向现代浏览器的 PicGo Cloud JavaScript SDK,使用 TypeScript 编写、Rolldown 构建,零运行时依赖。提供文件上传、分片续传和媒体管理,不包含 UI 组件。Node.js 场景请使用 PicGo-Core。 + +## 安装与使用 + +```sh +pnpm add @picgo/cloud-sdk +``` + +```ts +import { PicGoCloudClient } from '@picgo/cloud-sdk' + +const client = new PicGoCloudClient({ + token: userToken, +}) + +const media = await client.upload(file) +console.log(media.id, media.imgUrl) + +const page = await client.media.list({ limit: 20, offset: 0, search: '旅行' }) +await client.media.update(media.id, { fileName: '旅行照片.jpg' }) +await client.media.delete(media.id) +``` + +token 由使用网页的用户提供,代表该用户的账户权限。SDK 使用 `Authorization: Bearer`,不携带 Cookie,也不默认保存或打印 token。不要将站点开发者自己的 token 写进公开的前端构建产物。支持直接传入字符串,也支持 `token: () => currentToken` 或异步获取函数;每轮上传固定使用同一个 token,避免分片过程中混用账户。 + +```ts +const client = new PicGoCloudClient({ + token: async () => getCurrentUserToken(), + baseUrl: 'https://api.picgo.app', // 默认值;本地开发可指向 Worker + timeoutMs: 30_000, // 单次 API 请求超时 + uploadTimeoutMs: 120_000, // 单次文件/分片 PUT 超时 + // storage: false, // 可关闭续传记录持久化 +}) +``` + +API 使用原生 fetch,可通过 `fetch` 选项注入自定义实现。上传字节使用原生 XMLHttpRequest 以提供浏览器上传进度,不经过自定义 fetch。模块导入本身不访问 DOM;实际上传需要浏览器的 Blob/File、XMLHttpRequest、Web Crypto 和 AbortSignal.any,建议在 HTTPS 或 localhost 安全上下文使用。 + +## 上传 + +```ts +const media = await client.upload(file, { + onProgress({ phase, loaded, total, fraction, resumed }) { + console.log(phase, loaded, total, fraction, resumed) + }, +}) + +// Blob 没有文件名,需要显式指定。 +await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' }) +``` + +小于 10 MiB 使用单次预签名 PUT,大于等于 10 MiB 自动分片。分片大小使用服务端返回值,默认并发 3,可通过 `concurrency` 设置为 1–6。SDK 接受 1 byte–1 GiB,实际文件格式、类型大小限制、套餐和配额仍由服务端校验。不转换、压缩或重新编码文件。可通过 `width`、`height` 提供图片尺寸。 + +`phase` 为 `preparing`、`uploading`、`completing`、`completed`。`fraction` 仅表示文件传输比例,传输达到 100% 后还有合并和媒体入库步骤;只有 Promise 成功返回才代表上传完成。重试时传输进度可能回退。进度回调抛出的异常不会改变上传结果。 + +### 暂停、继续与取消 + +```ts +import { PicGoCloudError } from '@picgo/cloud-sdk' + +const task = client.createUpload(file, { onProgress: updateProgress }) + +pauseButton.addEventListener('click', () => task.pause()) +cancelButton.addEventListener('click', async () => { + try { + await task.cancel() + } catch (error) { + showError(error) // 网络失败时可再次调用 cancel() 重试清理 + } +}) + +async function startOrResume() { + try { + const media = await task.start() + showUploadedMedia(media) + } catch (error) { + if (error instanceof PicGoCloudError && error.kind === 'paused') return + showError(error) + } +} + +startButton.addEventListener('click', startOrResume) +resumeButton.addEventListener('click', startOrResume) +``` + +`createUpload()` 不立即发送请求。`start()` 开始上传;正在运行时重复调用会返回同一个 Promise。`pause()` 中断当前请求,当前 Promise 以 `kind: 'paused'` 拒绝;等待它结束后再次 `start()` 继续。`cancel()` 中断并清理未完成的分片会话,任务取消后需新建任务。清理请求失败会向调用方报告,保留记录以便再次尝试。取消不回滚已成功入库的媒体;已经传完但未入库的对象由服务端清理策略处理。 + +`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`;外部 signal 中断会保留续传信息,不等同于 `cancel()` 的服务端清理。已经中断的外部 signal 不能用于重新开始,应创建新任务。 + +### 断点续传 + +分片上传默认将会话及已完成分片的 ETag 写入当前站点的 localStorage,缓存有效期 24 小时。刷新页面后重新选择原文件,再调用 `upload()` 或 `createUpload().start()`,会自动恢复。SDK 以分块内容摘要识别文件,内存占用不随整个文件大小增长;准备阶段会读取文件内容。记录按 API 地址、账户和文件隔离,不包含 token、预签名 URL 或原始文件。 + +新 Worker 的 `whoami.userId` 用于账户隔离,同一账户更换 token 后仍可恢复;旧 Worker 没有该字段时使用 token 摘要隔离,更换 token 后不会命中旧记录。同一任务不允许切换账户。Web Locks 可用时阻止跨标签页同时恢复同一文件;不支持 Web Locks 时只提供当前页面内的互斥。 + +续传需要当前站点仍保留本地记录、用户提供同一文件,且服务端会话尚未失效;不支持跨站点或跨设备恢复。浏览器存储不可用时自动降级为任务内存中的续传。`storage: false` 关闭持久化,`resume: false` 让该次上传忽略持久化记录。小文件不提供跨刷新续传,但同一任务入库失败后重试不会重复 PUT。分片上传会保留已合并待入库状态,直到入库成功才清除记录。 + +SDK 只重试可安全重试的步骤,分片 PUT 使用最多 3 次额外重试和 1/2/4 秒退避,403 时重新获取签名 URL。媒体更新、删除及新建上传会话不会被静默重试。丢失分片合并响应时,优先尝试幂等的媒体入库以确认是否已经合并,避免直接创建另一份上传。 + +## 媒体管理 + +PicGo Cloud 当前的“相册”是媒体条目列表,没有相册分组实体。`media` 对应 `/api/album-items`,不使用已弃用的 `/api/media`,不提供第三方图床记录导入。 + +| 方法 | 返回值 | +| --- | --- | +| `client.whoami(options?)` | 当前用户资料,含新版服务端的 `userId` | +| `client.media.list(query?, options?)` | `{ items, total, limit, offset }` | +| `client.media.get(id, options?)` | `MediaItem` | +| `client.media.update(id, changes, options?)` | `MediaItem` | +| `client.media.updateMany(items, options?)` | `{ items, updated, skipped }` | +| `client.media.delete(id, options?)` | `{ message }` | +| `client.media.deleteMany(ids, options?)` | `{ deleted }` | +| `client.media.filters(options?)` | `{ contentTypes, types, exts }` | +| `client.media.stats(options?)` | `{ total, types }` | + +所有接口返回解包后的业务数据,失败则抛异常。`options` 可传 `signal`。列表支持 `search`、`contentType`、`type`、`ext`、`fileName`、`sort` 和 `order`,`sort` 为 `newest | oldest | fileName`,`order` 为 `asc | desc`。当前服务端 `search` 仅搜索文件名。`limit` 为 1–100,`offset` 从 0 开始;批量更新和删除每次 1–100 条,不自动拆批。批量结果保留后端处理数量,请检查 `skipped`。 + +```ts +await client.media.updateMany([ + { id: firstId, fileName: 'a.png' }, + { id: secondId, extra: { description: '截图' } }, +]) +await client.media.deleteMany([firstId, secondId]) +``` + +媒体字段沿用服务端命名,包括 `id`、`imgUrl`、`fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra`,时间戳为毫秒。更新修改媒体元数据,不修改存储中的文件内容;不允许更新 `size`、`extname`。删除是服务端软删除,目前没有恢复接口。 + +## 错误处理 + +```ts +import { PicGoCloudError } from '@picgo/cloud-sdk' + +try { + await client.upload(file) +} catch (error) { + if (error instanceof PicGoCloudError) { + console.log(error.kind, error.status, error.code) + if (error.status === 401) showTokenInput() + } else { + throw error + } +} +``` + +`PicGoCloudError` 包含错误类别 `kind`、可选 HTTP `status`、后端 `code` 和原始 `cause`。服务端部分错误没有 code,可按状态码兜底。SDK 不根据错误文案决定行为,不自动清除用户 token。浏览器可能把 CORS 拒绝表现为普通网络错误;诊断时检查 API 和 R2 两处跨域配置。 + +## 服务端接入条件 + +SDK 不会绕过浏览器跨域。配套 picgo-hub 分支 `feat-cloud-sdk` 为 SDK 使用的端点增加第三方 Bearer CORS,同时保留 Portal Cookie 和 OAuth 回调白名单。R2 Bucket 必须允许第三方 Origin 的 PUT、签名所需请求头,并通过 `ExposeHeaders` 暴露 `ETag`。具体配置及部署验证步骤见 hub 的 SDK CORS 文档。修改源码不代表线上配置已生效。 + +业务请求始终发送给配置的 API。生产 R2 直传不携带账户 token;仅 localhost/127.0.0.1 的同源、已知 Worker 上传代理路径会添加 Bearer 以支持本地开发。 + +## 开发 + +```sh +pnpm install +pnpm check +``` + +`pnpm check` 运行类型检查、ESLint、Vitest 和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。`examples/basic.html` 是可搭配本地静态服务器使用的原生浏览器示例,先执行 `pnpm build`;其中的 token 只保留在页面内存中。 + +真实浏览器协议验证可运行 `pnpm build && pnpm test:browser:serve`,然后访问 `http://localhost:41780`。这个本地测试使用三个不同 Origin 模拟网页、API 和存储,检查真实 CORS 预检、XHR 进度、ETag、媒体管理以及合并后入库失败的恢复,不会访问真实账户或写入云端。页面显示 `passed: true` 代表通过,终端 Ctrl+C 停止服务。每次重新测试前重启测试服务,以重置模拟状态。 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c0e52bf --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,11 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**', 'coverage/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { languageOptions: { globals: globals.browser } }, + { files: ['scripts/**/*.mjs'], languageOptions: { globals: globals.node } }, +) diff --git a/examples/basic.html b/examples/basic.html new file mode 100644 index 0000000..19c59c3 --- /dev/null +++ b/examples/basic.html @@ -0,0 +1,46 @@ + + + + +PicGo Cloud SDK 示例 + +

PicGo Cloud SDK

+

提供你自己的 token,选择文件上传。暂停后可继续,刷新后重新选择同一文件可续传。

+ + + + + +

+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f04ba17
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "@picgo/cloud-sdk",
+  "version": "0.1.0",
+  "description": "PicGo Cloud uploads and media management for modern browsers",
+  "type": "module",
+  "license": "MIT",
+  "repository": { "type": "git", "url": "git+https://github.com/PicGo/picgo-cloud-sdk.git" },
+  "sideEffects": false,
+  "files": ["dist", "README.md", "LICENSE"],
+  "main": "./dist/index.js",
+  "module": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
+  "scripts": {
+    "build": "rolldown -c && tsc -p tsconfig.build.json",
+    "typecheck": "tsc --noEmit",
+    "lint": "eslint .",
+    "test": "vitest run",
+    "test:browser:serve": "node scripts/browser-smoke.mjs",
+    "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build",
+    "prepublishOnly": "pnpm check"
+  },
+  "packageManager": "pnpm@11.7.0",
+  "devDependencies": {
+    "@eslint/js": "^9.39.1",
+    "eslint": "^9.39.1",
+    "globals": "^16.5.0",
+    "rolldown": "^1.2.9",
+    "typescript": "^5.9.3",
+    "typescript-eslint": "^8.49.0",
+    "vitest": "~4.1.10"
+  }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..9a53585
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,1608 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    devDependencies:
+      '@eslint/js':
+        specifier: ^9.39.1
+        version: 9.39.5
+      eslint:
+        specifier: ^9.39.1
+        version: 9.39.5
+      globals:
+        specifier: ^16.5.0
+        version: 16.5.0
+      rolldown:
+        specifier: ^1.2.9
+        version: 1.2.9
+      typescript:
+        specifier: ^5.9.3
+        version: 5.9.3
+      typescript-eslint:
+        specifier: ^8.49.0
+        version: 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      vitest:
+        specifier: ~4.1.10
+        version: 4.1.11(vite@8.3.0)
+
+packages:
+
+  '@eslint-community/eslint-utils@4.10.1':
+    resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/regexpp@4.12.2':
+    resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/config-array@0.21.2':
+    resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/config-helpers@0.4.2':
+    resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/core@0.17.0':
+    resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/eslintrc@3.3.7':
+    resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/js@9.39.5':
+    resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/object-schema@2.1.7':
+    resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/plugin-kit@0.4.1':
+    resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@humanfs/core@0.19.2':
+    resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/node@0.16.8':
+    resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/types@0.15.0':
+    resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanwhocodes/module-importer@1.0.1':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/retry@0.4.3':
+    resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+    engines: {node: '>=18.18'}
+
+  '@jridgewell/sourcemap-codec@1.6.0':
+    resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+  '@oxc-project/types@0.150.0':
+    resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
+
+  '@rolldown/binding-android-arm-eabi@1.2.9':
+    resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm]
+    os: [android]
+
+  '@rolldown/binding-android-arm64@1.2.9':
+    resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [android]
+
+  '@rolldown/binding-darwin-arm64@1.2.9':
+    resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rolldown/binding-darwin-x64@1.2.9':
+    resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rolldown/binding-freebsd-x64@1.2.9':
+    resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rolldown/binding-linux-arm-gnueabihf@1.2.9':
+    resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm]
+    os: [linux]
+
+  '@rolldown/binding-linux-arm64-gnu@1.2.9':
+    resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rolldown/binding-linux-arm64-musl@1.2.9':
+    resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rolldown/binding-linux-ppc64-gnu@1.2.9':
+    resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rolldown/binding-linux-s390x-gnu@1.2.9':
+    resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rolldown/binding-linux-x64-gnu@1.2.9':
+    resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rolldown/binding-linux-x64-musl@1.2.9':
+    resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rolldown/binding-openharmony-arm64@1.2.9':
+    resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@rolldown/binding-win32-arm64-msvc@1.2.9':
+    resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rolldown/binding-win32-x64-msvc@1.2.9':
+    resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [win32]
+
+  '@rolldown/pluginutils@1.0.1':
+    resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+  '@standard-schema/spec@1.1.0':
+    resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+  '@types/chai@5.2.3':
+    resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+  '@types/deep-eql@4.0.2':
+    resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+  '@types/estree@1.0.9':
+    resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+  '@types/json-schema@7.0.15':
+    resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+  '@typescript-eslint/eslint-plugin@8.70.1':
+    resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.70.1
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/parser@8.70.1':
+    resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/project-service@8.70.1':
+    resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/scope-manager@8.70.1':
+    resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/tsconfig-utils@8.70.1':
+    resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/type-utils@8.70.1':
+    resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/types@8.70.1':
+    resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/typescript-estree@8.70.1':
+    resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/utils@8.70.1':
+    resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/visitor-keys@8.70.1':
+    resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@vitest/expect@4.1.11':
+    resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
+
+  '@vitest/mocker@4.1.11':
+    resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
+    peerDependencies:
+      msw: ^2.4.9
+      vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+    peerDependenciesMeta:
+      msw:
+        optional: true
+      vite:
+        optional: true
+
+  '@vitest/pretty-format@4.1.11':
+    resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
+
+  '@vitest/runner@4.1.11':
+    resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
+
+  '@vitest/snapshot@4.1.11':
+    resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
+
+  '@vitest/spy@4.1.11':
+    resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
+
+  '@vitest/utils@4.1.11':
+    resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.18.0:
+    resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  ajv@6.15.0:
+    resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  balanced-match@4.0.4:
+    resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+    engines: {node: 18 || 20 || >=22}
+
+  brace-expansion@1.1.21:
+    resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
+
+  brace-expansion@5.0.12:
+    resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
+    engines: {node: 20 || >=22}
+
+  callsites@3.1.0:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  chai@6.2.2:
+    resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+    engines: {node: '>=18'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  convert-source-map@2.0.0:
+    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  debug@4.4.3:
+    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  deep-is@0.1.4:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  detect-libc@2.1.2:
+    resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+    engines: {node: '>=8'}
+
+  es-module-lexer@2.3.2:
+    resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eslint-scope@8.4.0:
+    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint-visitor-keys@3.4.3:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint-visitor-keys@4.2.1:
+    resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint-visitor-keys@5.0.1:
+    resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+    engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+  eslint@9.39.5:
+    resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  espree@10.4.0:
+    resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  esquery@1.7.0:
+    resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+    engines: {node: '>=0.10'}
+
+  esrecurse@4.3.0:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+    engines: {node: '>=4.0'}
+
+  estraverse@5.3.0:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+    engines: {node: '>=4.0'}
+
+  estree-walker@3.0.3:
+    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  expect-type@1.4.0:
+    resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+    engines: {node: '>=12.0.0'}
+
+  fast-deep-equal@3.1.3:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  fdir@6.5.0:
+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+    engines: {node: '>=12.0.0'}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
+  file-entry-cache@8.0.0:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  flat-cache@4.0.1:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
+
+  flatted@3.4.4:
+    resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  globals@16.5.0:
+    resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
+    engines: {node: '>=18'}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  ignore@7.0.9:
+    resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
+    engines: {node: '>= 4'}
+
+  import-fresh@3.3.1:
+    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+    engines: {node: '>=6'}
+
+  imurmurhash@0.1.4:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+    engines: {node: '>=0.8.19'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  js-yaml@4.3.2:
+    resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+    hasBin: true
+
+  json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  json-schema-traverse@0.4.1:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  json-stable-stringify-without-jsonify@1.0.1:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  levn@0.4.1:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  lightningcss-android-arm64@1.33.0:
+    resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [android]
+
+  lightningcss-darwin-arm64@1.33.0:
+    resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+
+  lightningcss-darwin-x64@1.33.0:
+    resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [darwin]
+
+  lightningcss-freebsd-x64@1.33.0:
+    resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+
+  lightningcss-linux-arm-gnueabihf@1.33.0:
+    resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  lightningcss-linux-arm64-gnu@1.33.0:
+    resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  lightningcss-linux-arm64-musl@1.33.0:
+    resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  lightningcss-linux-x64-gnu@1.33.0:
+    resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  lightningcss-linux-x64-musl@1.33.0:
+    resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  lightningcss-win32-arm64-msvc@1.33.0:
+    resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [win32]
+
+  lightningcss-win32-x64-msvc@1.33.0:
+    resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [win32]
+
+  lightningcss@1.33.0:
+    resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+    engines: {node: '>= 12.0.0'}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  lodash.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  magic-string@0.30.21:
+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+  minimatch@10.2.6:
+    resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
+    engines: {node: 18 || 20 || >=22}
+
+  minimatch@3.1.5:
+    resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  nanoid@3.3.19:
+    resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  natural-compare@1.4.0:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  obug@2.2.1:
+    resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
+    engines: {node: '>=12.20.0'}
+
+  optionator@0.9.4:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+    engines: {node: '>= 0.8.0'}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  parent-module@1.0.1:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+    engines: {node: '>=6'}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  path-key@3.1.1:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
+  pathe@2.0.3:
+    resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@4.0.7:
+    resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+    engines: {node: '>=12'}
+
+  postcss@8.5.28:
+    resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  rolldown@1.2.9:
+    resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    hasBin: true
+
+  semver@7.8.5:
+    resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  shebang-command@2.0.0:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  shebang-regex@3.0.0:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
+  siginfo@2.0.0:
+    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  stackback@0.0.2:
+    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+  std-env@4.2.0:
+    resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  tinybench@2.9.0:
+    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+  tinyexec@1.3.1:
+    resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
+    engines: {node: '>=18'}
+
+  tinyglobby@0.2.17:
+    resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+    engines: {node: '>=12.0.0'}
+
+  tinyrainbow@3.1.1:
+    resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
+    engines: {node: '>=14.0.0'}
+
+  ts-api-utils@2.5.0:
+    resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
+  type-check@0.4.0:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  typescript-eslint@8.70.1:
+    resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  vite@8.3.0:
+    resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^20.19.0 || >=22.12.0
+      '@vitejs/devtools': ^0.7.1
+      esbuild: ^0.27.0 || ^0.28.0
+      jiti: '>=1.21.0'
+      less: ^4.0.0
+      sass: ^1.70.0
+      sass-embedded: ^1.70.0
+      stylus: '>=0.54.8'
+      sugarss: ^5.0.0
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      '@vitejs/devtools':
+        optional: true
+      esbuild:
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
+  vitest@4.1.11:
+    resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
+    engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@opentelemetry/api': ^1.9.0
+      '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+      '@vitest/browser-playwright': 4.1.11
+      '@vitest/browser-preview': 4.1.11
+      '@vitest/browser-webdriverio': 4.1.11
+      '@vitest/coverage-istanbul': 4.1.11
+      '@vitest/coverage-v8': 4.1.11
+      '@vitest/ui': 4.1.11
+      happy-dom: '*'
+      jsdom: '*'
+      vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@opentelemetry/api':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser-playwright':
+        optional: true
+      '@vitest/browser-preview':
+        optional: true
+      '@vitest/browser-webdriverio':
+        optional: true
+      '@vitest/coverage-istanbul':
+        optional: true
+      '@vitest/coverage-v8':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  why-is-node-running@2.3.0:
+    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
+    dependencies:
+      eslint: 9.39.5
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.12.2': {}
+
+  '@eslint/config-array@0.21.2':
+    dependencies:
+      '@eslint/object-schema': 2.1.7
+      debug: 4.4.3
+      minimatch: 3.1.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/config-helpers@0.4.2':
+    dependencies:
+      '@eslint/core': 0.17.0
+
+  '@eslint/core@0.17.0':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
+  '@eslint/eslintrc@3.3.7':
+    dependencies:
+      ajv: 6.15.0
+      debug: 4.4.3
+      espree: 10.4.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.3.2
+      minimatch: 3.1.5
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@9.39.5': {}
+
+  '@eslint/object-schema@2.1.7': {}
+
+  '@eslint/plugin-kit@0.4.1':
+    dependencies:
+      '@eslint/core': 0.17.0
+      levn: 0.4.1
+
+  '@humanfs/core@0.19.2':
+    dependencies:
+      '@humanfs/types': 0.15.0
+
+  '@humanfs/node@0.16.8':
+    dependencies:
+      '@humanfs/core': 0.19.2
+      '@humanfs/types': 0.15.0
+      '@humanwhocodes/retry': 0.4.3
+
+  '@humanfs/types@0.15.0': {}
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/retry@0.4.3': {}
+
+  '@jridgewell/sourcemap-codec@1.6.0': {}
+
+  '@oxc-project/types@0.150.0': {}
+
+  '@rolldown/binding-android-arm-eabi@1.2.9':
+    optional: true
+
+  '@rolldown/binding-android-arm64@1.2.9':
+    optional: true
+
+  '@rolldown/binding-darwin-arm64@1.2.9':
+    optional: true
+
+  '@rolldown/binding-darwin-x64@1.2.9':
+    optional: true
+
+  '@rolldown/binding-freebsd-x64@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-arm-gnueabihf@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-arm64-gnu@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-arm64-musl@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-ppc64-gnu@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-s390x-gnu@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-x64-gnu@1.2.9':
+    optional: true
+
+  '@rolldown/binding-linux-x64-musl@1.2.9':
+    optional: true
+
+  '@rolldown/binding-openharmony-arm64@1.2.9':
+    optional: true
+
+  '@rolldown/binding-win32-arm64-msvc@1.2.9':
+    optional: true
+
+  '@rolldown/binding-win32-x64-msvc@1.2.9':
+    optional: true
+
+  '@rolldown/pluginutils@1.0.1': {}
+
+  '@standard-schema/spec@1.1.0': {}
+
+  '@types/chai@5.2.3':
+    dependencies:
+      '@types/deep-eql': 4.0.2
+      assertion-error: 2.0.1
+
+  '@types/deep-eql@4.0.2': {}
+
+  '@types/estree@1.0.9': {}
+
+  '@types/json-schema@7.0.15': {}
+
+  '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.2
+      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 8.70.1
+      '@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.70.1
+      eslint: 9.39.5
+      ignore: 7.0.9
+      natural-compare: 1.4.0
+      ts-api-utils: 2.5.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.70.1
+      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.70.1
+      debug: 4.4.3
+      eslint: 9.39.5
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/project-service@8.70.1(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/types': 8.70.1
+      debug: 4.4.3
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.70.1':
+    dependencies:
+      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/visitor-keys': 8.70.1
+
+  '@typescript-eslint/tsconfig-utils@8.70.1(typescript@5.9.3)':
+    dependencies:
+      typescript: 5.9.3
+
+  '@typescript-eslint/type-utils@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      debug: 4.4.3
+      eslint: 9.39.5
+      ts-api-utils: 2.5.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/types@8.70.1': {}
+
+  '@typescript-eslint/typescript-estree@8.70.1(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/project-service': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/visitor-keys': 8.70.1
+      debug: 4.4.3
+      minimatch: 10.2.6
+      semver: 7.8.5
+      tinyglobby: 0.2.17
+      ts-api-utils: 2.5.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@typescript-eslint/scope-manager': 8.70.1
+      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
+      eslint: 9.39.5
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/visitor-keys@8.70.1':
+    dependencies:
+      '@typescript-eslint/types': 8.70.1
+      eslint-visitor-keys: 5.0.1
+
+  '@vitest/expect@4.1.11':
+    dependencies:
+      '@standard-schema/spec': 1.1.0
+      '@types/chai': 5.2.3
+      '@vitest/spy': 4.1.11
+      '@vitest/utils': 4.1.11
+      chai: 6.2.2
+      tinyrainbow: 3.1.1
+
+  '@vitest/mocker@4.1.11(vite@8.3.0)':
+    dependencies:
+      '@vitest/spy': 4.1.11
+      estree-walker: 3.0.3
+      magic-string: 0.30.21
+    optionalDependencies:
+      vite: 8.3.0
+
+  '@vitest/pretty-format@4.1.11':
+    dependencies:
+      tinyrainbow: 3.1.1
+
+  '@vitest/runner@4.1.11':
+    dependencies:
+      '@vitest/utils': 4.1.11
+      pathe: 2.0.3
+
+  '@vitest/snapshot@4.1.11':
+    dependencies:
+      '@vitest/pretty-format': 4.1.11
+      '@vitest/utils': 4.1.11
+      magic-string: 0.30.21
+      pathe: 2.0.3
+
+  '@vitest/spy@4.1.11': {}
+
+  '@vitest/utils@4.1.11':
+    dependencies:
+      '@vitest/pretty-format': 4.1.11
+      convert-source-map: 2.0.0
+      tinyrainbow: 3.1.1
+
+  acorn-jsx@5.3.2(acorn@8.18.0):
+    dependencies:
+      acorn: 8.18.0
+
+  acorn@8.18.0: {}
+
+  ajv@6.15.0:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-json-stable-stringify: 2.1.0
+      json-schema-traverse: 0.4.1
+      uri-js: 4.4.1
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  argparse@2.0.1: {}
+
+  assertion-error@2.0.1: {}
+
+  balanced-match@1.0.2: {}
+
+  balanced-match@4.0.4: {}
+
+  brace-expansion@1.1.21:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@5.0.12:
+    dependencies:
+      balanced-match: 4.0.4
+
+  callsites@3.1.0: {}
+
+  chai@6.2.2: {}
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  concat-map@0.0.1: {}
+
+  convert-source-map@2.0.0: {}
+
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  debug@4.4.3:
+    dependencies:
+      ms: 2.1.3
+
+  deep-is@0.1.4: {}
+
+  detect-libc@2.1.2: {}
+
+  es-module-lexer@2.3.2: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  eslint-scope@8.4.0:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint-visitor-keys@4.2.1: {}
+
+  eslint-visitor-keys@5.0.1: {}
+
+  eslint@9.39.5:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@eslint-community/regexpp': 4.12.2
+      '@eslint/config-array': 0.21.2
+      '@eslint/config-helpers': 0.4.2
+      '@eslint/core': 0.17.0
+      '@eslint/eslintrc': 3.3.7
+      '@eslint/js': 9.39.5
+      '@eslint/plugin-kit': 0.4.1
+      '@humanfs/node': 0.16.8
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.3
+      '@types/estree': 1.0.9
+      ajv: 6.15.0
+      chalk: 4.1.2
+      cross-spawn: 7.0.6
+      debug: 4.4.3
+      escape-string-regexp: 4.0.0
+      eslint-scope: 8.4.0
+      eslint-visitor-keys: 4.2.1
+      espree: 10.4.0
+      esquery: 1.7.0
+      esutils: 2.0.3
+      fast-deep-equal: 3.1.3
+      file-entry-cache: 8.0.0
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      ignore: 5.3.2
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      json-stable-stringify-without-jsonify: 1.0.1
+      lodash.merge: 4.6.2
+      minimatch: 3.1.5
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@10.4.0:
+    dependencies:
+      acorn: 8.18.0
+      acorn-jsx: 5.3.2(acorn@8.18.0)
+      eslint-visitor-keys: 4.2.1
+
+  esquery@1.7.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  estree-walker@3.0.3:
+    dependencies:
+      '@types/estree': 1.0.9
+
+  esutils@2.0.3: {}
+
+  expect-type@1.4.0: {}
+
+  fast-deep-equal@3.1.3: {}
+
+  fast-json-stable-stringify@2.1.0: {}
+
+  fast-levenshtein@2.0.6: {}
+
+  fdir@6.5.0(picomatch@4.0.7):
+    optionalDependencies:
+      picomatch: 4.0.7
+
+  file-entry-cache@8.0.0:
+    dependencies:
+      flat-cache: 4.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat-cache@4.0.1:
+    dependencies:
+      flatted: 3.4.4
+      keyv: 4.5.4
+
+  flatted@3.4.4: {}
+
+  fsevents@2.3.3:
+    optional: true
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  globals@14.0.0: {}
+
+  globals@16.5.0: {}
+
+  has-flag@4.0.0: {}
+
+  ignore@5.3.2: {}
+
+  ignore@7.0.9: {}
+
+  import-fresh@3.3.1:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  imurmurhash@0.1.4: {}
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  isexe@2.0.0: {}
+
+  js-yaml@4.3.2:
+    dependencies:
+      argparse: 2.0.1
+
+  json-buffer@3.0.1: {}
+
+  json-schema-traverse@0.4.1: {}
+
+  json-stable-stringify-without-jsonify@1.0.1: {}
+
+  keyv@4.5.4:
+    dependencies:
+      json-buffer: 3.0.1
+
+  levn@0.4.1:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  lightningcss-android-arm64@1.33.0:
+    optional: true
+
+  lightningcss-darwin-arm64@1.33.0:
+    optional: true
+
+  lightningcss-darwin-x64@1.33.0:
+    optional: true
+
+  lightningcss-freebsd-x64@1.33.0:
+    optional: true
+
+  lightningcss-linux-arm-gnueabihf@1.33.0:
+    optional: true
+
+  lightningcss-linux-arm64-gnu@1.33.0:
+    optional: true
+
+  lightningcss-linux-arm64-musl@1.33.0:
+    optional: true
+
+  lightningcss-linux-x64-gnu@1.33.0:
+    optional: true
+
+  lightningcss-linux-x64-musl@1.33.0:
+    optional: true
+
+  lightningcss-win32-arm64-msvc@1.33.0:
+    optional: true
+
+  lightningcss-win32-x64-msvc@1.33.0:
+    optional: true
+
+  lightningcss@1.33.0:
+    dependencies:
+      detect-libc: 2.1.2
+    optionalDependencies:
+      lightningcss-android-arm64: 1.33.0
+      lightningcss-darwin-arm64: 1.33.0
+      lightningcss-darwin-x64: 1.33.0
+      lightningcss-freebsd-x64: 1.33.0
+      lightningcss-linux-arm-gnueabihf: 1.33.0
+      lightningcss-linux-arm64-gnu: 1.33.0
+      lightningcss-linux-arm64-musl: 1.33.0
+      lightningcss-linux-x64-gnu: 1.33.0
+      lightningcss-linux-x64-musl: 1.33.0
+      lightningcss-win32-arm64-msvc: 1.33.0
+      lightningcss-win32-x64-msvc: 1.33.0
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash.merge@4.6.2: {}
+
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.6.0
+
+  minimatch@10.2.6:
+    dependencies:
+      brace-expansion: 5.0.12
+
+  minimatch@3.1.5:
+    dependencies:
+      brace-expansion: 1.1.21
+
+  ms@2.1.3: {}
+
+  nanoid@3.3.19: {}
+
+  natural-compare@1.4.0: {}
+
+  obug@2.2.1: {}
+
+  optionator@0.9.4:
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+      word-wrap: 1.2.5
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  parent-module@1.0.1:
+    dependencies:
+      callsites: 3.1.0
+
+  path-exists@4.0.0: {}
+
+  path-key@3.1.1: {}
+
+  pathe@2.0.3: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@4.0.7: {}
+
+  postcss@8.5.28:
+    dependencies:
+      nanoid: 3.3.19
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  prelude-ls@1.2.1: {}
+
+  punycode@2.3.1: {}
+
+  resolve-from@4.0.0: {}
+
+  rolldown@1.2.9:
+    dependencies:
+      '@oxc-project/types': 0.150.0
+      '@rolldown/pluginutils': 1.0.1
+    optionalDependencies:
+      '@rolldown/binding-android-arm-eabi': 1.2.9
+      '@rolldown/binding-android-arm64': 1.2.9
+      '@rolldown/binding-darwin-arm64': 1.2.9
+      '@rolldown/binding-darwin-x64': 1.2.9
+      '@rolldown/binding-freebsd-x64': 1.2.9
+      '@rolldown/binding-linux-arm-gnueabihf': 1.2.9
+      '@rolldown/binding-linux-arm64-gnu': 1.2.9
+      '@rolldown/binding-linux-arm64-musl': 1.2.9
+      '@rolldown/binding-linux-ppc64-gnu': 1.2.9
+      '@rolldown/binding-linux-s390x-gnu': 1.2.9
+      '@rolldown/binding-linux-x64-gnu': 1.2.9
+      '@rolldown/binding-linux-x64-musl': 1.2.9
+      '@rolldown/binding-openharmony-arm64': 1.2.9
+      '@rolldown/binding-win32-arm64-msvc': 1.2.9
+      '@rolldown/binding-win32-x64-msvc': 1.2.9
+
+  semver@7.8.5: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  siginfo@2.0.0: {}
+
+  source-map-js@1.2.1: {}
+
+  stackback@0.0.2: {}
+
+  std-env@4.2.0: {}
+
+  strip-json-comments@3.1.1: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  tinybench@2.9.0: {}
+
+  tinyexec@1.3.1: {}
+
+  tinyglobby@0.2.17:
+    dependencies:
+      fdir: 6.5.0(picomatch@4.0.7)
+      picomatch: 4.0.7
+
+  tinyrainbow@3.1.1: {}
+
+  ts-api-utils@2.5.0(typescript@5.9.3):
+    dependencies:
+      typescript: 5.9.3
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  typescript-eslint@8.70.1(eslint@9.39.5)(typescript@5.9.3):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      eslint: 9.39.5
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  typescript@5.9.3: {}
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  vite@8.3.0:
+    dependencies:
+      lightningcss: 1.33.0
+      picomatch: 4.0.7
+      postcss: 8.5.28
+      rolldown: 1.2.9
+      tinyglobby: 0.2.17
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  vitest@4.1.11(vite@8.3.0):
+    dependencies:
+      '@vitest/expect': 4.1.11
+      '@vitest/mocker': 4.1.11(vite@8.3.0)
+      '@vitest/pretty-format': 4.1.11
+      '@vitest/runner': 4.1.11
+      '@vitest/snapshot': 4.1.11
+      '@vitest/spy': 4.1.11
+      '@vitest/utils': 4.1.11
+      es-module-lexer: 2.3.2
+      expect-type: 1.4.0
+      magic-string: 0.30.21
+      obug: 2.2.1
+      pathe: 2.0.3
+      picomatch: 4.0.7
+      std-env: 4.2.0
+      tinybench: 2.9.0
+      tinyexec: 1.3.1
+      tinyglobby: 0.2.17
+      tinyrainbow: 3.1.1
+      vite: 8.3.0
+      why-is-node-running: 2.3.0
+    transitivePeerDependencies:
+      - msw
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  why-is-node-running@2.3.0:
+    dependencies:
+      siginfo: 2.0.0
+      stackback: 0.0.2
+
+  word-wrap@1.2.5: {}
+
+  yocto-queue@0.1.0: {}
diff --git a/rolldown.config.ts b/rolldown.config.ts
new file mode 100644
index 0000000..dfee90e
--- /dev/null
+++ b/rolldown.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'rolldown'
+
+export default defineConfig({
+  input: 'src/index.ts',
+  platform: 'browser',
+  output: { dir: 'dist', format: 'esm', sourcemap: true, minify: true },
+})
diff --git a/scripts/browser-smoke.mjs b/scripts/browser-smoke.mjs
new file mode 100644
index 0000000..44040dc
--- /dev/null
+++ b/scripts/browser-smoke.mjs
@@ -0,0 +1,134 @@
+// Local protocol fixture: no real tokens, cloud storage, or database writes.
+import { createServer } from 'node:http'
+import { readFile } from 'node:fs/promises'
+
+const pagePort = 41780
+const apiPort = 41781
+const storagePort = 41782
+const sessions = new Map()
+const media = new Map()
+const counts = { puts: 0, preflights: 0, credentialsLeaked: 0, merges: 0 }
+let sequence = 0
+let failRegistration = true
+
+function json(response, body, status = 200) {
+  response.writeHead(status, { 'Content-Type': 'application/json' })
+  response.end(JSON.stringify(body))
+}
+
+async function body(request) {
+  const chunks = []
+  for await (const chunk of request) chunks.push(chunk)
+  return JSON.parse(Buffer.concat(chunks).toString() || '{}')
+}
+
+function cors(request, response) {
+  response.setHeader('Access-Control-Allow-Origin', '*')
+  response.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Signature')
+  response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, PUT, OPTIONS')
+  response.setHeader('Access-Control-Expose-Headers', 'ETag')
+  if (request.method === 'OPTIONS') {
+    counts.preflights++
+    response.writeHead(204)
+    response.end()
+    return true
+  }
+  return false
+}
+
+const api = createServer(async (request, response) => {
+  try {
+    if (cors(request, response)) return
+    if (request.headers.cookie) counts.credentialsLeaked++
+    if (request.headers.authorization !== 'Bearer browser-smoke-token') {
+      json(response, { success: false, message: 'Unauthorized' }, 401)
+      return
+    }
+    const url = new URL(request.url, `http://localhost:${apiPort}`)
+    const input = await body(request)
+    if (url.pathname === '/api/whoami') {
+      json(response, { success: true, data: { userId: 'browser-user', user: 'Browser test', avatar: null, plan: 1, autoImport: false } })
+    } else if (url.pathname === '/api/upload/presign' || url.pathname === '/api/upload/multipart/initiate') {
+      const id = String(++sequence)
+      const multipart = url.pathname.endsWith('initiate')
+      sessions.set(id, { ...input, bytes: 0, parts: new Map(), merged: !multipart })
+      const common = { success: true, objectKey: id, publicId: id }
+      const headers = { 'Content-Type': 'image/png', 'X-Signature': 'signed' }
+      if (multipart) {
+        const partSize = 8 * 1024 * 1024
+        const partCount = Math.ceil(input.sizeBytes / partSize)
+        json(response, { ...common, uploadId: id, partSize, partCount,
+          parts: Array.from({ length: partCount }, (_, index) => ({ partNumber: index + 1, method: 'PUT', headers,
+            url: `http://localhost:${storagePort}/${id}/${index + 1}` })) })
+      } else {
+        json(response, { ...common, method: 'PUT', headers, uploadUrl: `http://localhost:${storagePort}/${id}/1` })
+      }
+    } else if (url.pathname === '/api/upload/multipart/complete') {
+      const session = sessions.get(input.uploadId)
+      const valid = input.parts.every(part => session.parts.get(part.partNumber) === part.etag)
+      if (!valid || session.bytes !== session.sizeBytes) throw new Error('Incorrect multipart payload')
+      session.merged = true
+      counts.merges++
+      json(response, { success: true })
+    } else if (url.pathname === '/api/album-items/complete') {
+      const session = sessions.get(input.objectKey)
+      if (session.filename === 'large.png' && failRegistration) {
+        failRegistration = false
+        json(response, { success: false, code: 'TEST_REGISTRATION_FAILURE', message: 'Retry registration' }, 400)
+        return
+      }
+      if (!session.merged || session.bytes !== session.sizeBytes) throw new Error('Registration before upload')
+      const item = { id: input.publicId, imgUrl: `https://media.example/${input.publicId}.png`, fileName: input.filename }
+      media.set(item.id, item)
+      json(response, { success: true, data: { item } })
+    } else if (url.pathname === '/api/album-items' && request.method === 'GET') {
+      json(response, { success: true, data: { items: [...media.values()], total: media.size, limit: 20, offset: 0 } })
+    } else if (url.pathname.startsWith('/api/album-items/') && request.method === 'PATCH') {
+      const id = url.pathname.split('/').at(-1)
+      const item = { ...media.get(id), ...input }
+      media.set(id, item)
+      json(response, { success: true, data: { item } })
+    } else if (url.pathname.startsWith('/api/album-items/') && request.method === 'DELETE') {
+      media.delete(url.pathname.split('/').at(-1))
+      json(response, { success: true, data: { message: 'Deleted' } })
+    } else if (url.pathname === '/counts') json(response, counts)
+    else json(response, { success: false, message: 'Not found' }, 404)
+  } catch (error) { json(response, { success: false, message: error.message }, 500) }
+})
+
+const storage = createServer(async (request, response) => {
+  if (cors(request, response)) return
+  if (request.headers.authorization || request.headers.cookie) counts.credentialsLeaked++
+  if (request.method !== 'PUT' || request.headers['x-signature'] !== 'signed') {
+    response.writeHead(403); response.end(); return
+  }
+  const [, id, number] = request.url.split('/')
+  const session = sessions.get(id)
+  for await (const chunk of request) session.bytes += chunk.length
+  const etag = `"part-${number}"`
+  session.parts.set(Number(number), etag)
+  counts.puts++
+  response.writeHead(200, { ETag: etag })
+  response.end()
+})
+
+const page = createServer(async (request, response) => {
+  const files = {
+    '/': ['../tests/browser/smoke.html', 'text/html'],
+    '/smoke.js': ['../tests/browser/smoke.js', 'text/javascript'],
+    '/dist/index.js': ['../dist/index.js', 'text/javascript'],
+  }
+  const entry = files[request.url]
+  if (!entry) { response.writeHead(404); response.end(); return }
+  try {
+    response.writeHead(200, { 'Content-Type': entry[1] })
+    response.end(await readFile(new URL(entry[0], import.meta.url)))
+  } catch { response.writeHead(500); response.end('Run pnpm build first') }
+})
+
+api.listen(apiPort, '127.0.0.1')
+storage.listen(storagePort, '127.0.0.1')
+page.listen(pagePort, '127.0.0.1', () => console.log(`Browser smoke fixture: http://localhost:${pagePort}`))
+for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
+  api.close(); storage.close(); page.close()
+})
diff --git a/src/api-error-codes.ts b/src/api-error-codes.ts
new file mode 100644
index 0000000..9e0972b
--- /dev/null
+++ b/src/api-error-codes.ts
@@ -0,0 +1,4 @@
+/** Only codes used by SDK control flow, matching picgo-hub @picgo/shared. */
+export const ApiErrorCode = {
+  MultipartPartMismatch: 'MULTIPART_PART_MISMATCH',
+} as const
diff --git a/src/client.ts b/src/client.ts
new file mode 100644
index 0000000..1d76932
--- /dev/null
+++ b/src/client.ts
@@ -0,0 +1,30 @@
+import { HttpClient } from './http.js'
+import type { HttpClientConfig } from './http.js'
+import { MediaService } from './media.js'
+import type { RequestOptions, WhoAmI, MediaItem } from './types.js'
+import { UploadTask } from './upload/task.js'
+import type { UploadConfig, UploadOptions } from './upload/types.js'
+
+export interface PicGoCloudClientOptions extends HttpClientConfig, UploadConfig {}
+
+export class PicGoCloudClient {
+  readonly media: MediaService
+  private readonly http: HttpClient
+
+  constructor(private readonly options: PicGoCloudClientOptions) {
+    this.http = new HttpClient(options)
+    this.media = new MediaService(this.http)
+  }
+
+  whoami(options?: RequestOptions): Promise {
+    return this.http.request('/api/whoami', options)
+  }
+
+  createUpload(file: Blob, options?: UploadOptions): UploadTask {
+    return new UploadTask(this.http, file, options, this.options)
+  }
+
+  async upload(file: Blob, options?: UploadOptions): Promise {
+    return this.createUpload(file, options).start()
+  }
+}
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..7bc2bab
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,38 @@
+export type PicGoCloudErrorKind =
+  | 'api'
+  | 'authentication'
+  | 'network'
+  | 'timeout'
+  | 'aborted'
+  | 'paused'
+  | 'validation'
+  | 'protocol'
+  | 'storage'
+  | 'upload'
+
+export interface PicGoCloudErrorOptions {
+  kind: PicGoCloudErrorKind
+  status?: number
+  code?: string
+  cause?: unknown
+}
+
+export class PicGoCloudError extends Error {
+  readonly kind: PicGoCloudErrorKind
+  readonly status?: number
+  readonly code?: string
+  override readonly cause?: unknown
+
+  constructor(message: string, options: PicGoCloudErrorOptions) {
+    super(message, options.cause === undefined ? undefined : { cause: options.cause })
+    this.name = 'PicGoCloudError'
+    this.kind = options.kind
+    this.status = options.status
+    this.code = options.code
+    this.cause = options.cause
+  }
+}
+
+export function isPicGoCloudError(error: unknown): error is PicGoCloudError {
+  return error instanceof PicGoCloudError
+}
diff --git a/src/http.ts b/src/http.ts
new file mode 100644
index 0000000..30627d3
--- /dev/null
+++ b/src/http.ts
@@ -0,0 +1,355 @@
+import { PicGoCloudError, isPicGoCloudError } from './errors.js'
+import type { TokenProvider } from './types.js'
+
+const DEFAULT_BASE_URL = 'https://api.picgo.app'
+const DEFAULT_TIMEOUT_MS = 30_000
+
+export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
+
+export interface HttpClientConfig {
+  token: TokenProvider
+  baseUrl?: string
+  fetch?: typeof fetch
+  timeoutMs?: number
+}
+
+export interface HttpRequestOptions {
+  method?: HttpMethod
+  body?: unknown
+  signal?: AbortSignal
+}
+
+type ApiEnvelope = {
+  success?: unknown
+  data?: unknown
+  code?: unknown
+  message?: unknown
+} & Record
+
+function normalizeBaseUrl(value: string): string {
+  let url: URL
+  try {
+    url = new URL(value)
+  } catch (cause) {
+    throw new PicGoCloudError('Invalid PicGo Cloud API base URL', {
+      kind: 'validation',
+      cause,
+    })
+  }
+
+  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
+    throw new PicGoCloudError('PicGo Cloud API base URL must use HTTP or HTTPS', {
+      kind: 'validation',
+    })
+  }
+  if (url.username || url.password || url.search || url.hash) {
+    throw new PicGoCloudError('PicGo Cloud API base URL cannot contain credentials, a query, or a hash', {
+      kind: 'validation',
+    })
+  }
+
+  url.pathname = url.pathname.replace(/\/+$/, '')
+  return url.toString().replace(/\/$/, '')
+}
+
+function isRecord(value: unknown): value is Record {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function getApiMessage(payload: unknown, fallback: string): string {
+  if (isRecord(payload) && typeof payload.message === 'string' && payload.message.length > 0) {
+    return payload.message
+  }
+  return fallback
+}
+
+function getApiCode(payload: unknown): string | undefined {
+  if (isRecord(payload) && typeof payload.code === 'string') {
+    return payload.code
+  }
+  return undefined
+}
+
+function abortedError(signal: AbortSignal): PicGoCloudError {
+  if (isPicGoCloudError(signal.reason)) {
+    return signal.reason
+  }
+  return new PicGoCloudError('The request was aborted', {
+    kind: 'aborted',
+    cause: signal.reason,
+  })
+}
+
+async function awaitWithAbort(value: Promise, signal?: AbortSignal): Promise {
+  if (!signal) {
+    return value
+  }
+
+  return await new Promise((resolve, reject) => {
+    const handleAbort = (): void => {
+      reject(abortedError(signal))
+    }
+
+    value.then(
+      (result) => {
+        signal.removeEventListener('abort', handleAbort)
+        resolve(result)
+      },
+      (error: unknown) => {
+        signal.removeEventListener('abort', handleAbort)
+        reject(error)
+      },
+    )
+    if (signal.aborted) {
+      handleAbort()
+    } else {
+      signal.addEventListener('abort', handleAbort, { once: true })
+    }
+  })
+}
+
+export class HttpClient {
+  readonly baseUrl: string
+
+  private readonly tokenProvider: TokenProvider
+  private readonly fetchImpl: typeof fetch
+  private readonly timeoutMs: number
+  private readonly baseOrigin: string
+
+  constructor(config: HttpClientConfig) {
+    this.baseUrl = normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE_URL)
+    this.baseOrigin = new URL(this.baseUrl).origin
+    this.tokenProvider = config.token
+    this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
+
+    if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
+      throw new PicGoCloudError('timeoutMs must be a positive number', { kind: 'validation' })
+    }
+
+    const fetchImpl = config.fetch ?? globalThis.fetch
+    if (typeof fetchImpl !== 'function') {
+      throw new PicGoCloudError('This browser does not provide fetch', { kind: 'validation' })
+    }
+    this.fetchImpl = fetchImpl.bind(globalThis)
+  }
+
+  async resolveToken(signal?: AbortSignal): Promise {
+    const controller = new AbortController()
+    let timedOut = false
+    const handleAbort = (): void => {
+      controller.abort(signal?.reason)
+    }
+    if (signal?.aborted) {
+      handleAbort()
+    } else {
+      signal?.addEventListener('abort', handleAbort, { once: true })
+    }
+    const timeoutId = globalThis.setTimeout(() => {
+      timedOut = true
+      controller.abort()
+    }, this.timeoutMs)
+
+    try {
+      return await this.resolveTokenWithSignal(controller.signal)
+    } catch (cause) {
+      if (timedOut) {
+        throw new PicGoCloudError(`PicGo Cloud token resolution timed out after ${this.timeoutMs}ms`, {
+          kind: 'timeout',
+          cause,
+        })
+      }
+      if (controller.signal.aborted) {
+        throw abortedError(controller.signal)
+      }
+      throw cause
+    } finally {
+      globalThis.clearTimeout(timeoutId)
+      signal?.removeEventListener('abort', handleAbort)
+    }
+  }
+
+  private async resolveTokenWithSignal(signal: AbortSignal): Promise {
+    if (signal.aborted) {
+      throw abortedError(signal)
+    }
+
+    let tokenPromise: Promise
+    try {
+      tokenPromise = typeof this.tokenProvider === 'function'
+        ? Promise.resolve(this.tokenProvider())
+        : Promise.resolve(this.tokenProvider)
+    } catch (cause) {
+      throw new PicGoCloudError('Failed to resolve the PicGo Cloud token', {
+        kind: 'authentication',
+        cause,
+      })
+    }
+
+    let token: unknown
+    try {
+      token = await awaitWithAbort(tokenPromise, signal)
+    } catch (cause) {
+      if (isPicGoCloudError(cause)) {
+        throw cause
+      }
+      throw new PicGoCloudError('Failed to resolve the PicGo Cloud token', {
+        kind: 'authentication',
+        cause,
+      })
+    }
+
+    if (typeof token !== 'string' || token.trim().length === 0) {
+      throw new PicGoCloudError('PicGo Cloud token must be a non-empty string', {
+        kind: 'validation',
+      })
+    }
+    return token.trim()
+  }
+
+  withToken(token: string): HttpClient {
+    return new HttpClient({
+      token,
+      baseUrl: this.baseUrl,
+      fetch: this.fetchImpl,
+      timeoutMs: this.timeoutMs,
+    })
+  }
+
+  async request(path: string, options: HttpRequestOptions = {}): Promise {
+    if (!path.startsWith('/')) {
+      throw new PicGoCloudError('API request paths must start with /', { kind: 'validation' })
+    }
+
+    const url = new URL(`${this.baseUrl}${path}`)
+    if (url.origin !== this.baseOrigin) {
+      throw new PicGoCloudError('Refusing to send a PicGo Cloud token to another origin', {
+        kind: 'validation',
+      })
+    }
+
+    const controller = new AbortController()
+    let timedOut = false
+    const handleAbort = (): void => {
+      controller.abort(options.signal?.reason)
+    }
+    if (options.signal?.aborted) {
+      handleAbort()
+    } else {
+      options.signal?.addEventListener('abort', handleAbort, { once: true })
+    }
+    const timeoutId = globalThis.setTimeout(() => {
+      timedOut = true
+      controller.abort()
+    }, this.timeoutMs)
+
+    try {
+      const token = await this.resolveTokenWithSignal(controller.signal)
+      const headers: Record = {
+        Accept: 'application/json',
+        Authorization: `Bearer ${token}`,
+      }
+      let body: string | undefined
+      if (options.body !== undefined) {
+        headers['Content-Type'] = 'application/json'
+        try {
+          body = JSON.stringify(options.body)
+        } catch (cause) {
+          throw new PicGoCloudError('Request body is not JSON serializable', {
+            kind: 'validation',
+            cause,
+          })
+        }
+        if (body === undefined) {
+          throw new PicGoCloudError('Request body is not JSON serializable', {
+            kind: 'validation',
+          })
+        }
+      }
+
+      const response = await this.fetchImpl(url.toString(), {
+        method: options.method ?? 'GET',
+        headers,
+        body,
+        credentials: 'omit',
+        redirect: 'error',
+        signal: controller.signal,
+      })
+
+      if (response.status === 204) {
+        if (!response.ok) {
+          throw new PicGoCloudError(`PicGo Cloud API request failed with status ${response.status}`, {
+            kind: 'api',
+            status: response.status,
+          })
+        }
+        return undefined as T
+      }
+
+      const responseText = await response.text()
+      let payload: unknown
+      if (responseText.length === 0) {
+        if (!response.ok) {
+          throw new PicGoCloudError(
+            response.statusText || `PicGo Cloud API request failed with status ${response.status}`,
+            { kind: 'api', status: response.status },
+          )
+        }
+        throw new PicGoCloudError('PicGo Cloud API returned an empty response', {
+          kind: 'protocol',
+          status: response.status,
+        })
+      }
+      try {
+        payload = JSON.parse(responseText) as unknown
+      } catch (cause) {
+        throw new PicGoCloudError('PicGo Cloud API returned malformed JSON', {
+          kind: 'protocol',
+          status: response.status,
+          cause,
+        })
+      }
+
+      const envelope: ApiEnvelope | undefined = isRecord(payload) ? payload : undefined
+      if (envelope?.success === false || !response.ok) {
+        throw new PicGoCloudError(
+          getApiMessage(payload, response.statusText || `PicGo Cloud API request failed with status ${response.status}`),
+          {
+            kind: 'api',
+            status: response.status,
+            code: getApiCode(payload),
+          },
+        )
+      }
+
+      if (envelope?.success === true) {
+        if (Object.hasOwn(envelope, 'data')) {
+          return envelope.data as T
+        }
+        const result: Record = { ...envelope }
+        delete result.success
+        return result as T
+      }
+      return payload as T
+    } catch (cause) {
+      if (timedOut) {
+        throw new PicGoCloudError(`PicGo Cloud API request timed out after ${this.timeoutMs}ms`, {
+          kind: 'timeout',
+          cause,
+        })
+      }
+      if (controller.signal.aborted) {
+        throw abortedError(controller.signal)
+      }
+      if (isPicGoCloudError(cause)) {
+        throw cause
+      }
+      throw new PicGoCloudError('Could not reach the PicGo Cloud API', {
+        kind: 'network',
+        cause,
+      })
+    } finally {
+      globalThis.clearTimeout(timeoutId)
+      options.signal?.removeEventListener('abort', handleAbort)
+    }
+  }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..8f419f3
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,9 @@
+export { PicGoCloudClient } from './client.js'
+export { ApiErrorCode } from './api-error-codes.js'
+export type { PicGoCloudClientOptions } from './client.js'
+export { PicGoCloudError, isPicGoCloudError } from './errors.js'
+export type { PicGoCloudErrorKind, PicGoCloudErrorOptions } from './errors.js'
+export type * from './types.js'
+export { MediaService } from './media.js'
+export { UploadTask } from './upload/task.js'
+export type { UploadOptions, UploadConfig, UploadProgress, UploadPhase, UploadStatus, UploadStorage } from './upload/types.js'
diff --git a/src/media.ts b/src/media.ts
new file mode 100644
index 0000000..58cf584
--- /dev/null
+++ b/src/media.ts
@@ -0,0 +1,146 @@
+import { PicGoCloudError } from './errors.js'
+import type { HttpClient } from './http.js'
+import type {
+  MediaBulkDeleteResult,
+  MediaBulkUpdateItem,
+  MediaBulkUpdateResult,
+  MediaDeleteResult,
+  MediaFilters,
+  MediaItem,
+  MediaListQuery,
+  MediaListResult,
+  MediaStats,
+  MediaUpdate,
+  RequestOptions,
+} from './types.js'
+
+type MediaItemResult = { item: MediaItem }
+const RESERVED_MEDIA_IDS = new Set(['complete', 'filters', 'stats'])
+
+function validation(message: string): never {
+  throw new PicGoCloudError(message, { kind: 'validation' })
+}
+
+function validateMediaId(id: unknown): void {
+  if (typeof id !== 'string' || id.trim().length === 0) {
+    validation('Media id cannot be empty')
+  }
+  if (RESERVED_MEDIA_IDS.has(id.toLowerCase())) {
+    validation(`Media id cannot use the reserved path ${id}`)
+  }
+}
+
+function validateListQuery(query: MediaListQuery): void {
+  if (query.limit !== undefined
+    && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > 100)) {
+    validation('Media list limit must be an integer between 1 and 100')
+  }
+  if (query.offset !== undefined
+    && (!Number.isSafeInteger(query.offset) || query.offset < 0)) {
+    validation('Media list offset must be a non-negative integer')
+  }
+}
+
+function validateBatch(values: T[], name: string): void {
+  if (!Array.isArray(values) || values.length < 1 || values.length > 100) {
+    validation(`${name} must contain between 1 and 100 items`)
+  }
+}
+
+function addQueryValue(params: URLSearchParams, key: string, value: string | number | undefined): void {
+  if (value !== undefined) {
+    params.set(key, String(value))
+  }
+}
+
+export class MediaService {
+  constructor(private readonly http: HttpClient) {}
+
+  async list(query: MediaListQuery = {}, options: RequestOptions = {}): Promise {
+    validateListQuery(query)
+    const params = new URLSearchParams()
+    addQueryValue(params, 'limit', query.limit)
+    addQueryValue(params, 'offset', query.offset)
+    addQueryValue(params, 'search', query.search)
+    addQueryValue(params, 'contentType', query.contentType)
+    addQueryValue(params, 'ext', query.ext)
+    addQueryValue(params, 'fileName', query.fileName)
+    addQueryValue(params, 'type', query.type)
+    addQueryValue(params, 'sort', query.sort)
+    addQueryValue(params, 'order', query.order)
+
+    const queryString = params.toString()
+    return await this.http.request(
+      `/api/album-items${queryString ? `?${queryString}` : ''}`,
+      { signal: options.signal },
+    )
+  }
+
+  async get(id: string, options: RequestOptions = {}): Promise {
+    validateMediaId(id)
+    const result = await this.http.request(
+      `/api/album-items/${encodeURIComponent(id)}`,
+      { signal: options.signal },
+    )
+    return result.item
+  }
+
+  async update(id: string, changes: MediaUpdate, options: RequestOptions = {}): Promise {
+    validateMediaId(id)
+    const result = await this.http.request(
+      `/api/album-items/${encodeURIComponent(id)}`,
+      { method: 'PATCH', body: changes, signal: options.signal },
+    )
+    return result.item
+  }
+
+  async updateMany(
+    items: MediaBulkUpdateItem[],
+    options: RequestOptions = {},
+  ): Promise {
+    validateBatch(items, 'Media updates')
+    for (const item of items) {
+      if (typeof item !== 'object' || item === null || !('id' in item)) {
+        validation('Each media update must include an id')
+      }
+      validateMediaId(item.id)
+    }
+    return await this.http.request('/api/album-items', {
+      method: 'PATCH',
+      body: { items },
+      signal: options.signal,
+    })
+  }
+
+  async delete(id: string, options: RequestOptions = {}): Promise {
+    validateMediaId(id)
+    return await this.http.request(
+      `/api/album-items/${encodeURIComponent(id)}`,
+      { method: 'DELETE', signal: options.signal },
+    )
+  }
+
+  async deleteMany(ids: string[], options: RequestOptions = {}): Promise {
+    validateBatch(ids, 'Media ids')
+    for (const id of ids) {
+      validateMediaId(id)
+    }
+    return await this.http.request('/api/album-items', {
+      method: 'DELETE',
+      body: { ids },
+      signal: options.signal,
+    })
+  }
+
+  async filters(options: RequestOptions = {}): Promise {
+    return await this.http.request('/api/album-items/filters', {
+      signal: options.signal,
+    })
+  }
+
+  async stats(options: RequestOptions = {}): Promise {
+    return await this.http.request('/api/album-items/stats', {
+      signal: options.signal,
+    })
+  }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..46cc47f
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,99 @@
+export type TokenProvider = string | (() => string | Promise)
+
+export interface RequestOptions {
+  signal?: AbortSignal
+}
+
+export interface WhoAmI {
+  /** Available on current Workers; optional for compatibility with older deployments. */
+  userId?: string
+  user: string | null
+  avatar: string | null
+  plan: number
+  autoImport: boolean
+}
+
+export interface MediaItem {
+  id: string
+  imgUrl: string
+  fileName?: string
+  type?: string
+  contentType?: string
+  size?: number
+  width?: number
+  height?: number
+  extname?: string
+  createdAt?: number
+  updatedAt?: number
+  originImgUrl?: string
+  url?: string
+  extra?: Record
+}
+
+export type MediaSort = 'newest' | 'oldest' | 'fileName'
+export type SortOrder = 'asc' | 'desc'
+
+export interface MediaListQuery {
+  limit?: number
+  offset?: number
+  search?: string
+  contentType?: string
+  ext?: string
+  fileName?: string
+  type?: string
+  sort?: MediaSort
+  order?: SortOrder
+}
+
+export interface MediaListResult {
+  items: MediaItem[]
+  limit: number
+  offset: number
+  total: number
+}
+
+export interface MediaUpdate {
+  imgUrl?: string
+  fileName?: string
+  type?: string
+  contentType?: string
+  width?: number
+  height?: number
+  createdAt?: number
+  updatedAt?: number
+  originImgUrl?: string
+  url?: string
+  extra?: Record
+}
+
+export type MediaBulkUpdateItem = MediaUpdate & { id: string }
+
+export interface MediaBulkUpdateResult {
+  updated: number
+  skipped: number
+  items: MediaItem[]
+}
+
+export interface MediaDeleteResult {
+  message: string
+}
+
+export interface MediaBulkDeleteResult {
+  deleted: number
+}
+
+export interface MediaFilters {
+  contentTypes: string[]
+  types: string[]
+  exts: string[]
+}
+
+export interface MediaTypeStats {
+  type: string
+  count: number
+}
+
+export interface MediaStats {
+  total: number
+  types: MediaTypeStats[]
+}
diff --git a/src/upload/storage.ts b/src/upload/storage.ts
new file mode 100644
index 0000000..8a2dedf
--- /dev/null
+++ b/src/upload/storage.ts
@@ -0,0 +1,105 @@
+import { PicGoCloudError } from '../errors.js'
+import type { StoredUpload, UploadStorage } from './types.js'
+
+const TTL = 24 * 60 * 60 * 1000
+
+export function throwIfAborted(signal: AbortSignal): void {
+  if (signal.aborted) {
+    throw signal.reason instanceof PicGoCloudError
+      ? signal.reason
+      : new PicGoCloudError('Upload interrupted', { kind: 'aborted', cause: signal.reason })
+  }
+}
+
+export async function digest(value: string): Promise {
+  return hex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)))
+}
+
+function hex(buffer: ArrayBuffer): string {
+  return Array.from(new Uint8Array(buffer), byte => byte.toString(16).padStart(2, '0')).join('')
+}
+
+/** Hash every byte using bounded buffers, avoiding a whole-file allocation. */
+export async function fingerprint(file: Blob, filename: string, contentType: string, signal: AbortSignal): Promise {
+  const chunks: string[] = []
+  const chunkSize = 8 * 1024 * 1024
+  for (let start = 0; start < file.size; start += chunkSize) {
+    throwIfAborted(signal)
+    const chunk = await file.slice(start, start + chunkSize).arrayBuffer()
+    chunks.push(hex(await crypto.subtle.digest('SHA-256', chunk)))
+  }
+  throwIfAborted(signal)
+  return digest(JSON.stringify([filename, contentType, file.size, chunks]))
+}
+
+function isRecord(value: unknown): value is Record {
+  return typeof value === 'object' && value !== null
+}
+
+function valid(value: unknown, fileSize: number): value is StoredUpload {
+  if (!isRecord(value) || value.version !== 1 || typeof value.createdAt !== 'number'
+    || value.createdAt > Date.now() || Date.now() - value.createdAt > TTL
+    || typeof value.objectKey !== 'string' || !value.objectKey
+    || typeof value.publicId !== 'string' || !value.publicId
+    || typeof value.uploadId !== 'string' || !value.uploadId
+    || typeof value.partSize !== 'number' || !Number.isSafeInteger(value.partSize) || value.partSize <= 0
+    || typeof value.partCount !== 'number' || value.partCount < 1 || value.partCount > 10_000
+    || value.partCount !== Math.ceil(fileSize / value.partSize)
+    || !['uploading', 'merging', 'uploaded'].includes(String(value.phase))
+    || !Array.isArray(value.completedParts)) return false
+  const seen = new Set()
+  for (const part of value.completedParts) {
+    if (!isRecord(part) || typeof part.partNumber !== 'number' || !Number.isInteger(part.partNumber)
+      || part.partNumber < 1 || part.partNumber > value.partCount || seen.has(part.partNumber)
+      || typeof part.etag !== 'string' || !part.etag || part.etag.length > 256) return false
+    seen.add(part.partNumber)
+  }
+  return value.phase === 'uploading' || seen.size === value.partCount
+}
+
+export class ResumeStore {
+  constructor(private readonly configured?: UploadStorage | false) {}
+
+  private storage(): UploadStorage | undefined {
+    try { return this.configured === false ? undefined : this.configured ?? globalThis.localStorage }
+    catch { return undefined }
+  }
+
+  get(key: string, fileSize: number): StoredUpload | undefined {
+    try {
+      const raw = this.storage()?.getItem(key)
+      if (!raw) return undefined
+      const value: unknown = JSON.parse(raw)
+      if (valid(value, fileSize)) return value
+      this.remove(key)
+    } catch { this.remove(key) }
+    return undefined
+  }
+
+  set(key: string, state: StoredUpload): void {
+    try { this.storage()?.setItem(key, JSON.stringify(state)) } catch { /* Keep the in-memory session. */ }
+  }
+
+  remove(key: string): void {
+    try { this.storage()?.removeItem(key) } catch { /* Storage may be disabled by the browser. */ }
+  }
+}
+
+const activeUploads = new Set()
+
+/** Cross-tab exclusion when Web Locks is available; same-page fallback otherwise. */
+export async function withUploadLock(key: string, signal: AbortSignal, run: () => Promise): Promise {
+  const execute = async (): Promise => {
+    throwIfAborted(signal)
+    if (activeUploads.has(key)) throw new PicGoCloudError('This file is already uploading', { kind: 'validation' })
+    activeUploads.add(key)
+    try { return await run() } finally { activeUploads.delete(key) }
+  }
+  if (typeof navigator !== 'undefined' && navigator.locks) {
+    return navigator.locks.request(key, { ifAvailable: true }, async lock => {
+      if (!lock) throw new PicGoCloudError('This file is uploading in another tab', { kind: 'validation' })
+      return execute()
+    })
+  }
+  return execute()
+}
diff --git a/src/upload/task.ts b/src/upload/task.ts
new file mode 100644
index 0000000..c185185
--- /dev/null
+++ b/src/upload/task.ts
@@ -0,0 +1,385 @@
+import { PicGoCloudError } from '../errors.js'
+import { ApiErrorCode } from '../api-error-codes.js'
+import type { HttpClient } from '../http.js'
+import type { MediaItem } from '../types.js'
+import { digest, fingerprint, ResumeStore, throwIfAborted, withUploadLock } from './storage.js'
+import { putBlob } from './transport.js'
+import type { MultipartSession, PresignResult, SignedPart, StoredUpload, UploadConfig, UploadOptions, UploadPhase, UploadStatus } from './types.js'
+
+const MULTIPART_THRESHOLD = 10 * 1024 * 1024
+const MAX_SIZE = 1024 * 1024 * 1024
+const BACKOFF = [1000, 2000, 4000]
+
+function isRetryable(error: unknown): boolean {
+  return error instanceof PicGoCloudError && (error.kind === 'network' || error.kind === 'timeout'
+    || error.status === 408 || error.status === 429 || (error.status !== undefined && error.status >= 500))
+}
+
+function delay(ms: number, signal: AbortSignal): Promise {
+  throwIfAborted(signal)
+  return new Promise((resolve, reject) => {
+    const abort = () => { clearTimeout(timer); reject(signal.reason) }
+    const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve() }, ms)
+    signal.addEventListener('abort', abort, { once: true })
+  })
+}
+
+async function retry(run: () => Promise, signal: AbortSignal): Promise {
+  for (let attempt = 0; ; attempt++) {
+    throwIfAborted(signal)
+    try { return await run() } catch (error) {
+      const wait = BACKOFF[attempt]
+      if (!isRetryable(error) || wait === undefined) throw error
+      await delay(wait, signal)
+    }
+  }
+}
+
+function protocol(message: string): never {
+  throw new PicGoCloudError(message, { kind: 'protocol' })
+}
+
+function validateSession(data: MultipartSession, size: number): void {
+  if (!data || typeof data.uploadId !== 'string' || !data.uploadId
+    || typeof data.objectKey !== 'string' || !data.objectKey || typeof data.publicId !== 'string' || !data.publicId
+    || !Number.isSafeInteger(data.partSize) || data.partSize <= 0
+    || data.partCount < 1 || data.partCount > 10_000
+    || data.partCount !== Math.ceil(size / data.partSize) || !Array.isArray(data.parts)) {
+    protocol('Invalid multipart session response')
+  }
+}
+
+/** A lazy upload. start() resolves only after the media item has been registered. */
+export class UploadTask {
+  private currentStatus: UploadStatus = 'idle'
+  private controller?: AbortController
+  private running?: Promise
+  private result?: MediaItem
+  private state?: StoredUpload
+  private sessionHttp?: HttpClient
+  private uploadToken?: string
+  private owner?: string
+  private key?: string
+  private resumed = false
+  private cancelling = false
+  private readonly store: ResumeStore
+  private readonly filename: string
+  private readonly contentType: string
+  private readonly concurrency: number
+  private readonly timeoutMs: number
+
+  constructor(
+    private readonly http: HttpClient,
+    private readonly file: Blob,
+    private readonly options: UploadOptions = {},
+    config: UploadConfig = {},
+  ) {
+    this.filename = options.filename ?? (typeof File !== 'undefined' && file instanceof File ? file.name : '')
+    this.contentType = options.contentType ?? file.type
+    this.concurrency = options.concurrency ?? 3
+    this.timeoutMs = config.uploadTimeoutMs ?? 120_000
+    this.store = new ResumeStore(config.storage)
+    if (!this.filename.trim() || this.filename.length > 255) {
+      throw new PicGoCloudError('Provide a filename of 1–255 characters', { kind: 'validation' })
+    }
+    if (!Number.isSafeInteger(file.size) || file.size < 1 || file.size > MAX_SIZE) {
+      throw new PicGoCloudError('File size must be between 1 byte and 1 GiB', { kind: 'validation' })
+    }
+    if (!Number.isInteger(this.concurrency) || this.concurrency < 1 || this.concurrency > 6
+      || !Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
+      throw new PicGoCloudError('Invalid upload concurrency or timeout', { kind: 'validation' })
+    }
+    for (const dimension of [options.width, options.height]) {
+      if (dimension !== undefined && (!Number.isSafeInteger(dimension) || dimension <= 0)) {
+        throw new PicGoCloudError('Image dimensions must be positive integers', { kind: 'validation' })
+      }
+    }
+  }
+
+  get status(): UploadStatus { return this.currentStatus }
+
+  start(): Promise {
+    if (this.cancelling || this.currentStatus === 'cancelled') {
+      return Promise.reject(new PicGoCloudError('Upload was cancelled; create a new task', { kind: 'aborted' }))
+    }
+    if (this.result) return Promise.resolve(this.result)
+    if (this.running) return this.running
+    this.controller = new AbortController()
+    const signal = this.options.signal
+      ? AbortSignal.any([this.controller.signal, this.options.signal]) : this.controller.signal
+    this.currentStatus = 'running'
+    this.running = this.execute(signal).then(
+      result => {
+        this.result = result
+        this.currentStatus = 'completed'
+        this.running = undefined
+        return result
+      },
+      error => {
+        this.currentStatus = this.cancelling ? 'cancelled'
+          : signal.aborted && signal.reason instanceof PicGoCloudError && signal.reason.kind === 'paused' ? 'paused' : 'failed'
+        this.running = undefined
+        if (signal.aborted) throwIfAborted(signal)
+        throw error
+      },
+    )
+    return this.running
+  }
+
+  /** Stops local requests. Await/reject the pending start() before starting again. */
+  pause(): void {
+    this.controller?.abort(new PicGoCloudError('Upload paused', { kind: 'paused' }))
+  }
+
+  /** Cleans up an unfinished multipart session; does not delete committed media. */
+  async cancel(): Promise {
+    if (this.result) return
+    this.cancelling = true
+    this.controller?.abort(new PicGoCloudError('Upload cancelled', { kind: 'aborted' }))
+    try { await this.running } catch { /* The caller also receives the interrupted start(). */ }
+    if (this.result) { this.cancelling = false; return }
+    this.currentStatus = 'cancelled'
+    const cleanup = async (): Promise => {
+      if (this.state?.uploadId && this.state.phase !== 'uploaded' && this.sessionHttp) {
+        try {
+          await this.sessionHttp.request('/api/upload/multipart/abort', {
+            method: 'POST', body: { uploadId: this.state.uploadId, objectKey: this.state.objectKey },
+          })
+        } catch (error) {
+          if (!(error instanceof PicGoCloudError) || error.status !== 404) throw error
+        }
+      }
+      this.clear()
+    }
+    // A paused session may have been resumed by another task/tab in the meantime.
+    if (this.key && this.state) await withUploadLock(this.key, new AbortController().signal, cleanup)
+    else await cleanup()
+  }
+
+  private emit(phase: UploadPhase, loaded: number): void {
+    const bounded = Math.min(this.file.size, Math.max(0, loaded))
+    try {
+      this.options.onProgress?.({ phase, loaded: bounded, total: this.file.size, fraction: bounded / this.file.size, resumed: this.resumed })
+    } catch { /* Observers cannot change the outcome of an upload. */ }
+  }
+
+  private save(): void {
+    if (this.key && this.state && this.options.resume !== false) this.store.set(this.key, this.state)
+  }
+
+  private clear(): void {
+    if (this.key && this.state && this.options.resume !== false) this.store.remove(this.key)
+    this.state = undefined
+  }
+
+  private async execute(signal: AbortSignal): Promise {
+    throwIfAborted(signal)
+    this.emit('preparing', 0)
+    const token = await this.http.resolveToken(signal)
+    const sessionHttp = this.http.withToken(token)
+    // Pin the token throughout one attempt; never mix accounts across parts.
+    const identity = await sessionHttp.request<{ userId?: string }>('/api/whoami', { signal })
+    const owner = identity.userId || `token:${await digest(token)}`
+    if (this.owner && this.owner !== owner) {
+      throw new PicGoCloudError('The account changed; create a new upload task', { kind: 'validation' })
+    }
+    this.owner = owner
+    this.sessionHttp = sessionHttp
+    this.uploadToken = token
+    if (this.file.size < MULTIPART_THRESHOLD) return this.single(sessionHttp, signal)
+
+    const hash = await fingerprint(this.file, this.filename, this.contentType, signal)
+    const scope = await digest(JSON.stringify([this.http.baseUrl, owner, hash]))
+    this.key = `picgo-cloud-sdk:upload:v1:${scope}`
+    return withUploadLock(this.key, signal, async () => {
+      this.state ??= this.options.resume === false ? undefined : this.store.get(this.key!, this.file.size)
+      this.resumed = Boolean(this.state)
+      try { return await this.multipart(this.sessionHttp!, signal) } catch (error) {
+        // A vanished multipart session cannot be resumed. The next start creates a fresh one.
+        if (error instanceof PicGoCloudError && error.status === 404 && this.state?.phase === 'uploading') this.clear()
+        throw error
+      }
+    })
+  }
+
+  private uploadTarget(url: string, headers: Record): { url: string; headers: Record } {
+    const target = new URL(url, this.http.baseUrl)
+    const api = new URL(this.http.baseUrl)
+    const isLocalProxy = ['localhost', '127.0.0.1', '[::1]'].includes(api.hostname)
+      && target.origin === api.origin
+      && (/^\/api\/upload\/put\/.+/.test(target.pathname)
+        || /^\/api\/upload\/multipart-local\/[^/]+\/part\/\d+$/.test(target.pathname))
+    return { url: target.href, headers: isLocalProxy ? { ...headers, Authorization: `Bearer ${this.uploadToken}` } : headers }
+  }
+
+  private async finalize(http: HttpClient, signal: AbortSignal): Promise {
+    const state = this.state!
+    this.emit('completing', this.file.size)
+    let result: { item: MediaItem }
+    try {
+      result = await retry(() => http.request<{ item: MediaItem }>('/api/album-items/complete', {
+        method: 'POST', signal,
+        body: { objectKey: state.objectKey, publicId: state.publicId, filename: this.filename, width: this.options.width, height: this.options.height },
+      }), signal)
+    } catch (error) {
+      if (state.phase === 'uploaded' && error instanceof PicGoCloudError && error.status === 404) this.clear()
+      throw error
+    }
+    if (!result?.item || typeof result.item.id !== 'string' || typeof result.item.imgUrl !== 'string') {
+      protocol('Invalid media completion response')
+    }
+    this.clear()
+    this.emit('completed', this.file.size)
+    return result.item
+  }
+
+  private async single(http: HttpClient, signal: AbortSignal): Promise {
+    if (this.state?.phase === 'uploaded') {
+      this.resumed = true
+      return this.finalize(http, signal)
+    }
+    let presign = await this.presign(http, signal)
+    for (let attempt = 0; ; attempt++) {
+      this.state = { version: 1, createdAt: Date.now(), objectKey: presign.objectKey, publicId: presign.publicId,
+        partSize: this.file.size, partCount: 1, completedParts: [], phase: 'uploading' }
+      const headers = { ...presign.headers }
+      if (this.contentType && !Object.keys(headers).some(key => key.toLowerCase() === 'content-type')) headers['Content-Type'] = this.contentType
+      try {
+        await putBlob({ ...this.uploadTarget(presign.uploadUrl, headers), body: this.file, signal, timeoutMs: this.timeoutMs,
+          onProgress: loaded => this.emit('uploading', loaded) })
+        break
+      } catch (error) {
+        const wait = BACKOFF[attempt]
+        if (wait === undefined) throw error
+        if (error instanceof PicGoCloudError && error.status === 403) presign = await this.presign(http, signal)
+        else if (!isRetryable(error)) throw error
+        await delay(wait, signal)
+      }
+    }
+    this.state!.phase = 'uploaded'
+    return this.finalize(http, signal)
+  }
+
+  private async presign(http: HttpClient, signal: AbortSignal): Promise {
+    const data = await http.request('/api/upload/presign', {
+      method: 'POST', signal, body: { filename: this.filename, contentType: this.contentType || undefined, sizeBytes: this.file.size },
+    })
+    if (!data || typeof data.objectKey !== 'string' || typeof data.publicId !== 'string'
+      || typeof data.uploadUrl !== 'string' || data.method !== 'PUT' || typeof data.headers !== 'object' || !data.headers) {
+      protocol('Invalid upload presign response')
+    }
+    return data
+  }
+
+  private async multipart(http: HttpClient, signal: AbortSignal): Promise {
+    let signedParts: SignedPart[] = []
+    if (!this.state) {
+      const data = await http.request('/api/upload/multipart/initiate', {
+        method: 'POST', signal, body: { filename: this.filename, contentType: this.contentType || undefined, sizeBytes: this.file.size },
+      })
+      validateSession(data, this.file.size)
+      this.state = { version: 1, createdAt: Date.now(), uploadId: data.uploadId, objectKey: data.objectKey,
+        publicId: data.publicId, partSize: data.partSize, partCount: data.partCount, completedParts: [], phase: 'uploading' }
+      signedParts = data.parts
+      this.save()
+    }
+    const state = this.state
+    if (state.phase === 'uploaded') return this.finalize(http, signal)
+    // A lost merge response may mean R2 already committed the object. Probe the
+    // idempotent registration endpoint before attempting the merge again.
+    if (state.phase === 'merging') {
+      try { return await this.finalize(http, signal) } catch (error) {
+        if (!(error instanceof PicGoCloudError) || error.status !== 404) throw error
+      }
+    }
+    if (state.phase === 'uploading') await this.parts(http, signedParts, signal)
+    state.phase = 'merging'
+    this.save()
+    try {
+      await http.request('/api/upload/multipart/complete', {
+        method: 'POST', signal, body: { uploadId: state.uploadId, objectKey: state.objectKey,
+          parts: [...state.completedParts].sort((a, b) => a.partNumber - b.partNumber) },
+      })
+    } catch (error) {
+      if (error instanceof PicGoCloudError && error.code === ApiErrorCode.MultipartPartMismatch) {
+        this.clear()
+        try {
+          await http.request('/api/upload/multipart/abort', {
+            method: 'POST', signal, body: { uploadId: state.uploadId, objectKey: state.objectKey },
+          })
+        } catch { /* Preserve the merge error; the server also expires orphan sessions. */ }
+        throw error
+      }
+      if (!isRetryable(error) && !(error instanceof PicGoCloudError && error.status === 404)) throw error
+      // Don't blindly retry a potentially committed merge.
+      try { return await this.finalize(http, signal) } catch (completionError) {
+        if (completionError instanceof PicGoCloudError && completionError.status === 404
+          && error instanceof PicGoCloudError && error.status === 404) this.clear()
+        throw error
+      }
+    }
+    state.phase = 'uploaded'
+    this.save()
+    return this.finalize(http, signal)
+  }
+
+  private async parts(http: HttpClient, initial: SignedPart[], signal: AbortSignal): Promise {
+    const state = this.state!
+    const completed = new Set(state.completedParts.map(part => part.partNumber))
+    const remaining = Array.from({ length: state.partCount }, (_, index) => index + 1).filter(number => !completed.has(number))
+    const signed = new Map(initial.map(part => [part.partNumber, part]))
+    const progress = new Map()
+    const partBytes = (number: number) => Math.min(state.partSize, this.file.size - (number - 1) * state.partSize)
+    const emit = () => this.emit('uploading', state.completedParts.reduce((sum, part) => sum + partBytes(part.partNumber), 0)
+      + [...progress.values()].reduce((sum, value) => sum + value, 0))
+    emit()
+    const stop = new AbortController()
+    const groupSignal = AbortSignal.any([signal, stop.signal])
+    let cursor = 0
+    const refresh = async (number: number): Promise => {
+      const response = await retry(() => http.request<{ parts: SignedPart[] }>('/api/upload/multipart/part-urls', {
+        method: 'POST', signal: groupSignal, body: { uploadId: state.uploadId, objectKey: state.objectKey, partNumbers: [number] },
+      }), groupSignal)
+      const part = response?.parts?.find(part => part.partNumber === number)
+      if (!part) protocol('Missing signed multipart URL')
+      signed.set(number, part)
+      return part
+    }
+    const worker = async (): Promise => {
+      try {
+        while (cursor < remaining.length) {
+          throwIfAborted(groupSignal)
+          const number = remaining[cursor++]!
+          const start = (number - 1) * state.partSize
+          const body = this.file.slice(start, start + state.partSize)
+          let part = signed.get(number) ?? await refresh(number)
+          for (let attempt = 0; ; attempt++) {
+            throwIfAborted(groupSignal)
+            if (part.method !== 'PUT' || typeof part.url !== 'string' || !part.headers) protocol('Invalid signed multipart URL')
+            try {
+              const result = await putBlob({ ...this.uploadTarget(part.url, part.headers), body, signal: groupSignal, timeoutMs: this.timeoutMs,
+                onProgress: loaded => { progress.set(number, Math.min(body.size, loaded)); emit() } })
+              if (!result.etag || result.etag.length > 256) protocol('Upload response has no valid ETag; expose ETag in the R2 CORS configuration')
+              progress.delete(number)
+              state.completedParts.push({ partNumber: number, etag: result.etag })
+              this.save()
+              emit()
+              break
+            } catch (error) {
+              progress.delete(number)
+              const wait = BACKOFF[attempt]
+              if (wait === undefined) throw error
+              if (error instanceof PicGoCloudError && error.status === 403) part = await refresh(number)
+              else if (!isRetryable(error)) throw error
+              await delay(wait, groupSignal)
+            }
+          }
+        }
+      } catch (error) { stop.abort(error); throw error }
+    }
+    const results = await Promise.allSettled(Array.from({ length: Math.min(this.concurrency, remaining.length) }, worker))
+    const failure = results.find(result => result.status === 'rejected')
+    if (failure?.status === 'rejected') throw stop.signal.reason ?? failure.reason
+    throwIfAborted(signal)
+  }
+}
diff --git a/src/upload/transport.ts b/src/upload/transport.ts
new file mode 100644
index 0000000..ea8daf7
--- /dev/null
+++ b/src/upload/transport.ts
@@ -0,0 +1,179 @@
+import { PicGoCloudError } from '../errors.js'
+
+export interface PutOptions {
+  url: string
+  headers: Record
+  body: Blob
+  signal: AbortSignal
+  timeoutMs: number
+  onProgress?: (loaded: number) => void
+}
+
+export interface PutResult {
+  etag?: string
+}
+
+function validateUrl(url: string): void {
+  let parsedUrl: URL
+
+  try {
+    parsedUrl = new URL(url)
+  } catch (cause) {
+    throw new PicGoCloudError('The upload URL is invalid', {
+      kind: 'validation',
+      cause,
+    })
+  }
+
+  if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
+    throw new PicGoCloudError('The upload URL must use HTTP or HTTPS', {
+      kind: 'validation',
+    })
+  }
+}
+
+function validateTimeout(timeoutMs: number): void {
+  if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
+    throw new PicGoCloudError('The upload timeout must be a non-negative number', {
+      kind: 'validation',
+    })
+  }
+}
+
+export async function putBlob(options: PutOptions): Promise {
+  validateUrl(options.url)
+  validateTimeout(options.timeoutMs)
+
+  if (options.signal.aborted) {
+    throw new PicGoCloudError('The upload was aborted', {
+      kind: 'aborted',
+      cause: options.signal.reason,
+    })
+  }
+
+  return new Promise((resolve, reject) => {
+    const request = new XMLHttpRequest()
+    let settled = false
+
+    const cleanup = (): void => {
+      request.removeEventListener('load', handleLoad)
+      request.removeEventListener('error', handleNetworkError)
+      request.removeEventListener('abort', handleAbort)
+      request.removeEventListener('timeout', handleTimeout)
+      request.upload.removeEventListener('progress', handleProgress)
+      options.signal.removeEventListener('abort', handleSignalAbort)
+    }
+
+    const resolveOnce = (result: PutResult): void => {
+      if (settled) {
+        return
+      }
+
+      settled = true
+      cleanup()
+      resolve(result)
+    }
+
+    const rejectOnce = (error: unknown): void => {
+      if (settled) {
+        return
+      }
+
+      settled = true
+      cleanup()
+      reject(error)
+    }
+
+    function handleLoad(): void {
+      if (request.status >= 200 && request.status < 300) {
+        const etag = request.getResponseHeader('ETag')
+        resolveOnce(etag ? { etag } : {})
+        return
+      }
+
+      rejectOnce(
+        new PicGoCloudError(`The upload failed with status ${request.status}`, {
+          kind: 'api',
+          status: request.status,
+        }),
+      )
+    }
+
+    function handleNetworkError(event: Event): void {
+      rejectOnce(
+        new PicGoCloudError('A network error occurred while uploading', {
+          kind: 'network',
+          cause: event,
+        }),
+      )
+    }
+
+    function handleAbort(event: Event): void {
+      rejectOnce(
+        new PicGoCloudError('The upload was aborted', {
+          kind: 'aborted',
+          cause: options.signal.reason ?? event,
+        }),
+      )
+    }
+
+    function handleTimeout(event: Event): void {
+      rejectOnce(
+        new PicGoCloudError('The upload timed out', {
+          kind: 'timeout',
+          cause: event,
+        }),
+      )
+    }
+
+    function handleProgress(event: ProgressEvent): void {
+      if (!options.onProgress) {
+        return
+      }
+
+      try {
+        options.onProgress(event.loaded)
+      } catch (cause) {
+        rejectOnce(cause)
+        request.abort()
+      }
+    }
+
+    function handleSignalAbort(): void {
+      rejectOnce(
+        new PicGoCloudError('The upload was aborted', {
+          kind: 'aborted',
+          cause: options.signal.reason,
+        }),
+      )
+      request.abort()
+    }
+
+    request.addEventListener('load', handleLoad)
+    request.addEventListener('error', handleNetworkError)
+    request.addEventListener('abort', handleAbort)
+    request.addEventListener('timeout', handleTimeout)
+    request.upload.addEventListener('progress', handleProgress)
+    options.signal.addEventListener('abort', handleSignalAbort, { once: true })
+
+    try {
+      request.open('PUT', options.url, true)
+      request.withCredentials = false
+      request.timeout = options.timeoutMs
+
+      for (const [name, value] of Object.entries(options.headers)) {
+        request.setRequestHeader(name, value)
+      }
+
+      request.send(options.body)
+    } catch (cause) {
+      rejectOnce(
+        new PicGoCloudError('The upload request could not be started', {
+          kind: 'api',
+          cause,
+        }),
+      )
+      request.abort()
+    }
+  })
+}
diff --git a/src/upload/types.ts b/src/upload/types.ts
new file mode 100644
index 0000000..c4e7791
--- /dev/null
+++ b/src/upload/types.ts
@@ -0,0 +1,74 @@
+export type UploadPhase = 'preparing' | 'uploading' | 'completing' | 'completed'
+export type UploadStatus = 'idle' | 'running' | 'paused' | 'cancelled' | 'failed' | 'completed'
+
+export interface UploadProgress {
+  phase: UploadPhase
+  loaded: number
+  total: number
+  /** Transfer fraction; await the task result to confirm media registration. */
+  fraction: number
+  resumed: boolean
+}
+
+/** Compatible with localStorage; failures degrade to in-memory resume. */
+export interface UploadStorage {
+  getItem(key: string): string | null
+  setItem(key: string, value: string): void
+  removeItem(key: string): void
+}
+
+export interface UploadOptions {
+  /** Required for a Blob without a File name. */
+  filename?: string
+  contentType?: string
+  width?: number
+  height?: number
+  signal?: AbortSignal
+  onProgress?: (progress: UploadProgress) => void
+  /** Default true. Persistent recovery applies to multipart uploads only. */
+  resume?: boolean
+  /** Default 3; maximum 6. */
+  concurrency?: number
+}
+
+export interface UploadConfig {
+  /** Default localStorage. false disables persistence, but not same-task resume. */
+  storage?: UploadStorage | false
+  /** Timeout for each signed PUT, default 120 seconds. */
+  uploadTimeoutMs?: number
+}
+
+export interface CompletedPart { partNumber: number; etag: string }
+export interface SignedPart {
+  partNumber: number
+  url: string
+  method: 'PUT'
+  headers: Record
+}
+export interface MultipartSession {
+  uploadId: string
+  objectKey: string
+  publicId: string
+  partSize: number
+  partCount: number
+  parts: SignedPart[]
+}
+export interface PresignResult {
+  objectKey: string
+  publicId: string
+  uploadUrl: string
+  method: 'PUT'
+  headers: Record
+}
+
+export interface StoredUpload {
+  version: 1
+  createdAt: number
+  objectKey: string
+  publicId: string
+  uploadId?: string
+  partSize: number
+  partCount: number
+  completedParts: CompletedPart[]
+  phase: 'uploading' | 'merging' | 'uploaded'
+}
diff --git a/tests/browser/smoke.html b/tests/browser/smoke.html
new file mode 100644
index 0000000..b0e7fdb
--- /dev/null
+++ b/tests/browser/smoke.html
@@ -0,0 +1,8 @@
+
+
+
+PicGo Cloud SDK browser smoke
+

Browser smoke

+
Running…
+ + diff --git a/tests/browser/smoke.js b/tests/browser/smoke.js new file mode 100644 index 0000000..1b58d83 --- /dev/null +++ b/tests/browser/smoke.js @@ -0,0 +1,47 @@ +import { PicGoCloudClient } from '/dist/index.js' + +function assert(condition, message) { if (!condition) throw new Error(message) } + +async function run() { + document.cookie = 'should-not-leak=1; SameSite=Lax; Path=/' + localStorage.clear() + const options = { token: 'browser-smoke-token', baseUrl: 'http://localhost:41781' } + const client = new PicGoCloudClient(options) + const progress = [] + const small = await client.upload(new File(['png'], 'small.png', { type: 'image/png' }), { + onProgress: event => progress.push(event), + }) + assert(small.id === '1', 'small upload result') + const largeFile = new File([new Uint8Array(10 * 1024 * 1024)], 'large.png', { type: 'image/png' }) + let failedRegistration = false + try { await client.upload(largeFile) } + catch (error) { failedRegistration = error.code === 'TEST_REGISTRATION_FAILURE' } + assert(failedRegistration, 'registration error must reach the caller') + assert(localStorage.length > 0, 'merged session must remain persisted') + const recreated = new PicGoCloudClient(options) + const large = await recreated.upload(largeFile) + assert(large.id === '2', 'recreated client should register the existing object') + assert(localStorage.length === 0, 'successful registration clears the session') + assert((await client.media.list()).total === 2, 'list result') + assert((await client.media.update(small.id, { fileName: 'renamed.png' })).fileName === 'renamed.png', 'update result') + await client.media.delete(small.id) + assert((await client.media.list()).total === 1, 'delete result') + const counts = await fetch(`${options.baseUrl}/counts`, { credentials: 'omit', headers: { Authorization: `Bearer ${options.token}` } }).then(response => response.json()) + assert(counts.puts === 3, 'one single PUT and two parts; recovery must not upload again') + assert(counts.merges === 1, 'one multipart merge') + assert(counts.preflights > 0, 'browser performed real CORS preflights') + assert(counts.credentialsLeaked === 0, 'cookies/token leaked into a storage request') + assert(progress.some(event => event.phase === 'uploading'), 'real XHR progress received') + assert(progress.at(-1).phase === 'completed', 'last progress follows registration') + return { passed: true, counts, progressEvents: progress.length, recoveredMediaId: large.id } +} + +const result = document.getElementById('result') +const execution = run() +execution.then(value => { + result.dataset.status = 'passed' + result.textContent = JSON.stringify(value, null, 2) +}, error => { + result.dataset.status = 'failed' + result.textContent = `${error.stack}` +}) diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 0000000..274f7ad --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,336 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { HttpClient } from '../src/http.js' +import { MediaService } from '../src/media.js' + +function jsonResponse(body: unknown, init: ResponseInit = {}): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + ...init, + }) +} + +function getFetchCall(fetchMock: ReturnType>, index: number) { + const call = fetchMock.mock.calls[index] + if (!call) { + throw new Error(`Missing fetch call ${index}`) + } + return call +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('HttpClient', () => { + it('normalizes the base URL, adds bearer auth, and unwraps data envelopes', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, data: { value: 42 } })) + const client = new HttpClient({ + token: async () => 'user-token', + baseUrl: 'https://cloud.example.test/', + fetch: fetchMock, + }) + + await expect(client.request<{ value: number }>('/api/example')).resolves.toEqual({ value: 42 }) + expect(client.baseUrl).toBe('https://cloud.example.test') + + const [url, init] = getFetchCall(fetchMock, 0) + expect(url).toBe('https://cloud.example.test/api/example') + expect(init).toMatchObject({ + method: 'GET', + credentials: 'omit', + redirect: 'error', + }) + expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer user-token') + }) + + it('keeps top-level API fields while removing a successful envelope flag', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, uploadUrl: 'https://upload.test' })) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await expect(client.request<{ uploadUrl: string }>('/api/presign')).resolves.toEqual({ + uploadUrl: 'https://upload.test', + }) + }) + + it('creates a fixed-token client for multi-request operations', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })) + const tokenProvider = vi.fn(async () => 'snapshot-token') + const client = new HttpClient({ token: tokenProvider, fetch: fetchMock }) + const token = await client.resolveToken() + + await client.withToken(token).request('/api/example') + + expect(tokenProvider).toHaveBeenCalledTimes(1) + const [, init] = getFetchCall(fetchMock, 0) + expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer snapshot-token') + }) + + it('serializes JSON request bodies', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, data: { updated: true } })) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await client.request('/api/example', { method: 'PATCH', body: { name: 'new name' } }) + + const [, init] = getFetchCall(fetchMock, 0) + expect(init?.body).toBe('{"name":"new name"}') + expect(new Headers(init?.headers).get('Content-Type')).toBe('application/json') + }) + + it('exposes backend status and error code without branching on the message', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse( + { success: false, code: 'UNAUTHORIZED', message: 'Token expired' }, + { status: 401 }, + )) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await expect(client.request('/api/example')).rejects.toMatchObject({ + name: 'PicGoCloudError', + kind: 'api', + status: 401, + code: 'UNAUTHORIZED', + message: 'Token expired', + }) + }) + + it('rejects success:false even when the HTTP status is successful', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(jsonResponse({ + success: false, + code: 'INVALID_REQUEST', + message: 'Invalid request', + })) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await expect(client.request('/api/example')).rejects.toMatchObject({ + kind: 'api', + status: 200, + code: 'INVALID_REQUEST', + }) + }) + + it('reports malformed JSON as a protocol error', async () => { + const fetchMock = vi.fn() + fetchMock.mockResolvedValueOnce(new Response('{broken', { status: 200 })) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await expect(client.request('/api/example')).rejects.toMatchObject({ + kind: 'protocol', + status: 200, + }) + }) + + it('reports fetch failures as network errors', async () => { + const fetchMock = vi.fn() + fetchMock.mockRejectedValueOnce(new TypeError('offline')) + const client = new HttpClient({ token: 'token', fetch: fetchMock }) + + await expect(client.request('/api/example')).rejects.toMatchObject({ kind: 'network' }) + }) + + it('times out while an async token is still resolving', async () => { + vi.useFakeTimers() + const fetchMock = vi.fn() + const unresolvedToken = new Promise(() => undefined) + const client = new HttpClient({ + token: () => unresolvedToken, + fetch: fetchMock, + timeoutMs: 50, + }) + + const request = client.request('/api/example') + const assertion = expect(request).rejects.toMatchObject({ kind: 'timeout' }) + await vi.advanceTimersByTimeAsync(50) + await assertion + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('applies the configured timeout to standalone token resolution', async () => { + vi.useFakeTimers() + const unresolvedToken = new Promise(() => undefined) + const client = new HttpClient({ + token: () => unresolvedToken, + fetch: vi.fn(), + timeoutMs: 50, + }) + + const resolution = client.resolveToken() + const assertion = expect(resolution).rejects.toMatchObject({ kind: 'timeout' }) + await vi.advanceTimersByTimeAsync(50) + await assertion + }) + + it('does not invoke the token provider for a pre-aborted signal', async () => { + const tokenProvider = vi.fn(async () => 'token') + const client = new HttpClient({ token: tokenProvider, fetch: vi.fn() }) + const controller = new AbortController() + controller.abort() + + await expect(client.resolveToken(controller.signal)).rejects.toMatchObject({ kind: 'aborted' }) + expect(tokenProvider).not.toHaveBeenCalled() + }) + + it('observes a rejecting provider promise when token resolution is aborted synchronously', async () => { + const controller = new AbortController() + const tokenProvider = vi.fn(() => { + controller.abort() + return Promise.reject(new Error('provider rejected')) + }) + const client = new HttpClient({ token: tokenProvider, fetch: vi.fn() }) + + await expect(client.resolveToken(controller.signal)).rejects.toMatchObject({ kind: 'aborted' }) + expect(tokenProvider).toHaveBeenCalledTimes(1) + }) + + it('validates the runtime token value before using string methods', async () => { + const invalidProvider = (() => 123) as unknown as () => string + const client = new HttpClient({ token: invalidProvider, fetch: vi.fn() }) + + await expect(client.resolveToken()).rejects.toMatchObject({ kind: 'validation' }) + }) + + it('can abort while an async token is still resolving', async () => { + const fetchMock = vi.fn() + const unresolvedToken = new Promise(() => undefined) + const client = new HttpClient({ token: () => unresolvedToken, fetch: fetchMock }) + const controller = new AbortController() + + const request = client.request('/api/example', { signal: controller.signal }) + controller.abort() + + await expect(request).rejects.toMatchObject({ kind: 'aborted' }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('rejects cross-origin paths before resolving or sending the token', async () => { + const fetchMock = vi.fn() + const tokenProvider = vi.fn(async () => 'token') + const client = new HttpClient({ token: tokenProvider, fetch: fetchMock }) + + await expect(client.request('https://attacker.test/steal')).rejects.toMatchObject({ + kind: 'validation', + }) + expect(tokenProvider).not.toHaveBeenCalled() + expect(fetchMock).not.toHaveBeenCalled() + }) +}) + +describe('MediaService', () => { + it('maps media operations to album-item endpoints and request bodies', async () => { + const mediaItem = { id: 'media-id', imgUrl: 'https://img.test/a.png' } + const fetchMock = vi.fn() + fetchMock + .mockResolvedValueOnce(jsonResponse({ + success: true, + data: { items: [mediaItem], limit: 10, offset: 0, total: 1 }, + })) + .mockResolvedValueOnce(jsonResponse({ success: true, data: { item: mediaItem } })) + .mockResolvedValueOnce(jsonResponse({ success: true, data: { item: mediaItem } })) + .mockResolvedValueOnce(jsonResponse({ + success: true, + data: { updated: 1, skipped: 0, items: [mediaItem] }, + })) + .mockResolvedValueOnce(jsonResponse({ success: true, data: { message: 'Deleted' } })) + .mockResolvedValueOnce(jsonResponse({ success: true, data: { deleted: 1 } })) + .mockResolvedValueOnce(jsonResponse({ + success: true, + data: { contentTypes: ['image/png'], types: ['picgo-cloud'], exts: ['.png'] }, + })) + .mockResolvedValueOnce(jsonResponse({ + success: true, + data: { total: 1, types: [{ type: 'picgo-cloud', count: 1 }] }, + })) + + const service = new MediaService(new HttpClient({ + token: 'token', + baseUrl: 'https://cloud.example.test', + fetch: fetchMock, + })) + + await expect(service.list({ + limit: 10, + offset: 0, + search: 'cat photo', + contentType: 'image/png', + ext: '.png', + fileName: 'cat', + type: 'picgo-cloud', + sort: 'fileName', + order: 'asc', + })).resolves.toMatchObject({ total: 1 }) + await expect(service.get('media/id')).resolves.toEqual(mediaItem) + await expect(service.update('media/id', { fileName: 'renamed.png' })).resolves.toEqual(mediaItem) + await expect(service.updateMany([{ id: 'media-id', fileName: 'renamed.png' }])).resolves.toMatchObject({ + updated: 1, + }) + await expect(service.delete('media/id')).resolves.toEqual({ message: 'Deleted' }) + await expect(service.deleteMany(['media-id'])).resolves.toEqual({ deleted: 1 }) + await expect(service.filters()).resolves.toMatchObject({ types: ['picgo-cloud'] }) + await expect(service.stats()).resolves.toMatchObject({ total: 1 }) + + expect(getFetchCall(fetchMock, 0)[0]).toBe( + 'https://cloud.example.test/api/album-items?limit=10&offset=0&search=cat+photo&contentType=image%2Fpng&ext=.png&fileName=cat&type=picgo-cloud&sort=fileName&order=asc', + ) + expect(getFetchCall(fetchMock, 1)[0]).toBe('https://cloud.example.test/api/album-items/media%2Fid') + expect(getFetchCall(fetchMock, 2)[1]).toMatchObject({ + method: 'PATCH', + body: '{"fileName":"renamed.png"}', + }) + expect(getFetchCall(fetchMock, 3)[1]).toMatchObject({ + method: 'PATCH', + body: '{"items":[{"id":"media-id","fileName":"renamed.png"}]}', + }) + expect(getFetchCall(fetchMock, 4)[1]).toMatchObject({ method: 'DELETE' }) + expect(getFetchCall(fetchMock, 5)[1]).toMatchObject({ + method: 'DELETE', + body: '{"ids":["media-id"]}', + }) + expect(getFetchCall(fetchMock, 6)[0]).toBe('https://cloud.example.test/api/album-items/filters') + expect(getFetchCall(fetchMock, 7)[0]).toBe('https://cloud.example.test/api/album-items/stats') + }) + + it('rejects invalid pagination before making a request', async () => { + const fetchMock = vi.fn() + const service = new MediaService(new HttpClient({ token: 'token', fetch: fetchMock })) + + await expect(service.list({ limit: 0 })).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.list({ limit: 101 })).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.list({ offset: -1 })).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.list({ offset: Number.POSITIVE_INFINITY })).rejects.toMatchObject({ + kind: 'validation', + }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('rejects empty and reserved media ids before making a request', async () => { + const fetchMock = vi.fn() + const service = new MediaService(new HttpClient({ token: 'token', fetch: fetchMock })) + + await expect(service.get(' ')).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.update('filters', {})).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.delete('stats')).rejects.toMatchObject({ kind: 'validation' }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('enforces Worker batch bounds and validates every batch id', async () => { + const fetchMock = vi.fn() + const service = new MediaService(new HttpClient({ token: 'token', fetch: fetchMock })) + const tooManyIds = Array.from({ length: 101 }, (_, index) => `media-${index}`) + + await expect(service.updateMany([])).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.updateMany([{ id: '', fileName: 'name.png' }])).rejects.toMatchObject({ + kind: 'validation', + }) + const malformedUpdates = [null] as unknown as Parameters[0] + await expect(service.updateMany(malformedUpdates)).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.deleteMany([])).rejects.toMatchObject({ kind: 'validation' }) + await expect(service.deleteMany(tooManyIds)).rejects.toMatchObject({ kind: 'validation' }) + expect(fetchMock).not.toHaveBeenCalled() + }) +}) diff --git a/tests/storage.test.ts b/tests/storage.test.ts new file mode 100644 index 0000000..4141641 --- /dev/null +++ b/tests/storage.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' + +import { ResumeStore } from '../src/upload/storage.js' + +class MemoryStorage implements Storage { + private readonly values = new Map() + + get length(): number { + return this.values.size + } + + clear(): void { + this.values.clear() + } + + getItem(key: string): string | null { + return this.values.get(key) ?? null + } + + key(index: number): string | null { + return [...this.values.keys()][index] ?? null + } + + removeItem(key: string): void { + this.values.delete(key) + } + + setItem(key: string, value: string): void { + this.values.set(key, value) + } +} + +function baseState(): Record { + return { + version: 1, + createdAt: Date.now(), + objectKey: 'objects/photo.png', + publicId: 'public-1', + uploadId: 'upload-1', + partSize: 1, + partCount: 1, + completedParts: [], + phase: 'uploading', + } +} + +describe('ResumeStore validation', () => { + it('removes persisted sessions with more than 10,000 parts', () => { + const storage = new MemoryStorage() + const store = new ResumeStore(storage) + storage.setItem('upload', JSON.stringify({ + ...baseState(), + partCount: 10_001, + })) + + expect(store.get('upload', 10_001)).toBeUndefined() + expect(storage.getItem('upload')).toBeNull() + }) + + it('removes persisted sessions containing an oversized ETag', () => { + const storage = new MemoryStorage() + const store = new ResumeStore(storage) + storage.setItem('upload', JSON.stringify({ + ...baseState(), + completedParts: [{ partNumber: 1, etag: 'e'.repeat(257) }], + phase: 'uploaded', + })) + + expect(store.get('upload', 1)).toBeUndefined() + expect(storage.getItem('upload')).toBeNull() + }) +}) diff --git a/tests/transport.test.ts b/tests/transport.test.ts new file mode 100644 index 0000000..bcdf377 --- /dev/null +++ b/tests/transport.test.ts @@ -0,0 +1,296 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { putBlob, type PutOptions } from '../src/upload/transport.js' + +type RuntimeListener = EventListenerOrEventListenerObject + +class FakeEventTarget { + private readonly listeners = new Map>() + + addEventListener(type: string, listener: RuntimeListener | null): void { + if (!listener) { + return + } + + const listeners = this.listeners.get(type) ?? new Set() + listeners.add(listener) + this.listeners.set(type, listeners) + } + + removeEventListener(type: string, listener: RuntimeListener | null): void { + if (!listener) { + return + } + + this.listeners.get(type)?.delete(listener) + } + + dispatch(type: string, event: Event): void { + for (const listener of this.listeners.get(type) ?? []) { + if (typeof listener === 'function') { + listener(event) + } else { + listener.handleEvent(event) + } + } + } + + listenerCount(type: string): number { + return this.listeners.get(type)?.size ?? 0 + } +} + +class FakeXMLHttpRequest extends FakeEventTarget { + static instances: FakeXMLHttpRequest[] = [] + + readonly upload = new FakeEventTarget() + readonly headers = new Map() + abortCount = 0 + async = true + body: Document | XMLHttpRequestBodyInit | null = null + method = '' + status = 0 + timeout = 0 + url = '' + withCredentials = true + + private readonly responseHeaders = new Map() + + constructor() { + super() + FakeXMLHttpRequest.instances.push(this) + } + + static reset(): void { + FakeXMLHttpRequest.instances = [] + } + + open(method: string, url: string, async = true): void { + this.method = method + this.url = url + this.async = async + } + + setRequestHeader(name: string, value: string): void { + this.headers.set(name, value) + } + + send(body: Document | XMLHttpRequestBodyInit | null = null): void { + this.body = body + } + + abort(): void { + this.abortCount += 1 + this.dispatch('abort', new Event('abort')) + } + + getResponseHeader(name: string): string | null { + return this.responseHeaders.get(name.toLowerCase()) ?? null + } + + respond(status: number, etag?: string): void { + this.status = status + if (etag !== undefined) { + this.responseHeaders.set('etag', etag) + } + this.dispatch('load', new Event('load')) + } + + failNetwork(): void { + this.dispatch('error', new Event('error')) + } + + timeOut(): void { + this.dispatch('timeout', new Event('timeout')) + } + + reportProgress(loaded: number): void { + const event = { loaded } as ProgressEvent + this.upload.dispatch('progress', event) + } +} + +const originalXMLHttpRequest = globalThis.XMLHttpRequest + +function makeOptions(overrides: Partial = {}): PutOptions { + return { + url: 'https://uploads.example.com/signed-object', + headers: { + 'Content-Type': 'image/png', + 'x-amz-checksum-sha256': 'checksum', + }, + body: new Blob(['image-bytes'], { type: 'image/png' }), + signal: new AbortController().signal, + timeoutMs: 30_000, + ...overrides, + } +} + +function currentRequest(): FakeXMLHttpRequest { + const request = FakeXMLHttpRequest.instances.at(-1) + if (!request) { + throw new Error('Expected an XMLHttpRequest instance') + } + return request +} + +function expectListenersRemoved(request: FakeXMLHttpRequest): void { + expect(request.listenerCount('load')).toBe(0) + expect(request.listenerCount('error')).toBe(0) + expect(request.listenerCount('abort')).toBe(0) + expect(request.listenerCount('timeout')).toBe(0) + expect(request.upload.listenerCount('progress')).toBe(0) +} + +beforeEach(() => { + FakeXMLHttpRequest.reset() + Object.defineProperty(globalThis, 'XMLHttpRequest', { + configurable: true, + value: FakeXMLHttpRequest, + writable: true, + }) +}) + +afterEach(() => { + Object.defineProperty(globalThis, 'XMLHttpRequest', { + configurable: true, + value: originalXMLHttpRequest, + writable: true, + }) +}) + +describe('putBlob', () => { + it('sends an anonymous PUT with the exact presigned headers and body', async () => { + const options = makeOptions() + const upload = putBlob(options) + const request = currentRequest() + + expect(request.method).toBe('PUT') + expect(request.url).toBe(options.url) + expect(request.async).toBe(true) + expect(request.withCredentials).toBe(false) + expect(request.timeout).toBe(options.timeoutMs) + expect(request.headers).toEqual(new Map(Object.entries(options.headers))) + expect(request.headers.has('Authorization')).toBe(false) + expect(request.body).toBe(options.body) + + request.respond(200, '"part-etag"') + + await expect(upload).resolves.toEqual({ etag: '"part-etag"' }) + expectListenersRemoved(request) + }) + + it('allows successful uploads without an ETag', async () => { + const upload = putBlob(makeOptions()) + const request = currentRequest() + + request.respond(204) + + await expect(upload).resolves.toEqual({}) + }) + + it('reports uploaded bytes and removes the progress listener after success', async () => { + const onProgress = vi.fn() + const upload = putBlob(makeOptions({ onProgress })) + const request = currentRequest() + + request.reportProgress(4) + request.reportProgress(11) + request.respond(200) + + await upload + expect(onProgress.mock.calls).toEqual([[4], [11]]) + expectListenersRemoved(request) + }) + + it('rejects non-success responses with status details', async () => { + const upload = putBlob(makeOptions()) + const request = currentRequest() + + request.respond(403) + + await expect(upload).rejects.toMatchObject({ + kind: 'api', + status: 403, + }) + expectListenersRemoved(request) + }) + + it('maps network failures and cleans up listeners', async () => { + const upload = putBlob(makeOptions()) + const request = currentRequest() + + request.failNetwork() + + await expect(upload).rejects.toMatchObject({ + kind: 'network', + }) + expectListenersRemoved(request) + }) + + it('maps timeouts and cleans up listeners', async () => { + const upload = putBlob(makeOptions()) + const request = currentRequest() + + request.timeOut() + + await expect(upload).rejects.toMatchObject({ + kind: 'timeout', + }) + expectListenersRemoved(request) + }) + + it('aborts the request when its signal is cancelled', async () => { + const controller = new AbortController() + const removeListener = vi.spyOn(controller.signal, 'removeEventListener') + const upload = putBlob(makeOptions({ signal: controller.signal })) + const request = currentRequest() + + controller.abort('cancelled by caller') + + await expect(upload).rejects.toMatchObject({ + kind: 'aborted', + cause: 'cancelled by caller', + }) + expect(request.abortCount).toBe(1) + expect(removeListener).toHaveBeenCalledWith('abort', expect.any(Function)) + expectListenersRemoved(request) + }) + + it('rejects immediately when the signal is already aborted', async () => { + const controller = new AbortController() + controller.abort() + + await expect(putBlob(makeOptions({ signal: controller.signal }))).rejects.toMatchObject({ + kind: 'aborted', + }) + expect(FakeXMLHttpRequest.instances).toHaveLength(0) + }) + + it('aborts and rejects if a progress callback throws', async () => { + const callbackError = new Error('progress consumer failed') + const upload = putBlob( + makeOptions({ + onProgress: () => { + throw callbackError + }, + }), + ) + const request = currentRequest() + + request.reportProgress(2) + + await expect(upload).rejects.toBe(callbackError) + expect(request.abortCount).toBe(1) + expectListenersRemoved(request) + }) + + it.each(['ftp://uploads.example.com/object', 'not a url'])( + 'rejects an invalid upload URL before creating a request: %s', + async url => { + await expect(putBlob(makeOptions({ url }))).rejects.toMatchObject({ + kind: 'validation', + }) + expect(FakeXMLHttpRequest.instances).toHaveLength(0) + }, + ) +}) diff --git a/tests/upload.test.ts b/tests/upload.test.ts new file mode 100644 index 0000000..861a8f6 --- /dev/null +++ b/tests/upload.test.ts @@ -0,0 +1,783 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { PicGoCloudClient } from '../src/client.js' +import { PicGoCloudError } from '../src/errors.js' +import type { PutOptions, PutResult } from '../src/upload/transport.js' + +const { putBlobMock } = vi.hoisted(() => ({ + putBlobMock: vi.fn<(options: PutOptions) => Promise>(), +})) + +vi.mock('../src/upload/transport.js', () => ({ putBlob: putBlobMock })) + +const MIB = 1024 * 1024 +const MULTIPART_THRESHOLD = 10 * MIB +const API_BASE_URL = 'https://api.example.test' +const MEDIA = { id: 'media-1', imgUrl: 'https://cdn.example.test/media-1.png' } + +interface ApiCall { + path: string + body: unknown + authorization: string | null + signal?: AbortSignal | null +} + +type ApiHandler = (call: ApiCall) => Response | Promise + +class MemoryStorage implements Storage { + private readonly values = new Map() + + get length(): number { + return this.values.size + } + + clear(): void { + this.values.clear() + } + + getItem(key: string): string | null { + return this.values.get(key) ?? null + } + + key(index: number): string | null { + return [...this.values.keys()][index] ?? null + } + + removeItem(key: string): void { + this.values.delete(key) + } + + setItem(key: string, value: string): void { + this.values.set(key, value) + } + + storedValues(): string[] { + return [...this.values.values()] + } +} + +function dataResponse(data: unknown): Response { + return new Response(JSON.stringify({ success: true, data }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) +} + +function errorResponse(status: number, code = 'UPLOAD_FAILED'): Response { + return new Response(JSON.stringify({ success: false, code, message: code }), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +function createFetch(handler: ApiHandler): { + calls: ApiCall[] + fetchMock: ReturnType> +} { + const calls: ApiCall[] = [] + const fetchMock = vi.fn(async (input, init) => { + const rawUrl = typeof input === 'string' || input instanceof URL ? input.toString() : input.url + const rawBody = init?.body + const call: ApiCall = { + path: new URL(rawUrl).pathname, + body: typeof rawBody === 'string' ? JSON.parse(rawBody) as unknown : undefined, + authorization: new Headers(init?.headers).get('Authorization'), + signal: init?.signal, + } + calls.push(call) + return handler(call) + }) + return { calls, fetchMock } +} + +function imageFile(size: number, name = 'photo.png'): File { + return new File([new Uint8Array(size)], name, { type: 'image/png' }) +} + +function signedPart(partNumber: number, prefix = 'initial') { + return { + partNumber, + url: `https://uploads.example.test/${prefix}/part-${partNumber}`, + method: 'PUT' as const, + headers: { 'x-picgo-part': `${prefix}-${partNumber}` }, + } +} + +function multipartSession(fileSize: number, partSize: number, prefix = 'initial') { + const partCount = Math.ceil(fileSize / partSize) + return { + uploadId: 'upload-1', + objectKey: 'objects/photo.png', + publicId: 'public-1', + partSize, + partCount, + parts: Array.from({ length: partCount }, (_, index) => signedPart(index + 1, prefix)), + } +} + +function unexpected(call: ApiCall): never { + throw new Error(`Unexpected API request: ${call.path}`) +} + +function requestBody(call: ApiCall): T { + return call.body as T +} + +function uploadCalls(): PutOptions[] { + return putBlobMock.mock.calls.map(([options]) => options) +} + +beforeEach(() => { + putBlobMock.mockReset() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('PicGoCloud uploads', () => { + it('switches at 10 MiB, preserves presign headers, and uses server part sizes and ETags', async () => { + const largeFile = imageFile(MULTIPART_THRESHOLD, 'large.png') + const smallFile = imageFile(1024, 'small.png') + const session = multipartSession(largeFile.size, 3 * MIB) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/presign': + return dataResponse({ + objectKey: 'objects/small.png', + publicId: 'small-public', + uploadUrl: 'https://uploads.example.test/single', + method: 'PUT', + headers: { 'Content-Type': 'image/custom', 'x-presigned': 'keep-me' }, + }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/complete': + return dataResponse({ completed: true }) + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(async options => { + const match = /part-(\d+)$/.exec(options.url) + return match ? { etag: `etag-${match[1]}` } : {} + }) + const client = new PicGoCloudClient({ token: 'user-token', baseUrl: API_BASE_URL, fetch: fetchMock }) + + await expect(client.upload(smallFile)).resolves.toEqual(MEDIA) + await expect(client.upload(largeFile, { concurrency: 1 })).resolves.toEqual(MEDIA) + + expect(calls.filter(call => call.path === '/api/upload/presign')).toHaveLength(1) + expect(calls.filter(call => call.path === '/api/upload/multipart/initiate')).toHaveLength(1) + expect(uploadCalls()[0]?.headers).toEqual({ + 'Content-Type': 'image/custom', + 'x-presigned': 'keep-me', + }) + expect(uploadCalls().slice(1).map(call => call.body.size)).toEqual([3 * MIB, 3 * MIB, 3 * MIB, MIB]) + + const merge = calls.find(call => call.path === '/api/upload/multipart/complete') + expect(merge).toBeDefined() + expect(requestBody<{ parts: Array<{ partNumber: number; etag: string }> }>(merge!).parts).toEqual([ + { partNumber: 1, etag: 'etag-1' }, + { partNumber: 2, etag: 'etag-2' }, + { partNumber: 3, etag: 'etag-3' }, + { partNumber: 4, etag: 'etag-4' }, + ]) + }) + + it('resumes the same file for the same user, skips completed parts, and never persists either token', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, 4 * MIB) + let initiateCount = 0 + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'stable-user' }) + case '/api/upload/multipart/initiate': + initiateCount += 1 + return dataResponse(session) + case '/api/upload/multipart/part-urls': { + const body = requestBody<{ partNumbers: number[] }>(call) + return dataResponse({ parts: body.partNumbers.map(number => signedPart(number, 'refreshed')) }) + } + case '/api/upload/multipart/complete': + return dataResponse({ completed: true }) + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(async options => { + if (options.url.endsWith('part-1')) { + return { etag: 'etag-1' } + } + throw new PicGoCloudError('part rejected', { kind: 'upload' }) + }) + const firstClient = new PicGoCloudClient({ + token: 'secret-token-a', + baseUrl: API_BASE_URL, + fetch: fetchMock, + storage, + }) + + await expect(firstClient.upload(file, { concurrency: 1 })).rejects.toMatchObject({ kind: 'upload' }) + expect(storage.length).toBe(1) + const persisted = storage.storedValues().join('\n') + expect(persisted).not.toContain('secret-token-a') + expect(persisted).not.toContain('secret-token-b') + expect(persisted).toContain('"partNumber":1') + + putBlobMock.mockReset() + putBlobMock.mockImplementation(async options => { + const match = /part-(\d+)$/.exec(options.url) + return { etag: `etag-${match?.[1] ?? 'missing'}` } + }) + const secondClient = new PicGoCloudClient({ + token: 'secret-token-b', + baseUrl: API_BASE_URL, + fetch: fetchMock, + storage, + }) + + await expect(secondClient.upload(file, { concurrency: 1 })).resolves.toEqual(MEDIA) + expect(initiateCount).toBe(1) + expect(uploadCalls().map(call => call.url)).toEqual([ + 'https://uploads.example.test/refreshed/part-2', + 'https://uploads.example.test/refreshed/part-3', + ]) + expect(calls.filter(call => call.path === '/api/whoami').map(call => call.authorization)).toEqual([ + 'Bearer secret-token-a', + 'Bearer secret-token-b', + ]) + expect(storage.length).toBe(0) + }) + + it('keeps a paused multipart session but aborts and removes it on cancel', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, 5 * MIB) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/abort': + return dataResponse({ aborted: true }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(options => new Promise((resolve, reject) => { + if (options.signal.aborted) { + reject(options.signal.reason) + return + } + options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }) + })) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const task = client.createUpload(file, { concurrency: 1 }) + + const running = task.start() + await vi.waitFor(() => expect(putBlobMock).toHaveBeenCalledTimes(1)) + task.pause() + + await expect(running).rejects.toMatchObject({ kind: 'paused' }) + expect(task.status).toBe('paused') + expect(storage.length).toBe(1) + expect(calls.some(call => call.path === '/api/upload/multipart/abort')).toBe(false) + + await task.cancel() + + expect(task.status).toBe('cancelled') + expect(calls.filter(call => call.path === '/api/upload/multipart/abort')).toHaveLength(1) + expect(storage.length).toBe(0) + }) + + it('retains an uploaded state after registration failure and retries without uploading or merging again', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + let registrationAttempts = 0 + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/complete': + return dataResponse({ completed: true }) + case '/api/album-items/complete': + registrationAttempts += 1 + return registrationAttempts === 1 ? errorResponse(422, 'REGISTRATION_REJECTED') : dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockResolvedValue({ etag: 'etag-1' }) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const task = client.createUpload(file) + + await expect(task.start()).rejects.toMatchObject({ status: 422, code: 'REGISTRATION_REJECTED' }) + expect(storage.storedValues().join('')).toContain('"phase":"uploaded"') + expect(putBlobMock).toHaveBeenCalledTimes(1) + expect(calls.filter(call => call.path === '/api/upload/multipart/complete')).toHaveLength(1) + + await expect(task.start()).resolves.toEqual(MEDIA) + expect(putBlobMock).toHaveBeenCalledTimes(1) + expect(calls.filter(call => call.path === '/api/upload/multipart/complete')).toHaveLength(1) + expect(registrationAttempts).toBe(2) + expect(storage.length).toBe(0) + }) + + it('recovers a lost merge response by probing media registration instead of merging again', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const session = multipartSession(file.size, file.size) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/complete': + throw new TypeError('connection dropped after merge') + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockResolvedValue({ etag: 'etag-1' }) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock }) + + await expect(client.upload(file)).resolves.toEqual(MEDIA) + expect(calls.filter(call => call.path === '/api/upload/multipart/complete')).toHaveLength(1) + expect(calls.filter(call => call.path === '/api/album-items/complete')).toHaveLength(1) + }) + + it('aborts sibling multipart workers when one part fails permanently', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const session = multipartSession(file.size, 5 * MIB) + const siblingSignals: AbortSignal[] = [] + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(options => { + if (options.url.endsWith('part-1')) { + return Promise.reject(new PicGoCloudError('permanent part failure', { kind: 'upload' })) + } + siblingSignals.push(options.signal) + return new Promise((resolve, reject) => { + if (options.signal.aborted) { + reject(options.signal.reason) + return + } + options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }) + }) + }) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock }) + + await expect(client.upload(file, { concurrency: 2 })).rejects.toMatchObject({ + kind: 'upload', + message: 'permanent part failure', + }) + expect(siblingSignals).toHaveLength(1) + expect(siblingSignals[0]?.aborted).toBe(true) + expect(calls.some(call => call.path === '/api/upload/multipart/complete')).toBe(false) + }) + + it('gets a fresh single-upload URL and retries after a 403', async () => { + vi.useFakeTimers() + const file = imageFile(1024) + let presignCount = 0 + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/presign': + presignCount += 1 + return dataResponse({ + objectKey: `objects/photo-${presignCount}.png`, + publicId: `public-${presignCount}`, + uploadUrl: `https://uploads.example.test/single-${presignCount}`, + method: 'PUT', + headers: { 'x-signature': `signature-${presignCount}` }, + }) + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock + .mockRejectedValueOnce(new PicGoCloudError('signature expired', { kind: 'api', status: 403 })) + .mockResolvedValueOnce({}) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock }) + + const upload = client.upload(file) + await vi.advanceTimersByTimeAsync(0) + expect(presignCount).toBe(2) + await vi.advanceTimersByTimeAsync(1000) + + await expect(upload).resolves.toEqual(MEDIA) + expect(uploadCalls().map(call => call.url)).toEqual([ + 'https://uploads.example.test/single-1', + 'https://uploads.example.test/single-2', + ]) + const registration = calls.find(call => call.path === '/api/album-items/complete') + expect(requestBody<{ objectKey: string; publicId: string }>(registration!)).toMatchObject({ + objectKey: 'objects/photo-2.png', + publicId: 'public-2', + }) + }) + + it('rejects multipart uploads when the PUT response does not expose an ETag', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const session = multipartSession(file.size, file.size) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + default: + return unexpected(call) + } + }) + putBlobMock.mockResolvedValue({}) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock }) + + await expect(client.upload(file)).rejects.toMatchObject({ + kind: 'protocol', + message: expect.stringContaining('ETag'), + }) + expect(calls.some(call => call.path === '/api/upload/multipart/complete')).toBe(false) + }) + + it('resolves localhost upload paths and only adds bearer auth for exact local proxy routes', async () => { + const localBaseUrl = 'http://localhost:8787' + const targets = [ + '/api/upload/put/object-1', + '/api/upload/putevil/object-2', + 'https://r2.example.test/object-3', + ] + let presignCount = 0 + const { fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/presign': { + const index = presignCount++ + return dataResponse({ + objectKey: `objects/photo-${index}.png`, + publicId: `public-${index}`, + uploadUrl: targets[index], + method: 'PUT', + headers: { 'x-presigned': `signature-${index}` }, + }) + } + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockResolvedValue({}) + const client = new PicGoCloudClient({ token: 'local-user-token', baseUrl: localBaseUrl, fetch: fetchMock }) + + await client.upload(imageFile(128, 'first.png')) + await client.upload(imageFile(128, 'second.png')) + await client.upload(imageFile(128, 'third.png')) + + expect(uploadCalls().map(call => ({ url: call.url, headers: call.headers }))).toEqual([ + { + url: 'http://localhost:8787/api/upload/put/object-1', + headers: { + 'x-presigned': 'signature-0', + 'Content-Type': 'image/png', + Authorization: 'Bearer local-user-token', + }, + }, + { + url: 'http://localhost:8787/api/upload/putevil/object-2', + headers: { 'x-presigned': 'signature-1', 'Content-Type': 'image/png' }, + }, + { + url: 'https://r2.example.test/object-3', + headers: { 'x-presigned': 'signature-2', 'Content-Type': 'image/png' }, + }, + ]) + }) + + it('clears an uploaded session after registration returns 404 so the next start uploads afresh', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + let registrationAttempts = 0 + let initiationAttempts = 0 + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + initiationAttempts += 1 + return dataResponse({ ...session, uploadId: `upload-${initiationAttempts}` }) + case '/api/upload/multipart/complete': + return dataResponse({ completed: true }) + case '/api/album-items/complete': + registrationAttempts += 1 + return registrationAttempts === 1 ? errorResponse(404, 'OBJECT_NOT_FOUND') : dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(async () => ({ etag: `etag-${putBlobMock.mock.calls.length}` })) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const task = client.createUpload(file) + + await expect(task.start()).rejects.toMatchObject({ status: 404 }) + expect(storage.length).toBe(0) + + await expect(task.start()).resolves.toEqual(MEDIA) + expect(initiationAttempts).toBe(2) + expect(putBlobMock).toHaveBeenCalledTimes(2) + expect(calls.filter(call => call.path === '/api/upload/multipart/complete')).toHaveLength(2) + }) + + it('aborts and clears a multipart session when the backend reports a part mismatch', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/complete': + return errorResponse(409, 'MULTIPART_PART_MISMATCH') + case '/api/upload/multipart/abort': + return dataResponse({ aborted: true }) + default: + return unexpected(call) + } + }) + putBlobMock.mockResolvedValue({ etag: 'etag-1' }) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + + await expect(client.upload(file)).rejects.toMatchObject({ + status: 409, + code: 'MULTIPART_PART_MISMATCH', + }) + expect(calls.filter(call => call.path === '/api/upload/multipart/abort')).toHaveLength(1) + expect(storage.length).toBe(0) + }) + + it('uses the original account token to cancel after a restarted task detects an account change', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + const tokenProvider = vi.fn<() => Promise>() + .mockResolvedValueOnce('token-a') + .mockResolvedValueOnce('token-b') + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: call.authorization === 'Bearer token-a' ? 'user-a' : 'user-b' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/abort': + return dataResponse({ aborted: true }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(options => new Promise((resolve, reject) => { + if (options.signal.aborted) { + reject(options.signal.reason) + return + } + options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }) + })) + const client = new PicGoCloudClient({ token: tokenProvider, baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const task = client.createUpload(file) + + const firstStart = task.start() + await vi.waitFor(() => expect(putBlobMock).toHaveBeenCalledTimes(1)) + task.pause() + await expect(firstStart).rejects.toMatchObject({ kind: 'paused' }) + + await expect(task.start()).rejects.toMatchObject({ + kind: 'validation', + message: expect.stringContaining('account changed'), + }) + await task.cancel() + + const abortCall = calls.find(call => call.path === '/api/upload/multipart/abort') + expect(abortCall?.authorization).toBe('Bearer token-a') + expect(storage.length).toBe(0) + }) + + it('does not delete another cached session when a resume:false upload succeeds', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + let initiationAttempts = 0 + const { fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': { + initiationAttempts += 1 + const prefix = initiationAttempts === 1 ? 'cached' : 'independent' + return dataResponse({ + ...multipartSession(file.size, 5 * MIB, prefix), + uploadId: `${prefix}-upload`, + objectKey: `objects/${prefix}.png`, + publicId: `${prefix}-public`, + }) + } + case '/api/upload/multipart/complete': + return dataResponse({ completed: true }) + case '/api/album-items/complete': + return dataResponse({ item: MEDIA }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(async options => { + if (options.url.includes('/cached/part-1')) { + return { etag: 'cached-etag-1' } + } + throw new PicGoCloudError('leave the first session resumable', { kind: 'upload' }) + }) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + + await expect(client.upload(file, { concurrency: 1 })).rejects.toMatchObject({ kind: 'upload' }) + const cachedKey = storage.key(0) + const cachedValue = cachedKey ? storage.getItem(cachedKey) : null + expect(cachedValue).toContain('cached-upload') + + putBlobMock.mockReset() + putBlobMock.mockImplementation(async options => { + const partNumber = /part-(\d+)$/.exec(options.url)?.[1] + return { etag: `independent-etag-${partNumber}` } + }) + + await expect(client.upload(file, { concurrency: 1, resume: false })).resolves.toEqual(MEDIA) + expect(initiationAttempts).toBe(2) + expect(storage.length).toBe(1) + expect(cachedKey && storage.getItem(cachedKey)).toBe(cachedValue) + }) + + it('does not remove the active owner session when a duplicate task fails to acquire its lock', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/abort': + return dataResponse({ aborted: true }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(options => new Promise((resolve, reject) => { + if (options.signal.aborted) { + reject(options.signal.reason) + return + } + options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }) + })) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const owner = client.createUpload(file) + const duplicate = client.createUpload(file) + + const ownerRunning = owner.start() + await vi.waitFor(() => expect(putBlobMock).toHaveBeenCalledTimes(1)) + const cachedKey = storage.key(0) + const cachedValue = cachedKey ? storage.getItem(cachedKey) : null + + await expect(duplicate.start()).rejects.toMatchObject({ + kind: 'validation', + }) + await duplicate.cancel() + + expect(storage.length).toBe(1) + expect(cachedKey && storage.getItem(cachedKey)).toBe(cachedValue) + expect(calls.some(call => call.path === '/api/upload/multipart/abort')).toBe(false) + + await owner.cancel() + await expect(ownerRunning).rejects.toMatchObject({ kind: 'aborted' }) + expect(calls.filter(call => call.path === '/api/upload/multipart/abort')).toHaveLength(1) + expect(storage.length).toBe(0) + }) + + it('refuses to cancel a paused task while another task actively resumes its session', async () => { + const file = imageFile(MULTIPART_THRESHOLD) + const storage = new MemoryStorage() + const session = multipartSession(file.size, file.size) + const { calls, fetchMock } = createFetch(call => { + switch (call.path) { + case '/api/whoami': + return dataResponse({ userId: 'user-1' }) + case '/api/upload/multipart/initiate': + return dataResponse(session) + case '/api/upload/multipart/part-urls': + return dataResponse({ parts: [signedPart(1, 'resumed')] }) + case '/api/upload/multipart/abort': + return dataResponse({ aborted: true }) + default: + return unexpected(call) + } + }) + putBlobMock.mockImplementation(options => new Promise((resolve, reject) => { + if (options.signal.aborted) { + reject(options.signal.reason) + return + } + options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }) + })) + const client = new PicGoCloudClient({ token: 'token', baseUrl: API_BASE_URL, fetch: fetchMock, storage }) + const pausedTask = client.createUpload(file) + + const firstRun = pausedTask.start() + await vi.waitFor(() => expect(putBlobMock).toHaveBeenCalledTimes(1)) + pausedTask.pause() + await expect(firstRun).rejects.toMatchObject({ kind: 'paused' }) + + const resumedTask = client.createUpload(file) + const resumedRun = resumedTask.start() + await vi.waitFor(() => expect(putBlobMock).toHaveBeenCalledTimes(2)) + const cachedKey = storage.key(0) + const cachedValue = cachedKey ? storage.getItem(cachedKey) : null + + await expect(pausedTask.cancel()).rejects.toMatchObject({ + kind: 'validation', + }) + expect(storage.length).toBe(1) + expect(cachedKey && storage.getItem(cachedKey)).toBe(cachedValue) + expect(calls.some(call => call.path === '/api/upload/multipart/abort')).toBe(false) + + await resumedTask.cancel() + await expect(resumedRun).rejects.toMatchObject({ kind: 'aborted' }) + expect(calls.filter(call => call.path === '/api/upload/multipart/abort')).toHaveLength(1) + expect(storage.length).toBe(0) + }) +}) diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..7d6a7f8 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ace648c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "rolldown.config.ts", "vitest.config.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c84485b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ test: { include: ['tests/**/*.test.ts'] } }) From ba9966286d50e7dcea9f6805ea77bd23110ae97d Mon Sep 17 00:00:00 2001 From: PiEgg Date: Wed, 23 Sep 2026 18:51:38 +0800 Subject: [PATCH 2/8] docs: add English README and preserve Chinese documentation --- README.md | 78 ++++++++++++------------ README_ZH.md | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 README_ZH.md diff --git a/README.md b/README.md index 3854eeb..4135ca0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # @picgo/cloud-sdk -面向现代浏览器的 PicGo Cloud JavaScript SDK,使用 TypeScript 编写、Rolldown 构建,零运行时依赖。提供文件上传、分片续传和媒体管理,不包含 UI 组件。Node.js 场景请使用 PicGo-Core。 +[中文文档](README_ZH.md) -## 安装与使用 +A PicGo Cloud JavaScript SDK for modern browsers, written in TypeScript and built with Rolldown, with zero runtime dependencies. It provides file uploads, resumable multipart uploads, and media management without UI components. For Node.js, use PicGo-Core. + +## Installation and usage ```sh pnpm add @picgo/cloud-sdk @@ -18,26 +20,26 @@ const client = new PicGoCloudClient({ const media = await client.upload(file) console.log(media.id, media.imgUrl) -const page = await client.media.list({ limit: 20, offset: 0, search: '旅行' }) -await client.media.update(media.id, { fileName: '旅行照片.jpg' }) +const page = await client.media.list({ limit: 20, offset: 0, search: 'travel' }) +await client.media.update(media.id, { fileName: 'travel-photo.jpg' }) await client.media.delete(media.id) ``` -token 由使用网页的用户提供,代表该用户的账户权限。SDK 使用 `Authorization: Bearer`,不携带 Cookie,也不默认保存或打印 token。不要将站点开发者自己的 token 写进公开的前端构建产物。支持直接传入字符串,也支持 `token: () => currentToken` 或异步获取函数;每轮上传固定使用同一个 token,避免分片过程中混用账户。 +The person using your application supplies their own token, which grants access to their account. The SDK sends `Authorization: Bearer`, omits cookies, and does not persist or log tokens by default. Do not embed the site developer's token in a public frontend bundle. You can supply a string, `token: () => currentToken`, or an asynchronous token provider. Each upload attempt uses a single token throughout to avoid switching accounts between parts. ```ts const client = new PicGoCloudClient({ token: async () => getCurrentUserToken(), - baseUrl: 'https://api.picgo.app', // 默认值;本地开发可指向 Worker - timeoutMs: 30_000, // 单次 API 请求超时 - uploadTimeoutMs: 120_000, // 单次文件/分片 PUT 超时 - // storage: false, // 可关闭续传记录持久化 + baseUrl: 'https://api.picgo.app', // Default; use your Worker URL for local development + timeoutMs: 30_000, // Timeout per API request + uploadTimeoutMs: 120_000, // Timeout per file or part PUT + // storage: false, // Disable persistent upload recovery }) ``` -API 使用原生 fetch,可通过 `fetch` 选项注入自定义实现。上传字节使用原生 XMLHttpRequest 以提供浏览器上传进度,不经过自定义 fetch。模块导入本身不访问 DOM;实际上传需要浏览器的 Blob/File、XMLHttpRequest、Web Crypto 和 AbortSignal.any,建议在 HTTPS 或 localhost 安全上下文使用。 +API requests use native fetch; you can inject an implementation through the `fetch` option. File transfers use native XMLHttpRequest for upload progress and do not use the custom fetch implementation. Importing the module does not access the DOM. Uploads require the browser's Blob/File, XMLHttpRequest, Web Crypto, and AbortSignal.any APIs. Use an HTTPS or localhost secure context. -## 上传 +## Uploading files ```ts const media = await client.upload(file, { @@ -46,15 +48,15 @@ const media = await client.upload(file, { }, }) -// Blob 没有文件名,需要显式指定。 +// A Blob has no filename, so provide one explicitly. await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' }) ``` -小于 10 MiB 使用单次预签名 PUT,大于等于 10 MiB 自动分片。分片大小使用服务端返回值,默认并发 3,可通过 `concurrency` 设置为 1–6。SDK 接受 1 byte–1 GiB,实际文件格式、类型大小限制、套餐和配额仍由服务端校验。不转换、压缩或重新编码文件。可通过 `width`、`height` 提供图片尺寸。 +Files smaller than 10 MiB use a single presigned PUT; files at or above 10 MiB automatically use multipart uploads. Part sizes come from the server. The default concurrency is 3 and can be set to 1–6 using `concurrency`. The SDK accepts files from 1 byte to 1 GiB; the server still enforces supported formats, size limits by media type, plans, and quotas. Files are not converted, compressed, or re-encoded. Use `width` and `height` to supply image dimensions. -`phase` 为 `preparing`、`uploading`、`completing`、`completed`。`fraction` 仅表示文件传输比例,传输达到 100% 后还有合并和媒体入库步骤;只有 Promise 成功返回才代表上传完成。重试时传输进度可能回退。进度回调抛出的异常不会改变上传结果。 +The `phase` is `preparing`, `uploading`, `completing`, or `completed`. The `fraction` measures byte transfer only: reaching 100% may still leave multipart completion and media registration to finish. The upload is complete only when its Promise resolves. Transfer progress can decrease during retries. Exceptions thrown by progress callbacks do not affect the upload result. -### 暂停、继续与取消 +### Pausing, resuming, and cancelling ```ts import { PicGoCloudError } from '@picgo/cloud-sdk' @@ -66,7 +68,7 @@ cancelButton.addEventListener('click', async () => { try { await task.cancel() } catch (error) { - showError(error) // 网络失败时可再次调用 cancel() 重试清理 + showError(error) // Call cancel() again to retry cleanup after a network failure } }) @@ -84,27 +86,27 @@ startButton.addEventListener('click', startOrResume) resumeButton.addEventListener('click', startOrResume) ``` -`createUpload()` 不立即发送请求。`start()` 开始上传;正在运行时重复调用会返回同一个 Promise。`pause()` 中断当前请求,当前 Promise 以 `kind: 'paused'` 拒绝;等待它结束后再次 `start()` 继续。`cancel()` 中断并清理未完成的分片会话,任务取消后需新建任务。清理请求失败会向调用方报告,保留记录以便再次尝试。取消不回滚已成功入库的媒体;已经传完但未入库的对象由服务端清理策略处理。 +`createUpload()` does not send requests immediately. Call `start()` to begin; repeated calls while it is running return the same Promise. `pause()` interrupts the current requests and rejects that Promise with `kind: 'paused'`; once it settles, call `start()` again to continue. `cancel()` interrupts the upload and cleans up an unfinished multipart session. Create a new task after cancellation. Cleanup failures are reported to the caller, and recovery records remain available for another attempt. Cancellation does not roll back registered media; uploaded objects that were never registered are handled by the server's cleanup policy. -`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`;外部 signal 中断会保留续传信息,不等同于 `cancel()` 的服务端清理。已经中断的外部 signal 不能用于重新开始,应创建新任务。 +The `task.status` is `idle`, `running`, `paused`, `cancelled`, `failed`, or `completed`. If your application already uses an AbortController, you can pass its `signal`. Aborting an external signal retains recovery information and does not perform the server cleanup that `cancel()` does. An already-aborted external signal cannot be reused to restart an upload; create a new task. -### 断点续传 +### Resumable uploads -分片上传默认将会话及已完成分片的 ETag 写入当前站点的 localStorage,缓存有效期 24 小时。刷新页面后重新选择原文件,再调用 `upload()` 或 `createUpload().start()`,会自动恢复。SDK 以分块内容摘要识别文件,内存占用不随整个文件大小增长;准备阶段会读取文件内容。记录按 API 地址、账户和文件隔离,不包含 token、预签名 URL 或原始文件。 +By default, multipart uploads store their session and completed part ETags in the current site's localStorage, with a 24-hour cache lifetime. After refreshing the page, select the original file and call `upload()` or `createUpload().start()` to resume automatically. The SDK identifies files using chunked content hashes, reading the file during preparation without loading it all into memory. Records are isolated by API URL, account, and file. They contain no tokens, presigned URLs, or file contents. -新 Worker 的 `whoami.userId` 用于账户隔离,同一账户更换 token 后仍可恢复;旧 Worker 没有该字段时使用 token 摘要隔离,更换 token 后不会命中旧记录。同一任务不允许切换账户。Web Locks 可用时阻止跨标签页同时恢复同一文件;不支持 Web Locks 时只提供当前页面内的互斥。 +Newer Workers expose `whoami.userId` for account isolation, allowing the same account to resume after changing tokens. Older Workers without this field use a token hash for isolation, so changing the token will not match previous records. A task cannot switch accounts. When available, Web Locks prevent multiple tabs from resuming the same file simultaneously; otherwise, exclusion applies only within the current page. -续传需要当前站点仍保留本地记录、用户提供同一文件,且服务端会话尚未失效;不支持跨站点或跨设备恢复。浏览器存储不可用时自动降级为任务内存中的续传。`storage: false` 关闭持久化,`resume: false` 让该次上传忽略持久化记录。小文件不提供跨刷新续传,但同一任务入库失败后重试不会重复 PUT。分片上传会保留已合并待入库状态,直到入库成功才清除记录。 +Recovery requires the current site's local record, the same file, and an unexpired server session. It does not work across sites or devices. If browser storage is unavailable, recovery falls back to the task's in-memory state. Set `storage: false` to disable persistence, or `resume: false` to ignore persisted records for an upload. Small files cannot resume across page refreshes, but retrying registration on the same task does not repeat the PUT. Multipart uploads retain their merged-but-unregistered state until registration succeeds. -SDK 只重试可安全重试的步骤,分片 PUT 使用最多 3 次额外重试和 1/2/4 秒退避,403 时重新获取签名 URL。媒体更新、删除及新建上传会话不会被静默重试。丢失分片合并响应时,优先尝试幂等的媒体入库以确认是否已经合并,避免直接创建另一份上传。 +The SDK retries only steps that can safely be retried. Part PUTs receive up to 3 additional attempts with 1/2/4-second backoff; a 403 triggers a new signed URL. Media updates, deletions, and upload session creation are not silently retried. If the multipart completion response is lost, the SDK first attempts idempotent media registration to determine whether the object was already assembled, rather than immediately creating another upload. -## 媒体管理 +## Media management -PicGo Cloud 当前的“相册”是媒体条目列表,没有相册分组实体。`media` 对应 `/api/album-items`,不使用已弃用的 `/api/media`,不提供第三方图床记录导入。 +PicGo Cloud's current album is a list of media items, with no separate album grouping entity. The `media` API maps to `/api/album-items`, does not use the deprecated `/api/media`, and does not import records from third-party image hosts. -| 方法 | 返回值 | +| Method | Returns | | --- | --- | -| `client.whoami(options?)` | 当前用户资料,含新版服务端的 `userId` | +| `client.whoami(options?)` | Current user details, including `userId` on newer servers | | `client.media.list(query?, options?)` | `{ items, total, limit, offset }` | | `client.media.get(id, options?)` | `MediaItem` | | `client.media.update(id, changes, options?)` | `MediaItem` | @@ -114,19 +116,19 @@ PicGo Cloud 当前的“相册”是媒体条目列表,没有相册分组实 | `client.media.filters(options?)` | `{ contentTypes, types, exts }` | | `client.media.stats(options?)` | `{ total, types }` | -所有接口返回解包后的业务数据,失败则抛异常。`options` 可传 `signal`。列表支持 `search`、`contentType`、`type`、`ext`、`fileName`、`sort` 和 `order`,`sort` 为 `newest | oldest | fileName`,`order` 为 `asc | desc`。当前服务端 `search` 仅搜索文件名。`limit` 为 1–100,`offset` 从 0 开始;批量更新和删除每次 1–100 条,不自动拆批。批量结果保留后端处理数量,请检查 `skipped`。 +All methods return unwrapped business data and throw on failure. You can pass a `signal` through `options`. Listing supports `search`, `contentType`, `type`, `ext`, `fileName`, `sort`, and `order`. The `sort` values are `newest | oldest | fileName`, and `order` is `asc | desc`. The server currently searches filenames only. The `limit` is 1–100 and `offset` starts at 0. Batch updates and deletions accept 1–100 items per request and are not automatically split. Batch results preserve the server's processing counts; check `skipped` when updating items. ```ts await client.media.updateMany([ { id: firstId, fileName: 'a.png' }, - { id: secondId, extra: { description: '截图' } }, + { id: secondId, extra: { description: 'Screenshot' } }, ]) await client.media.deleteMany([firstId, secondId]) ``` -媒体字段沿用服务端命名,包括 `id`、`imgUrl`、`fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra`,时间戳为毫秒。更新修改媒体元数据,不修改存储中的文件内容;不允许更新 `size`、`extname`。删除是服务端软删除,目前没有恢复接口。 +Media fields retain their server names: `id`, `imgUrl`, `fileName`, `type`, `contentType`, `size`, `width`, `height`, `extname`, `createdAt`, `updatedAt`, `originImgUrl`, `url`, and `extra`. Timestamps are in milliseconds. Updates change media metadata, not the stored file contents; `size` and `extname` cannot be updated. Deletion is a server-side soft delete, and there is currently no restore API. -## 错误处理 +## Error handling ```ts import { PicGoCloudError } from '@picgo/cloud-sdk' @@ -143,21 +145,21 @@ try { } ``` -`PicGoCloudError` 包含错误类别 `kind`、可选 HTTP `status`、后端 `code` 和原始 `cause`。服务端部分错误没有 code,可按状态码兜底。SDK 不根据错误文案决定行为,不自动清除用户 token。浏览器可能把 CORS 拒绝表现为普通网络错误;诊断时检查 API 和 R2 两处跨域配置。 +`PicGoCloudError` includes the error `kind`, optional HTTP `status`, server `code`, and original `cause`. Some server errors have no code; use the status as a fallback. The SDK does not branch on error messages or automatically clear the user's token. Browsers may report CORS rejections as ordinary network errors, so check both the API and R2 CORS configurations when troubleshooting. -## 服务端接入条件 +## Server requirements -SDK 不会绕过浏览器跨域。配套 picgo-hub 分支 `feat-cloud-sdk` 为 SDK 使用的端点增加第三方 Bearer CORS,同时保留 Portal Cookie 和 OAuth 回调白名单。R2 Bucket 必须允许第三方 Origin 的 PUT、签名所需请求头,并通过 `ExposeHeaders` 暴露 `ETag`。具体配置及部署验证步骤见 hub 的 SDK CORS 文档。修改源码不代表线上配置已生效。 +The SDK does not bypass browser CORS. The companion picgo-hub branch, `feat-cloud-sdk`, enables third-party Bearer CORS for the SDK's endpoints while preserving Portal cookies and the OAuth callback allowlist. The R2 bucket must allow PUT requests from third-party origins and the headers required by signing, and expose `ETag` through `ExposeHeaders`. See the hub SDK CORS documentation for configuration and deployment verification. Source changes alone do not update production configuration. -业务请求始终发送给配置的 API。生产 R2 直传不携带账户 token;仅 localhost/127.0.0.1 的同源、已知 Worker 上传代理路径会添加 Bearer 以支持本地开发。 +Business requests always go to the configured API. Production R2 transfers do not include the account token. For local development, Bearer authentication is added only to recognized Worker upload proxy paths on the same localhost/127.0.0.1 origin as the API. -## 开发 +## Development ```sh pnpm install pnpm check ``` -`pnpm check` 运行类型检查、ESLint、Vitest 和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。`examples/basic.html` 是可搭配本地静态服务器使用的原生浏览器示例,先执行 `pnpm build`;其中的 token 只保留在页面内存中。 +`pnpm check` runs type checking, ESLint, Vitest, and the build. Output includes `dist/index.js`, a source map, and type declarations. The package is ESM-only and contains no Node polyfills. After running `pnpm build`, serve `examples/basic.html` with a local static server for a native browser example. Its token stays in page memory. -真实浏览器协议验证可运行 `pnpm build && pnpm test:browser:serve`,然后访问 `http://localhost:41780`。这个本地测试使用三个不同 Origin 模拟网页、API 和存储,检查真实 CORS 预检、XHR 进度、ETag、媒体管理以及合并后入库失败的恢复,不会访问真实账户或写入云端。页面显示 `passed: true` 代表通过,终端 Ctrl+C 停止服务。每次重新测试前重启测试服务,以重置模拟状态。 +To verify the protocol in a real browser, run `pnpm build && pnpm test:browser:serve` and open `http://localhost:41780`. This local test uses three different origins to simulate the page, API, and storage. It checks real CORS preflights, XHR progress, ETag access, media management, and recovery from registration failure after multipart completion. It does not access real accounts or write to cloud storage. The page displays `passed: true` on success. Stop the server with Ctrl+C, and restart it before each new test to reset the simulated state. diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..3854eeb --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,163 @@ +# @picgo/cloud-sdk + +面向现代浏览器的 PicGo Cloud JavaScript SDK,使用 TypeScript 编写、Rolldown 构建,零运行时依赖。提供文件上传、分片续传和媒体管理,不包含 UI 组件。Node.js 场景请使用 PicGo-Core。 + +## 安装与使用 + +```sh +pnpm add @picgo/cloud-sdk +``` + +```ts +import { PicGoCloudClient } from '@picgo/cloud-sdk' + +const client = new PicGoCloudClient({ + token: userToken, +}) + +const media = await client.upload(file) +console.log(media.id, media.imgUrl) + +const page = await client.media.list({ limit: 20, offset: 0, search: '旅行' }) +await client.media.update(media.id, { fileName: '旅行照片.jpg' }) +await client.media.delete(media.id) +``` + +token 由使用网页的用户提供,代表该用户的账户权限。SDK 使用 `Authorization: Bearer`,不携带 Cookie,也不默认保存或打印 token。不要将站点开发者自己的 token 写进公开的前端构建产物。支持直接传入字符串,也支持 `token: () => currentToken` 或异步获取函数;每轮上传固定使用同一个 token,避免分片过程中混用账户。 + +```ts +const client = new PicGoCloudClient({ + token: async () => getCurrentUserToken(), + baseUrl: 'https://api.picgo.app', // 默认值;本地开发可指向 Worker + timeoutMs: 30_000, // 单次 API 请求超时 + uploadTimeoutMs: 120_000, // 单次文件/分片 PUT 超时 + // storage: false, // 可关闭续传记录持久化 +}) +``` + +API 使用原生 fetch,可通过 `fetch` 选项注入自定义实现。上传字节使用原生 XMLHttpRequest 以提供浏览器上传进度,不经过自定义 fetch。模块导入本身不访问 DOM;实际上传需要浏览器的 Blob/File、XMLHttpRequest、Web Crypto 和 AbortSignal.any,建议在 HTTPS 或 localhost 安全上下文使用。 + +## 上传 + +```ts +const media = await client.upload(file, { + onProgress({ phase, loaded, total, fraction, resumed }) { + console.log(phase, loaded, total, fraction, resumed) + }, +}) + +// Blob 没有文件名,需要显式指定。 +await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' }) +``` + +小于 10 MiB 使用单次预签名 PUT,大于等于 10 MiB 自动分片。分片大小使用服务端返回值,默认并发 3,可通过 `concurrency` 设置为 1–6。SDK 接受 1 byte–1 GiB,实际文件格式、类型大小限制、套餐和配额仍由服务端校验。不转换、压缩或重新编码文件。可通过 `width`、`height` 提供图片尺寸。 + +`phase` 为 `preparing`、`uploading`、`completing`、`completed`。`fraction` 仅表示文件传输比例,传输达到 100% 后还有合并和媒体入库步骤;只有 Promise 成功返回才代表上传完成。重试时传输进度可能回退。进度回调抛出的异常不会改变上传结果。 + +### 暂停、继续与取消 + +```ts +import { PicGoCloudError } from '@picgo/cloud-sdk' + +const task = client.createUpload(file, { onProgress: updateProgress }) + +pauseButton.addEventListener('click', () => task.pause()) +cancelButton.addEventListener('click', async () => { + try { + await task.cancel() + } catch (error) { + showError(error) // 网络失败时可再次调用 cancel() 重试清理 + } +}) + +async function startOrResume() { + try { + const media = await task.start() + showUploadedMedia(media) + } catch (error) { + if (error instanceof PicGoCloudError && error.kind === 'paused') return + showError(error) + } +} + +startButton.addEventListener('click', startOrResume) +resumeButton.addEventListener('click', startOrResume) +``` + +`createUpload()` 不立即发送请求。`start()` 开始上传;正在运行时重复调用会返回同一个 Promise。`pause()` 中断当前请求,当前 Promise 以 `kind: 'paused'` 拒绝;等待它结束后再次 `start()` 继续。`cancel()` 中断并清理未完成的分片会话,任务取消后需新建任务。清理请求失败会向调用方报告,保留记录以便再次尝试。取消不回滚已成功入库的媒体;已经传完但未入库的对象由服务端清理策略处理。 + +`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`;外部 signal 中断会保留续传信息,不等同于 `cancel()` 的服务端清理。已经中断的外部 signal 不能用于重新开始,应创建新任务。 + +### 断点续传 + +分片上传默认将会话及已完成分片的 ETag 写入当前站点的 localStorage,缓存有效期 24 小时。刷新页面后重新选择原文件,再调用 `upload()` 或 `createUpload().start()`,会自动恢复。SDK 以分块内容摘要识别文件,内存占用不随整个文件大小增长;准备阶段会读取文件内容。记录按 API 地址、账户和文件隔离,不包含 token、预签名 URL 或原始文件。 + +新 Worker 的 `whoami.userId` 用于账户隔离,同一账户更换 token 后仍可恢复;旧 Worker 没有该字段时使用 token 摘要隔离,更换 token 后不会命中旧记录。同一任务不允许切换账户。Web Locks 可用时阻止跨标签页同时恢复同一文件;不支持 Web Locks 时只提供当前页面内的互斥。 + +续传需要当前站点仍保留本地记录、用户提供同一文件,且服务端会话尚未失效;不支持跨站点或跨设备恢复。浏览器存储不可用时自动降级为任务内存中的续传。`storage: false` 关闭持久化,`resume: false` 让该次上传忽略持久化记录。小文件不提供跨刷新续传,但同一任务入库失败后重试不会重复 PUT。分片上传会保留已合并待入库状态,直到入库成功才清除记录。 + +SDK 只重试可安全重试的步骤,分片 PUT 使用最多 3 次额外重试和 1/2/4 秒退避,403 时重新获取签名 URL。媒体更新、删除及新建上传会话不会被静默重试。丢失分片合并响应时,优先尝试幂等的媒体入库以确认是否已经合并,避免直接创建另一份上传。 + +## 媒体管理 + +PicGo Cloud 当前的“相册”是媒体条目列表,没有相册分组实体。`media` 对应 `/api/album-items`,不使用已弃用的 `/api/media`,不提供第三方图床记录导入。 + +| 方法 | 返回值 | +| --- | --- | +| `client.whoami(options?)` | 当前用户资料,含新版服务端的 `userId` | +| `client.media.list(query?, options?)` | `{ items, total, limit, offset }` | +| `client.media.get(id, options?)` | `MediaItem` | +| `client.media.update(id, changes, options?)` | `MediaItem` | +| `client.media.updateMany(items, options?)` | `{ items, updated, skipped }` | +| `client.media.delete(id, options?)` | `{ message }` | +| `client.media.deleteMany(ids, options?)` | `{ deleted }` | +| `client.media.filters(options?)` | `{ contentTypes, types, exts }` | +| `client.media.stats(options?)` | `{ total, types }` | + +所有接口返回解包后的业务数据,失败则抛异常。`options` 可传 `signal`。列表支持 `search`、`contentType`、`type`、`ext`、`fileName`、`sort` 和 `order`,`sort` 为 `newest | oldest | fileName`,`order` 为 `asc | desc`。当前服务端 `search` 仅搜索文件名。`limit` 为 1–100,`offset` 从 0 开始;批量更新和删除每次 1–100 条,不自动拆批。批量结果保留后端处理数量,请检查 `skipped`。 + +```ts +await client.media.updateMany([ + { id: firstId, fileName: 'a.png' }, + { id: secondId, extra: { description: '截图' } }, +]) +await client.media.deleteMany([firstId, secondId]) +``` + +媒体字段沿用服务端命名,包括 `id`、`imgUrl`、`fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra`,时间戳为毫秒。更新修改媒体元数据,不修改存储中的文件内容;不允许更新 `size`、`extname`。删除是服务端软删除,目前没有恢复接口。 + +## 错误处理 + +```ts +import { PicGoCloudError } from '@picgo/cloud-sdk' + +try { + await client.upload(file) +} catch (error) { + if (error instanceof PicGoCloudError) { + console.log(error.kind, error.status, error.code) + if (error.status === 401) showTokenInput() + } else { + throw error + } +} +``` + +`PicGoCloudError` 包含错误类别 `kind`、可选 HTTP `status`、后端 `code` 和原始 `cause`。服务端部分错误没有 code,可按状态码兜底。SDK 不根据错误文案决定行为,不自动清除用户 token。浏览器可能把 CORS 拒绝表现为普通网络错误;诊断时检查 API 和 R2 两处跨域配置。 + +## 服务端接入条件 + +SDK 不会绕过浏览器跨域。配套 picgo-hub 分支 `feat-cloud-sdk` 为 SDK 使用的端点增加第三方 Bearer CORS,同时保留 Portal Cookie 和 OAuth 回调白名单。R2 Bucket 必须允许第三方 Origin 的 PUT、签名所需请求头,并通过 `ExposeHeaders` 暴露 `ETag`。具体配置及部署验证步骤见 hub 的 SDK CORS 文档。修改源码不代表线上配置已生效。 + +业务请求始终发送给配置的 API。生产 R2 直传不携带账户 token;仅 localhost/127.0.0.1 的同源、已知 Worker 上传代理路径会添加 Bearer 以支持本地开发。 + +## 开发 + +```sh +pnpm install +pnpm check +``` + +`pnpm check` 运行类型检查、ESLint、Vitest 和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。`examples/basic.html` 是可搭配本地静态服务器使用的原生浏览器示例,先执行 `pnpm build`;其中的 token 只保留在页面内存中。 + +真实浏览器协议验证可运行 `pnpm build && pnpm test:browser:serve`,然后访问 `http://localhost:41780`。这个本地测试使用三个不同 Origin 模拟网页、API 和存储,检查真实 CORS 预检、XHR 进度、ETag、媒体管理以及合并后入库失败的恢复,不会访问真实账户或写入云端。页面显示 `passed: true` 代表通过,终端 Ctrl+C 停止服务。每次重新测试前重启测试服务,以重置模拟状态。 diff --git a/package.json b/package.json index f04ba17..819dc10 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/PicGo/picgo-cloud-sdk.git" }, "sideEffects": false, - "files": ["dist", "README.md", "LICENSE"], + "files": ["dist", "README.md", "README_ZH.md", "LICENSE"], "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", From 23613c7c22722ec7960b5121b7bfb1e55a90ba74 Mon Sep 17 00:00:00 2001 From: PiEgg Date: Wed, 23 Sep 2026 18:58:17 +0800 Subject: [PATCH 3/8] feat(dev): add env-configured browser playground --- .env.example | 5 +++++ README.md | 27 +++++++++++++++++++++- README_ZH.md | 27 +++++++++++++++++++++- examples/basic.html | 31 ++++++++++++++++++++++--- package.json | 3 ++- scripts/dev-server.mjs | 45 +++++++++++++++++++++++++++++++++++++ scripts/dev-server.test.mjs | 35 +++++++++++++++++++++++++++++ scripts/dev.mjs | 36 +++++++++++++++++++++++++++++ 8 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 .env.example create mode 100644 scripts/dev-server.mjs create mode 100644 scripts/dev-server.test.mjs create mode 100644 scripts/dev.mjs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..69cfcf2 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Backend used by the local browser playground (pnpm dev). +PICGO_API_URL=https://dev-api.picgo.app +# For PR 89: https://pr-89-dev-api.picgo.app +# Optional local port. 5175 exercises third-party CORS rather than Portal's allowlist. +PICGO_DEV_PORT=5175 diff --git a/README.md b/README.md index 4135ca0..4d6f533 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,36 @@ Business requests always go to the configured API. Production R2 transfers do no ## Development +Use Node.js 22.18+ (Node.js 24 recommended) for development. Environment files are loaded by Node.js itself; no dotenv package is needed. + ```sh pnpm install pnpm check ``` -`pnpm check` runs type checking, ESLint, Vitest, and the build. Output includes `dist/index.js`, a source map, and type declarations. The package is ESM-only and contains no Node polyfills. After running `pnpm build`, serve `examples/basic.html` with a local static server for a native browser example. Its token stays in page memory. +`pnpm check` runs type checking, ESLint, Vitest, local development server tests, and the build. Output includes `dist/index.js`, a source map, and type declarations. The package is ESM-only and contains no Node polyfills. + +### Test against a real backend + +Copy `.env.example` to `.env` and set the API URL for the environment you want to test: + +```dotenv +PICGO_API_URL=https://pr-89-dev-api.picgo.app +PICGO_DEV_PORT=5175 +``` + +```sh +pnpm dev +``` + +Open `http://localhost:5175`. The playground loads the configured API URL and provides token verification, uploads, pause/resume/cancel, media listing, details, renaming, and deletion. Enter your own token for that backend in the page; it stays in memory. Uploaded items automatically populate the media ID field for follow-up operations. Operations on this page use the real backend and affect that account's media. + +Start by verifying the token, then upload a file smaller than 10 MiB and one at or above 10 MiB. Test pausing and resuming a multipart upload, and refresh the page and reselect the same file to test recovery. Use the media buttons to verify that the uploaded item's metadata can be read, renamed, and deleted. If a fast connection makes interruption difficult, throttle the network in browser developer tools. + +To switch to the shared development environment, set `PICGO_API_URL=https://dev-api.picgo.app` and restart `pnpm dev`. Existing shell environment variables take precedence over `.env`. The dev command rebuilds the SDK when source files change; refresh the page to load the new build. Changes to `.env` require a server restart. `.env` is ignored by Git; only `PICGO_API_URL` is exposed to the browser, not other environment variables. + +Requests go directly from the browser to the selected backend and R2, without a local API proxy. Port 5175 deliberately exercises third-party CORS instead of the Portal's existing local-origin allowlist. The Worker CORS change must be deployed to the selected backend, and its R2 bucket must allow PUT and expose `ETag`. The `.env` setting configures only this development playground; applications consuming the published SDK still pass `baseUrl` explicitly. + +### Test with local protocol fixtures To verify the protocol in a real browser, run `pnpm build && pnpm test:browser:serve` and open `http://localhost:41780`. This local test uses three different origins to simulate the page, API, and storage. It checks real CORS preflights, XHR progress, ETag access, media management, and recovery from registration failure after multipart completion. It does not access real accounts or write to cloud storage. The page displays `passed: true` on success. Stop the server with Ctrl+C, and restart it before each new test to reset the simulated state. diff --git a/README_ZH.md b/README_ZH.md index 3854eeb..814d8f5 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -153,11 +153,36 @@ SDK 不会绕过浏览器跨域。配套 picgo-hub 分支 `feat-cloud-sdk` 为 S ## 开发 +开发需要 Node.js 22.18+,推荐 Node.js 24。环境文件由 Node.js 原生加载,无需安装 dotenv。 + ```sh pnpm install pnpm check ``` -`pnpm check` 运行类型检查、ESLint、Vitest 和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。`examples/basic.html` 是可搭配本地静态服务器使用的原生浏览器示例,先执行 `pnpm build`;其中的 token 只保留在页面内存中。 +`pnpm check` 运行类型检查、ESLint、Vitest、本地开发服务器测试和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。 + +### 连接真实后端测试 + +将 `.env.example` 复制为 `.env`,设置要测试的 API 地址: + +```dotenv +PICGO_API_URL=https://pr-89-dev-api.picgo.app +PICGO_DEV_PORT=5175 +``` + +```sh +pnpm dev +``` + +打开 `http://localhost:5175`。测试页会读取配置的 API 地址,提供验证 token、上传、暂停/继续/取消、媒体列表、详情、重命名和删除。请在页面中输入该环境下你自己的 token,token 仅保留在内存中。上传成功后会自动填写媒体 ID,方便继续操作。这个页面连接真实后端,操作会作用于该账户的媒体数据。 + +建议先验证 token,再分别上传小于 10 MiB 和大于等于 10 MiB 的文件。对分片上传测试暂停与继续,再刷新页面、重新选择同一文件测试恢复。通过媒体操作按钮验证上传后的条目能查询、重命名和删除。网速太快不方便中断时,可在浏览器开发者工具中启用网络限速。 + +切换到公共开发环境时,将 `PICGO_API_URL` 改为 `https://dev-api.picgo.app` 后重启 `pnpm dev`。已有的 shell 环境变量优先于 `.env`。修改 SDK 源码会自动重新构建,刷新页面即可加载;修改 `.env` 需要重启。`.env` 已被 Git 忽略,只有 `PICGO_API_URL` 会提供给浏览器,其他环境变量不会暴露。 + +请求由浏览器直接发送至选定后端和 R2,不经过本地 API 代理。默认端口 5175 用于验证第三方跨域,不复用 Portal 已允许的本地域名。目标后端需要部署 Worker CORS 改动,对应 R2 Bucket 需要允许 PUT 并暴露 `ETag`。`.env` 仅配置本地测试页,接入已发布 SDK 的应用仍需显式传入 `baseUrl`。 + +### 使用本地模拟服务测试 真实浏览器协议验证可运行 `pnpm build && pnpm test:browser:serve`,然后访问 `http://localhost:41780`。这个本地测试使用三个不同 Origin 模拟网页、API 和存储,检查真实 CORS 预检、XHR 进度、ETag、媒体管理以及合并后入库失败的恢复,不会访问真实账户或写入云端。页面显示 `passed: true` 代表通过,终端 Ctrl+C 停止服务。每次重新测试前重启测试服务,以重置模拟状态。 diff --git a/examples/basic.html b/examples/basic.html index 19c59c3..e1cb997 100644 --- a/examples/basic.html +++ b/examples/basic.html @@ -6,10 +6,13 @@

PicGo Cloud SDK

提供你自己的 token,选择文件上传。暂停后可继续,刷新后重新选择同一文件可续传。

- + - + + + +

 
 
diff --git a/package.json b/package.json
index 819dc10..93c2530 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,11 @@
   "types": "./dist/index.d.ts",
   "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
   "scripts": {
+    "dev": "pnpm build && node --env-file-if-exists=.env scripts/dev.mjs",
     "build": "rolldown -c && tsc -p tsconfig.build.json",
     "typecheck": "tsc --noEmit",
     "lint": "eslint .",
-    "test": "vitest run",
+    "test": "vitest run && node --test scripts/dev-server.test.mjs",
     "test:browser:serve": "node scripts/browser-smoke.mjs",
     "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build",
     "prepublishOnly": "pnpm check"
diff --git a/scripts/dev-server.mjs b/scripts/dev-server.mjs
new file mode 100644
index 0000000..f4730bc
--- /dev/null
+++ b/scripts/dev-server.mjs
@@ -0,0 +1,45 @@
+import { createServer } from 'node:http'
+import { readFile } from 'node:fs/promises'
+
+export function readDevConfig(env = process.env) {
+  const api = new URL(env.PICGO_API_URL || 'https://dev-api.picgo.app')
+  if (!['https:', 'http:'].includes(api.protocol) || api.username || api.password || api.search || api.hash) {
+    throw new Error('PICGO_API_URL must be an HTTP(S) URL without credentials, query, or hash')
+  }
+  const port = Number(env.PICGO_DEV_PORT || 5175)
+  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PICGO_DEV_PORT must be between 1 and 65535')
+  return { apiUrl: api.href.replace(/\/+$/, ''), port }
+}
+
+export function createDevServer(config) {
+  // Serve an explicit allowlist. Never expose .env or the rest of the workspace.
+  const files = new Map([
+    ['/', ['../examples/basic.html', 'text/html; charset=utf-8']],
+    ['/examples/basic.html', ['../examples/basic.html', 'text/html; charset=utf-8']],
+    ['/dist/index.js', ['../dist/index.js', 'text/javascript; charset=utf-8']],
+    ['/dist/index.js.map', ['../dist/index.js.map', 'application/json']],
+  ])
+  return createServer(async (request, response) => {
+    response.setHeader('Cache-Control', 'no-store')
+    response.setHeader('X-Content-Type-Options', 'nosniff')
+    if (request.method !== 'GET' && request.method !== 'HEAD') {
+      response.writeHead(405, { Allow: 'GET, HEAD' }); response.end(); return
+    }
+    const pathname = new URL(request.url, 'http://localhost').pathname
+    if (pathname === '/config.json') {
+      response.writeHead(200, { 'Content-Type': 'application/json' })
+      response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ apiUrl: config.apiUrl }))
+      return
+    }
+    const entry = files.get(pathname)
+    if (!entry) { response.writeHead(404); response.end('Not found'); return }
+    try {
+      const content = await readFile(new URL(entry[0], import.meta.url))
+      response.writeHead(200, { 'Content-Type': entry[1] })
+      response.end(request.method === 'HEAD' ? undefined : content)
+    } catch {
+      response.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' })
+      response.end('Build output is not ready. Run pnpm build and reload.')
+    }
+  })
+}
diff --git a/scripts/dev-server.test.mjs b/scripts/dev-server.test.mjs
new file mode 100644
index 0000000..dfdaed7
--- /dev/null
+++ b/scripts/dev-server.test.mjs
@@ -0,0 +1,35 @@
+import assert from 'node:assert/strict'
+import { once } from 'node:events'
+import test from 'node:test'
+import { createDevServer, readDevConfig } from './dev-server.mjs'
+
+test('development config uses only the API URL and local port', () => {
+  assert.deepEqual(readDevConfig({ PICGO_API_URL: 'https://pr-89-dev-api.picgo.app/', PICGO_DEV_PORT: '5176', SECRET: 'private' }), {
+    apiUrl: 'https://pr-89-dev-api.picgo.app', port: 5176,
+  })
+  assert.equal(readDevConfig({}).apiUrl, 'https://dev-api.picgo.app')
+  for (const value of ['file:///tmp/file', 'https://user:secret@example.com', 'https://example.com?token=secret']) {
+    assert.throws(() => readDevConfig({ PICGO_API_URL: value }))
+  }
+  assert.throws(() => readDevConfig({ PICGO_DEV_PORT: '0' }))
+})
+
+test('development server exposes config and examples, never env files or a backend proxy', async () => {
+  const server = createDevServer({ apiUrl: 'https://preview.example.com', secret: 'private' })
+  server.listen(0, '127.0.0.1')
+  await once(server, 'listening')
+  const base = `http://127.0.0.1:${server.address().port}`
+  try {
+    const config = await fetch(`${base}/config.json`)
+    assert.deepEqual(await config.json(), { apiUrl: 'https://preview.example.com' })
+    assert.equal(config.headers.get('cache-control'), 'no-store')
+    assert.equal((await fetch(base)).status, 200)
+    for (const path of ['/.env', '/.env.example', '/package.json', '/src/client.ts', '/api/whoami', '/%2eenv']) {
+      assert.equal((await fetch(`${base}${path}`)).status, 404)
+    }
+    assert.equal((await fetch(`${base}/config.json`, { method: 'POST' })).status, 405)
+  } finally {
+    server.closeAllConnections()
+    await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
+  }
+})
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
new file mode 100644
index 0000000..d3303bf
--- /dev/null
+++ b/scripts/dev.mjs
@@ -0,0 +1,36 @@
+import { watch } from 'rolldown'
+import buildConfig from '../rolldown.config.ts'
+import { createDevServer, readDevConfig } from './dev-server.mjs'
+
+const config = readDevConfig()
+const server = createDevServer(config)
+const watcher = watch(buildConfig)
+
+watcher.on('event', async event => {
+  if (event.code === 'BUNDLE_END') {
+    await event.result.close()
+    console.log('SDK rebuilt. Refresh the browser to use the latest build.')
+  } else if (event.code === 'ERROR') {
+    console.error(event.error)
+    await event.result?.close()
+  }
+})
+
+server.listen(config.port, '127.0.0.1', () => {
+  console.log(`Playground: http://localhost:${config.port}`)
+  console.log(`Backend: ${config.apiUrl}`)
+  console.log('Enter your token in the browser. Edit .env and restart to switch backends.')
+})
+
+async function shutdown() {
+  server.close()
+  server.closeAllConnections()
+  await watcher.close()
+}
+
+server.on('error', async error => {
+  console.error(error)
+  process.exitCode = 1
+  await shutdown()
+})
+for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, shutdown)

From 12f8f70ae28dc0dc544ead17114a0948b4b047a7 Mon Sep 17 00:00:00 2001
From: PiEgg 
Date: Wed, 23 Sep 2026 19:03:44 +0800
Subject: [PATCH 4/8] docs: focus SDK documentation on third-party integration

---
 .env.example                |  5 ++-
 README.md                   | 60 +++++++++++------------------------
 README_ZH.md                | 62 ++++++++++++-------------------------
 scripts/dev-server.mjs      |  2 +-
 scripts/dev-server.test.mjs |  6 ++--
 5 files changed, 44 insertions(+), 91 deletions(-)

diff --git a/.env.example b/.env.example
index 69cfcf2..fd450d4 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,4 @@
 # Backend used by the local browser playground (pnpm dev).
-PICGO_API_URL=https://dev-api.picgo.app
-# For PR 89: https://pr-89-dev-api.picgo.app
-# Optional local port. 5175 exercises third-party CORS rather than Portal's allowlist.
+PICGO_API_URL=https://api.picgo.app
+# Optional local port.
 PICGO_DEV_PORT=5175
diff --git a/README.md b/README.md
index 4d6f533..7349b6e 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ The person using your application supplies their own token, which grants access
 ```ts
 const client = new PicGoCloudClient({
   token: async () => getCurrentUserToken(),
-  baseUrl: 'https://api.picgo.app', // Default; use your Worker URL for local development
+  baseUrl: 'https://api.picgo.app', // Default PicGo Cloud API URL
   timeoutMs: 30_000, // Timeout per API request
   uploadTimeoutMs: 120_000, // Timeout per file or part PUT
   // storage: false, // Disable persistent upload recovery
@@ -52,9 +52,9 @@ const media = await client.upload(file, {
 await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' })
 ```
 
-Files smaller than 10 MiB use a single presigned PUT; files at or above 10 MiB automatically use multipart uploads. Part sizes come from the server. The default concurrency is 3 and can be set to 1–6 using `concurrency`. The SDK accepts files from 1 byte to 1 GiB; the server still enforces supported formats, size limits by media type, plans, and quotas. Files are not converted, compressed, or re-encoded. Use `width` and `height` to supply image dimensions.
+The SDK chooses the upload method automatically. Files at or above 10 MiB support multipart uploads, with a default concurrency of 3; set `concurrency` to a value from 1–6 to adjust it. Accepted files are between 1 byte and 1 GiB, subject to your PicGo Cloud plan, remaining quota, and supported file formats. Files are not converted, compressed, or re-encoded. Use `width` and `height` to supply image dimensions.
 
-The `phase` is `preparing`, `uploading`, `completing`, or `completed`. The `fraction` measures byte transfer only: reaching 100% may still leave multipart completion and media registration to finish. The upload is complete only when its Promise resolves. Transfer progress can decrease during retries. Exceptions thrown by progress callbacks do not affect the upload result.
+The `phase` is `preparing`, `uploading`, `completing`, or `completed`. The `fraction` measures transfer progress: reaching 100% does not mean the file is ready to use yet. Wait for the Promise to resolve before using the returned media item. Transfer progress can decrease during retries. Exceptions thrown by progress callbacks do not affect the upload result.
 
 ### Pausing, resuming, and cancelling
 
@@ -86,27 +86,25 @@ startButton.addEventListener('click', startOrResume)
 resumeButton.addEventListener('click', startOrResume)
 ```
 
-`createUpload()` does not send requests immediately. Call `start()` to begin; repeated calls while it is running return the same Promise. `pause()` interrupts the current requests and rejects that Promise with `kind: 'paused'`; once it settles, call `start()` again to continue. `cancel()` interrupts the upload and cleans up an unfinished multipart session. Create a new task after cancellation. Cleanup failures are reported to the caller, and recovery records remain available for another attempt. Cancellation does not roll back registered media; uploaded objects that were never registered are handled by the server's cleanup policy.
+`createUpload()` does not send requests immediately. Call `start()` to begin; repeated calls while it is running return the same Promise. `pause()` interrupts the upload and rejects that Promise with `kind: 'paused'`; once it settles, call `start()` again to continue. `cancel()` stops the upload and releases its pending resources. If it fails, you can call it again. Create a new task after cancellation. Cancelling does not delete an already completed media item; use `client.media.delete()` for that.
 
-The `task.status` is `idle`, `running`, `paused`, `cancelled`, `failed`, or `completed`. If your application already uses an AbortController, you can pass its `signal`. Aborting an external signal retains recovery information and does not perform the server cleanup that `cancel()` does. An already-aborted external signal cannot be reused to restart an upload; create a new task.
+The `task.status` is `idle`, `running`, `paused`, `cancelled`, `failed`, or `completed`. If your application already uses an AbortController, you can pass its `signal`. Aborting an external signal retains recovery information; use `cancel()` to abandon the upload. An already-aborted external signal cannot be reused to restart an upload; create a new task.
 
 ### Resumable uploads
 
-By default, multipart uploads store their session and completed part ETags in the current site's localStorage, with a 24-hour cache lifetime. After refreshing the page, select the original file and call `upload()` or `createUpload().start()` to resume automatically. The SDK identifies files using chunked content hashes, reading the file during preparation without loading it all into memory. Records are isolated by API URL, account, and file. They contain no tokens, presigned URLs, or file contents.
+Multipart uploads save recovery information in the current site's localStorage for up to 24 hours. After refreshing the page, select the same file and call `upload()` or `createUpload().start()` to resume automatically. Recovery records are isolated by account and file and contain no tokens or file contents. Keep the same account and API URL when resuming.
 
-Newer Workers expose `whoami.userId` for account isolation, allowing the same account to resume after changing tokens. Older Workers without this field use a token hash for isolation, so changing the token will not match previous records. A task cannot switch accounts. When available, Web Locks prevent multiple tabs from resuming the same file simultaneously; otherwise, exclusion applies only within the current page.
+Recovery is limited to the same site and browser, while the saved upload remains valid. It does not work across sites or devices. If browser storage is unavailable, you can still pause and resume the current task while the page remains open. Set `storage: false` on the client to disable persistence, or `resume: false` in upload options to ignore persisted records. If a file smaller than 10 MiB is paused during transfer, resuming restarts that transfer. Avoid uploading the same file from multiple tabs at once.
 
-Recovery requires the current site's local record, the same file, and an unexpired server session. It does not work across sites or devices. If browser storage is unavailable, recovery falls back to the task's in-memory state. Set `storage: false` to disable persistence, or `resume: false` to ignore persisted records for an upload. Small files cannot resume across page refreshes, but retrying registration on the same task does not repeat the PUT. Multipart uploads retain their merged-but-unregistered state until registration succeeds.
-
-The SDK retries only steps that can safely be retried. Part PUTs receive up to 3 additional attempts with 1/2/4-second backoff; a 403 triggers a new signed URL. Media updates, deletions, and upload session creation are not silently retried. If the multipart completion response is lost, the SDK first attempts idempotent media registration to determine whether the object was already assembled, rather than immediately creating another upload.
+Temporary upload failures are retried automatically when safe. If a task fails, catch the error and call `start()` again to retry. Media updates and deletions are not automatically retried.
 
 ## Media management
 
-PicGo Cloud's current album is a list of media items, with no separate album grouping entity. The `media` API maps to `/api/album-items`, does not use the deprecated `/api/media`, and does not import records from third-party image hosts.
+Use `client.media` to browse and manage the media in the user's PicGo Cloud account.
 
 | Method | Returns |
 | --- | --- |
-| `client.whoami(options?)` | Current user details, including `userId` on newer servers |
+| `client.whoami(options?)` | Current user details; also verifies the supplied token |
 | `client.media.list(query?, options?)` | `{ items, total, limit, offset }` |
 | `client.media.get(id, options?)` | `MediaItem` |
 | `client.media.update(id, changes, options?)` | `MediaItem` |
@@ -126,7 +124,7 @@ await client.media.updateMany([
 await client.media.deleteMany([firstId, secondId])
 ```
 
-Media fields retain their server names: `id`, `imgUrl`, `fileName`, `type`, `contentType`, `size`, `width`, `height`, `extname`, `createdAt`, `updatedAt`, `originImgUrl`, `url`, and `extra`. Timestamps are in milliseconds. Updates change media metadata, not the stored file contents; `size` and `extname` cannot be updated. Deletion is a server-side soft delete, and there is currently no restore API.
+`MediaItem` includes `id`, `imgUrl`, and optional metadata such as `fileName`, `type`, `contentType`, `size`, `width`, `height`, `extname`, `createdAt`, `updatedAt`, `originImgUrl`, `url`, and `extra`. Timestamps are in milliseconds. Updates change metadata, not the file contents; `size` and `extname` cannot be updated. Deleted items disappear from the media list. The SDK does not provide a restore operation.
 
 ## Error handling
 
@@ -145,46 +143,24 @@ try {
 }
 ```
 
-`PicGoCloudError` includes the error `kind`, optional HTTP `status`, server `code`, and original `cause`. Some server errors have no code; use the status as a fallback. The SDK does not branch on error messages or automatically clear the user's token. Browsers may report CORS rejections as ordinary network errors, so check both the API and R2 CORS configurations when troubleshooting.
-
-## Server requirements
-
-The SDK does not bypass browser CORS. The companion picgo-hub branch, `feat-cloud-sdk`, enables third-party Bearer CORS for the SDK's endpoints while preserving Portal cookies and the OAuth callback allowlist. The R2 bucket must allow PUT requests from third-party origins and the headers required by signing, and expose `ETag` through `ExposeHeaders`. See the hub SDK CORS documentation for configuration and deployment verification. Source changes alone do not update production configuration.
-
-Business requests always go to the configured API. Production R2 transfers do not include the account token. For local development, Bearer authentication is added only to recognized Worker upload proxy paths on the same localhost/127.0.0.1 origin as the API.
+`PicGoCloudError` includes the error `kind`, optional HTTP `status`, service `code`, and original `cause`. Use `kind`, `status`, and `code` for application logic rather than matching error messages. A 401 usually means the user needs to provide a valid token. For network errors, check connectivity and the configured API URL. The SDK does not automatically clear the user's token.
 
-## Development
+## Try the example locally
 
-Use Node.js 22.18+ (Node.js 24 recommended) for development. Environment files are loaded by Node.js itself; no dotenv package is needed.
-
-```sh
-pnpm install
-pnpm check
-```
-
-`pnpm check` runs type checking, ESLint, Vitest, local development server tests, and the build. Output includes `dist/index.js`, a source map, and type declarations. The package is ESM-only and contains no Node polyfills.
-
-### Test against a real backend
-
-Copy `.env.example` to `.env` and set the API URL for the environment you want to test:
+Clone this repository and use Node.js 22.18+ (Node.js 24 recommended). Copy `.env.example` to `.env`:
 
 ```dotenv
-PICGO_API_URL=https://pr-89-dev-api.picgo.app
+PICGO_API_URL=https://api.picgo.app
 PICGO_DEV_PORT=5175
 ```
 
 ```sh
+pnpm install
 pnpm dev
 ```
 
-Open `http://localhost:5175`. The playground loads the configured API URL and provides token verification, uploads, pause/resume/cancel, media listing, details, renaming, and deletion. Enter your own token for that backend in the page; it stays in memory. Uploaded items automatically populate the media ID field for follow-up operations. Operations on this page use the real backend and affect that account's media.
+Open `http://localhost:5175` and enter your PicGo Cloud token. The example lets you verify the token, upload files, pause/resume/cancel uploads, and list, inspect, rename, or delete media. Uploaded items automatically populate the media ID field for follow-up operations. These actions affect the account associated with your token.
 
 Start by verifying the token, then upload a file smaller than 10 MiB and one at or above 10 MiB. Test pausing and resuming a multipart upload, and refresh the page and reselect the same file to test recovery. Use the media buttons to verify that the uploaded item's metadata can be read, renamed, and deleted. If a fast connection makes interruption difficult, throttle the network in browser developer tools.
 
-To switch to the shared development environment, set `PICGO_API_URL=https://dev-api.picgo.app` and restart `pnpm dev`. Existing shell environment variables take precedence over `.env`. The dev command rebuilds the SDK when source files change; refresh the page to load the new build. Changes to `.env` require a server restart. `.env` is ignored by Git; only `PICGO_API_URL` is exposed to the browser, not other environment variables.
-
-Requests go directly from the browser to the selected backend and R2, without a local API proxy. Port 5175 deliberately exercises third-party CORS instead of the Portal's existing local-origin allowlist. The Worker CORS change must be deployed to the selected backend, and its R2 bucket must allow PUT and expose `ETag`. The `.env` setting configures only this development playground; applications consuming the published SDK still pass `baseUrl` explicitly.
-
-### Test with local protocol fixtures
-
-To verify the protocol in a real browser, run `pnpm build && pnpm test:browser:serve` and open `http://localhost:41780`. This local test uses three different origins to simulate the page, API, and storage. It checks real CORS preflights, XHR progress, ETag access, media management, and recovery from registration failure after multipart completion. It does not access real accounts or write to cloud storage. The page displays `passed: true` on success. Stop the server with Ctrl+C, and restart it before each new test to reset the simulated state.
+You can change `PICGO_API_URL` to test another PicGo Cloud API endpoint; restart `pnpm dev` after editing `.env`. Existing shell environment variables take precedence. Source changes rebuild automatically; refresh the page to load them. `.env` is ignored by Git, and the token stays in page memory. These environment settings apply only to the example. In your own application, pass `baseUrl` to `PicGoCloudClient` when overriding the default URL.
diff --git a/README_ZH.md b/README_ZH.md
index 814d8f5..7ef4c56 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -1,5 +1,7 @@
 # @picgo/cloud-sdk
 
+[English](README.md)
+
 面向现代浏览器的 PicGo Cloud JavaScript SDK,使用 TypeScript 编写、Rolldown 构建,零运行时依赖。提供文件上传、分片续传和媒体管理,不包含 UI 组件。Node.js 场景请使用 PicGo-Core。
 
 ## 安装与使用
@@ -28,7 +30,7 @@ token 由使用网页的用户提供,代表该用户的账户权限。SDK 使
 ```ts
 const client = new PicGoCloudClient({
   token: async () => getCurrentUserToken(),
-  baseUrl: 'https://api.picgo.app', // 默认值;本地开发可指向 Worker
+  baseUrl: 'https://api.picgo.app', // 默认 PicGo Cloud API 地址
   timeoutMs: 30_000, // 单次 API 请求超时
   uploadTimeoutMs: 120_000, // 单次文件/分片 PUT 超时
   // storage: false, // 可关闭续传记录持久化
@@ -50,9 +52,9 @@ const media = await client.upload(file, {
 await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' })
 ```
 
-小于 10 MiB 使用单次预签名 PUT,大于等于 10 MiB 自动分片。分片大小使用服务端返回值,默认并发 3,可通过 `concurrency` 设置为 1–6。SDK 接受 1 byte–1 GiB,实际文件格式、类型大小限制、套餐和配额仍由服务端校验。不转换、压缩或重新编码文件。可通过 `width`、`height` 提供图片尺寸。
+SDK 会自动选择上传方式,大于等于 10 MiB 的文件支持分片上传。默认并发 3,可通过 `concurrency` 设置为 1–6。支持的文件大小为 1 byte–1 GiB,实际可上传的格式和大小还取决于你的 PicGo Cloud 套餐及剩余配额。文件不会被转换、压缩或重新编码。可通过 `width`、`height` 提供图片尺寸。
 
-`phase` 为 `preparing`、`uploading`、`completing`、`completed`。`fraction` 仅表示文件传输比例,传输达到 100% 后还有合并和媒体入库步骤;只有 Promise 成功返回才代表上传完成。重试时传输进度可能回退。进度回调抛出的异常不会改变上传结果。
+`phase` 为 `preparing`、`uploading`、`completing`、`completed`。`fraction` 表示传输进度,达到 100% 不代表文件已经可以使用;请等待 Promise 成功返回后再使用媒体条目。重试时进度可能回退。进度回调抛出的异常不会改变上传结果。
 
 ### 暂停、继续与取消
 
@@ -84,27 +86,25 @@ startButton.addEventListener('click', startOrResume)
 resumeButton.addEventListener('click', startOrResume)
 ```
 
-`createUpload()` 不立即发送请求。`start()` 开始上传;正在运行时重复调用会返回同一个 Promise。`pause()` 中断当前请求,当前 Promise 以 `kind: 'paused'` 拒绝;等待它结束后再次 `start()` 继续。`cancel()` 中断并清理未完成的分片会话,任务取消后需新建任务。清理请求失败会向调用方报告,保留记录以便再次尝试。取消不回滚已成功入库的媒体;已经传完但未入库的对象由服务端清理策略处理。
+`createUpload()` 不立即发送请求。调用 `start()` 开始上传,正在运行时重复调用会返回同一个 Promise。`pause()` 中断上传,当前 Promise 以 `kind: 'paused'` 拒绝;等待它结束后再次调用 `start()` 继续。`cancel()` 停止上传并释放未完成任务的资源,失败时可以再次调用。取消后需新建任务。取消不会删除已经上传成功的媒体,删除请使用 `client.media.delete()`。
 
-`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`;外部 signal 中断会保留续传信息,不等同于 `cancel()` 的服务端清理。已经中断的外部 signal 不能用于重新开始,应创建新任务。
+`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`。外部 signal 中断会保留续传信息;要放弃上传,请使用 `cancel()`。已经中断的外部 signal 不能用于重新开始,应创建新任务。
 
 ### 断点续传
 
-分片上传默认将会话及已完成分片的 ETag 写入当前站点的 localStorage,缓存有效期 24 小时。刷新页面后重新选择原文件,再调用 `upload()` 或 `createUpload().start()`,会自动恢复。SDK 以分块内容摘要识别文件,内存占用不随整个文件大小增长;准备阶段会读取文件内容。记录按 API 地址、账户和文件隔离,不包含 token、预签名 URL 或原始文件。
-
-新 Worker 的 `whoami.userId` 用于账户隔离,同一账户更换 token 后仍可恢复;旧 Worker 没有该字段时使用 token 摘要隔离,更换 token 后不会命中旧记录。同一任务不允许切换账户。Web Locks 可用时阻止跨标签页同时恢复同一文件;不支持 Web Locks 时只提供当前页面内的互斥。
+分片上传会在当前站点的 localStorage 中保存最多 24 小时的恢复信息。刷新页面后重新选择同一文件,再调用 `upload()` 或 `createUpload().start()`,即可自动恢复。恢复记录按账户和文件隔离,不包含 token 或文件内容。续传时请保持同一账户和 API 地址。
 
-续传需要当前站点仍保留本地记录、用户提供同一文件,且服务端会话尚未失效;不支持跨站点或跨设备恢复。浏览器存储不可用时自动降级为任务内存中的续传。`storage: false` 关闭持久化,`resume: false` 让该次上传忽略持久化记录。小文件不提供跨刷新续传,但同一任务入库失败后重试不会重复 PUT。分片上传会保留已合并待入库状态,直到入库成功才清除记录。
+恢复仅适用于同一站点、同一浏览器中仍有效的上传记录,不支持跨站点或跨设备恢复。浏览器存储不可用时,页面保持打开期间仍可暂停和继续当前任务。在客户端设置 `storage: false` 可关闭持久化,在上传选项中设置 `resume: false` 可忽略已有记录。小于 10 MiB 的文件若在传输过程中暂停,继续时会重新传输。请避免在多个标签页同时上传同一文件。
 
-SDK 只重试可安全重试的步骤,分片 PUT 使用最多 3 次额外重试和 1/2/4 秒退避,403 时重新获取签名 URL。媒体更新、删除及新建上传会话不会被静默重试。丢失分片合并响应时,优先尝试幂等的媒体入库以确认是否已经合并,避免直接创建另一份上传。
+临时上传失败会在安全的情况下自动重试。任务失败后,可以捕获错误并再次调用 `start()` 重试。媒体更新和删除不会自动重试。
 
 ## 媒体管理
 
-PicGo Cloud 当前的“相册”是媒体条目列表,没有相册分组实体。`media` 对应 `/api/album-items`,不使用已弃用的 `/api/media`,不提供第三方图床记录导入。
+使用 `client.media` 浏览和管理用户 PicGo Cloud 账户中的媒体。
 
 | 方法 | 返回值 |
 | --- | --- |
-| `client.whoami(options?)` | 当前用户资料,含新版服务端的 `userId` |
+| `client.whoami(options?)` | 当前用户资料,也可用于验证 token |
 | `client.media.list(query?, options?)` | `{ items, total, limit, offset }` |
 | `client.media.get(id, options?)` | `MediaItem` |
 | `client.media.update(id, changes, options?)` | `MediaItem` |
@@ -124,7 +124,7 @@ await client.media.updateMany([
 await client.media.deleteMany([firstId, secondId])
 ```
 
-媒体字段沿用服务端命名,包括 `id`、`imgUrl`、`fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra`,时间戳为毫秒。更新修改媒体元数据,不修改存储中的文件内容;不允许更新 `size`、`extname`。删除是服务端软删除,目前没有恢复接口。
+`MediaItem` 包含 `id`、`imgUrl`,以及可选的 `fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra` 等元数据。时间戳为毫秒。更新修改元数据,不修改文件内容;不允许更新 `size`、`extname`。删除后条目将不再出现在媒体列表中,SDK 不提供恢复操作。
 
 ## 错误处理
 
@@ -143,46 +143,24 @@ try {
 }
 ```
 
-`PicGoCloudError` 包含错误类别 `kind`、可选 HTTP `status`、后端 `code` 和原始 `cause`。服务端部分错误没有 code,可按状态码兜底。SDK 不根据错误文案决定行为,不自动清除用户 token。浏览器可能把 CORS 拒绝表现为普通网络错误;诊断时检查 API 和 R2 两处跨域配置。
-
-## 服务端接入条件
-
-SDK 不会绕过浏览器跨域。配套 picgo-hub 分支 `feat-cloud-sdk` 为 SDK 使用的端点增加第三方 Bearer CORS,同时保留 Portal Cookie 和 OAuth 回调白名单。R2 Bucket 必须允许第三方 Origin 的 PUT、签名所需请求头,并通过 `ExposeHeaders` 暴露 `ETag`。具体配置及部署验证步骤见 hub 的 SDK CORS 文档。修改源码不代表线上配置已生效。
-
-业务请求始终发送给配置的 API。生产 R2 直传不携带账户 token;仅 localhost/127.0.0.1 的同源、已知 Worker 上传代理路径会添加 Bearer 以支持本地开发。
-
-## 开发
-
-开发需要 Node.js 22.18+,推荐 Node.js 24。环境文件由 Node.js 原生加载,无需安装 dotenv。
-
-```sh
-pnpm install
-pnpm check
-```
-
-`pnpm check` 运行类型检查、ESLint、Vitest、本地开发服务器测试和构建。产物为 `dist/index.js`、sourcemap 及类型声明,仅发布 ESM,无 Node polyfill。
+`PicGoCloudError` 包含错误类别 `kind`、可选 HTTP `status`、服务错误码 `code` 和原始 `cause`。请根据 `kind`、`status`、`code` 处理错误,不要匹配错误文案。401 通常表示需要用户提供有效的 token。遇到网络错误时,检查网络连接和配置的 API 地址。SDK 不会自动清除用户 token。
 
-### 连接真实后端测试
+## 本地试用示例
 
-将 `.env.example` 复制为 `.env`,设置要测试的 API 地址:
+克隆本仓库,使用 Node.js 22.18+,推荐 Node.js 24。将 `.env.example` 复制为 `.env`:
 
 ```dotenv
-PICGO_API_URL=https://pr-89-dev-api.picgo.app
+PICGO_API_URL=https://api.picgo.app
 PICGO_DEV_PORT=5175
 ```
 
 ```sh
+pnpm install
 pnpm dev
 ```
 
-打开 `http://localhost:5175`。测试页会读取配置的 API 地址,提供验证 token、上传、暂停/继续/取消、媒体列表、详情、重命名和删除。请在页面中输入该环境下你自己的 token,token 仅保留在内存中。上传成功后会自动填写媒体 ID,方便继续操作。这个页面连接真实后端,操作会作用于该账户的媒体数据。
+打开 `http://localhost:5175`,输入你的 PicGo Cloud token。示例页支持验证 token、上传、暂停/继续/取消、媒体列表、详情、重命名和删除。上传成功后会自动填写媒体 ID,方便继续操作。这些操作会作用于 token 所属账户的媒体数据。
 
 建议先验证 token,再分别上传小于 10 MiB 和大于等于 10 MiB 的文件。对分片上传测试暂停与继续,再刷新页面、重新选择同一文件测试恢复。通过媒体操作按钮验证上传后的条目能查询、重命名和删除。网速太快不方便中断时,可在浏览器开发者工具中启用网络限速。
 
-切换到公共开发环境时,将 `PICGO_API_URL` 改为 `https://dev-api.picgo.app` 后重启 `pnpm dev`。已有的 shell 环境变量优先于 `.env`。修改 SDK 源码会自动重新构建,刷新页面即可加载;修改 `.env` 需要重启。`.env` 已被 Git 忽略,只有 `PICGO_API_URL` 会提供给浏览器,其他环境变量不会暴露。
-
-请求由浏览器直接发送至选定后端和 R2,不经过本地 API 代理。默认端口 5175 用于验证第三方跨域,不复用 Portal 已允许的本地域名。目标后端需要部署 Worker CORS 改动,对应 R2 Bucket 需要允许 PUT 并暴露 `ETag`。`.env` 仅配置本地测试页,接入已发布 SDK 的应用仍需显式传入 `baseUrl`。
-
-### 使用本地模拟服务测试
-
-真实浏览器协议验证可运行 `pnpm build && pnpm test:browser:serve`,然后访问 `http://localhost:41780`。这个本地测试使用三个不同 Origin 模拟网页、API 和存储,检查真实 CORS 预检、XHR 进度、ETag、媒体管理以及合并后入库失败的恢复,不会访问真实账户或写入云端。页面显示 `passed: true` 代表通过,终端 Ctrl+C 停止服务。每次重新测试前重启测试服务,以重置模拟状态。
+可以修改 `PICGO_API_URL` 来测试其他 PicGo Cloud API 地址,修改 `.env` 后需重启 `pnpm dev`。已有的 shell 环境变量优先于 `.env`。修改源码后会自动重新构建,刷新页面即可加载。`.env` 已被 Git 忽略,token 仅保留在页面内存中。这些环境设置只作用于示例页;在自己的应用中覆盖默认 API 地址时,请向 `PicGoCloudClient` 传入 `baseUrl`。
diff --git a/scripts/dev-server.mjs b/scripts/dev-server.mjs
index f4730bc..12f6482 100644
--- a/scripts/dev-server.mjs
+++ b/scripts/dev-server.mjs
@@ -2,7 +2,7 @@ import { createServer } from 'node:http'
 import { readFile } from 'node:fs/promises'
 
 export function readDevConfig(env = process.env) {
-  const api = new URL(env.PICGO_API_URL || 'https://dev-api.picgo.app')
+  const api = new URL(env.PICGO_API_URL || 'https://api.picgo.app')
   if (!['https:', 'http:'].includes(api.protocol) || api.username || api.password || api.search || api.hash) {
     throw new Error('PICGO_API_URL must be an HTTP(S) URL without credentials, query, or hash')
   }
diff --git a/scripts/dev-server.test.mjs b/scripts/dev-server.test.mjs
index dfdaed7..f34ad24 100644
--- a/scripts/dev-server.test.mjs
+++ b/scripts/dev-server.test.mjs
@@ -4,10 +4,10 @@ import test from 'node:test'
 import { createDevServer, readDevConfig } from './dev-server.mjs'
 
 test('development config uses only the API URL and local port', () => {
-  assert.deepEqual(readDevConfig({ PICGO_API_URL: 'https://pr-89-dev-api.picgo.app/', PICGO_DEV_PORT: '5176', SECRET: 'private' }), {
-    apiUrl: 'https://pr-89-dev-api.picgo.app', port: 5176,
+  assert.deepEqual(readDevConfig({ PICGO_API_URL: 'https://preview.example.com/', PICGO_DEV_PORT: '5176', SECRET: 'private' }), {
+    apiUrl: 'https://preview.example.com', port: 5176,
   })
-  assert.equal(readDevConfig({}).apiUrl, 'https://dev-api.picgo.app')
+  assert.equal(readDevConfig({}).apiUrl, 'https://api.picgo.app')
   for (const value of ['file:///tmp/file', 'https://user:secret@example.com', 'https://example.com?token=secret']) {
     assert.throws(() => readDevConfig({ PICGO_API_URL: value }))
   }

From 8aa6bdaca355fd8e26237443e62c5404c60fc43e Mon Sep 17 00:00:00 2001
From: PiEgg 
Date: Wed, 23 Sep 2026 19:27:58 +0800
Subject: [PATCH 5/8] feat: expose a consistent URL for all media types

---
 README.md                | 27 ++++++++++++++++++++++++--
 README_ZH.md             | 27 ++++++++++++++++++++++++--
 src/media-item.ts        | 24 +++++++++++++++++++++++
 src/media.ts             | 16 ++++++++++-----
 src/types.ts             |  4 +++-
 src/upload/task.ts       | 11 +++++++----
 tests/browser/smoke.js   |  5 ++++-
 tests/client.test.ts     |  2 +-
 tests/media-item.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++
 tests/upload.test.ts     | 26 ++++++++++++++++++++++++-
 10 files changed, 167 insertions(+), 17 deletions(-)
 create mode 100644 src/media-item.ts
 create mode 100644 tests/media-item.test.ts

diff --git a/README.md b/README.md
index 7349b6e..f29ecea 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ const client = new PicGoCloudClient({
 })
 
 const media = await client.upload(file)
-console.log(media.id, media.imgUrl)
+console.log(media.id, media.url)
 
 const page = await client.media.list({ limit: 20, offset: 0, search: 'travel' })
 await client.media.update(media.id, { fileName: 'travel-photo.jpg' })
@@ -124,7 +124,30 @@ await client.media.updateMany([
 await client.media.deleteMany([firstId, secondId])
 ```
 
-`MediaItem` includes `id`, `imgUrl`, and optional metadata such as `fileName`, `type`, `contentType`, `size`, `width`, `height`, `extname`, `createdAt`, `updatedAt`, `originImgUrl`, `url`, and `extra`. Timestamps are in milliseconds. Updates change metadata, not the file contents; `size` and `extname` cannot be updated. Deleted items disappear from the media list. The SDK does not provide a restore operation.
+Uploads and media queries return `MediaItem`. Use `url` to access images, videos, or other files, and `contentType` to choose how to display them.
+
+```ts
+interface MediaItem {
+  id: string
+  url: string                      // Media URL; always present
+  fileName?: string
+  type?: string                    // Upload source, e.g. "picgo-cloud"
+  contentType?: string             // MIME type, e.g. "image/png" or "video/mp4"
+  size?: number                    // Bytes
+  width?: number                   // Pixels, when available
+  height?: number                  // Pixels, when available
+  extname?: string                 // File extension, e.g. ".mp4"
+  createdAt?: number               // Unix timestamp in milliseconds
+  updatedAt?: number               // Unix timestamp in milliseconds
+  originImgUrl?: string            // Original URL, when available
+  extra?: Record  // Additional metadata
+
+  /** @deprecated Use url. Retained for compatibility. */
+  imgUrl: string
+}
+```
+
+Import this type with `import type { MediaItem } from '@picgo/cloud-sdk'`. Updates change metadata, not file contents; `size` and `extname` cannot be updated. Deleted items disappear from the media list. The SDK does not provide a restore operation.
 
 ## Error handling
 
diff --git a/README_ZH.md b/README_ZH.md
index 7ef4c56..66b58c9 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -18,7 +18,7 @@ const client = new PicGoCloudClient({
 })
 
 const media = await client.upload(file)
-console.log(media.id, media.imgUrl)
+console.log(media.id, media.url)
 
 const page = await client.media.list({ limit: 20, offset: 0, search: '旅行' })
 await client.media.update(media.id, { fileName: '旅行照片.jpg' })
@@ -124,7 +124,30 @@ await client.media.updateMany([
 await client.media.deleteMany([firstId, secondId])
 ```
 
-`MediaItem` 包含 `id`、`imgUrl`,以及可选的 `fileName`、`type`、`contentType`、`size`、`width`、`height`、`extname`、`createdAt`、`updatedAt`、`originImgUrl`、`url`、`extra` 等元数据。时间戳为毫秒。更新修改元数据,不修改文件内容;不允许更新 `size`、`extname`。删除后条目将不再出现在媒体列表中,SDK 不提供恢复操作。
+上传和媒体查询返回 `MediaItem`。图片、视频或其他文件都通过 `url` 访问,可以根据 `contentType` 选择展示方式。
+
+```ts
+interface MediaItem {
+  id: string
+  url: string                      // 媒体访问地址,始终有值
+  fileName?: string
+  type?: string                    // 上传来源,例如 "picgo-cloud"
+  contentType?: string             // MIME 类型,例如 "image/png"、"video/mp4"
+  size?: number                    // 文件大小,单位 byte
+  width?: number                   // 宽度,单位 px,可用时返回
+  height?: number                  // 高度,单位 px,可用时返回
+  extname?: string                 // 文件扩展名,例如 ".mp4"
+  createdAt?: number               // Unix 时间戳,单位毫秒
+  updatedAt?: number               // Unix 时间戳,单位毫秒
+  originImgUrl?: string            // 原始地址,可用时返回
+  extra?: Record  // 扩展元数据
+
+  /** @deprecated 请使用 url,此字段仅用于兼容。 */
+  imgUrl: string
+}
+```
+
+可以通过 `import type { MediaItem } from '@picgo/cloud-sdk'` 导入类型。更新修改元数据,不修改文件内容;不允许更新 `size`、`extname`。删除后条目将不再出现在媒体列表中,SDK 不提供恢复操作。
 
 ## 错误处理
 
diff --git a/src/media-item.ts b/src/media-item.ts
new file mode 100644
index 0000000..fdfe686
--- /dev/null
+++ b/src/media-item.ts
@@ -0,0 +1,24 @@
+import { PicGoCloudError } from './errors.js'
+import type { MediaItem } from './types.js'
+
+/** Transport compatibility stays internal; the public item always has a URL. */
+export type MediaItemResponse = Omit & {
+  url?: string
+  imgUrl?: string
+}
+
+function nonEmpty(value: unknown): value is string {
+  return typeof value === 'string' && value.trim().length > 0
+}
+
+export function normalizeMediaItem(item: MediaItemResponse): MediaItem {
+  if (!item || !nonEmpty(item.id)) {
+    throw new PicGoCloudError('Invalid media item response', { kind: 'protocol' })
+  }
+  // Cloud items may carry their current media domain in the legacy field while
+  // url still contains the address saved when the item was created.
+  const candidates = item.type === 'picgo-cloud' ? [item.imgUrl, item.url] : [item.url, item.imgUrl]
+  const url = candidates.find(nonEmpty)
+  if (!url) throw new PicGoCloudError('Media item response has no URL', { kind: 'protocol' })
+  return { ...item, url, imgUrl: nonEmpty(item.imgUrl) ? item.imgUrl : url }
+}
diff --git a/src/media.ts b/src/media.ts
index 58cf584..ea0ab11 100644
--- a/src/media.ts
+++ b/src/media.ts
@@ -1,5 +1,7 @@
 import { PicGoCloudError } from './errors.js'
 import type { HttpClient } from './http.js'
+import { normalizeMediaItem } from './media-item.js'
+import type { MediaItemResponse } from './media-item.js'
 import type {
   MediaBulkDeleteResult,
   MediaBulkUpdateItem,
@@ -14,7 +16,9 @@ import type {
   RequestOptions,
 } from './types.js'
 
-type MediaItemResult = { item: MediaItem }
+type MediaItemResult = { item: MediaItemResponse }
+type MediaListResponse = Omit & { items: MediaItemResponse[] }
+type MediaBulkUpdateResponse = Omit & { items: MediaItemResponse[] }
 const RESERVED_MEDIA_IDS = new Set(['complete', 'filters', 'stats'])
 
 function validation(message: string): never {
@@ -70,10 +74,11 @@ export class MediaService {
     addQueryValue(params, 'order', query.order)
 
     const queryString = params.toString()
-    return await this.http.request(
+    const result = await this.http.request(
       `/api/album-items${queryString ? `?${queryString}` : ''}`,
       { signal: options.signal },
     )
+    return { ...result, items: result.items.map(normalizeMediaItem) }
   }
 
   async get(id: string, options: RequestOptions = {}): Promise {
@@ -82,7 +87,7 @@ export class MediaService {
       `/api/album-items/${encodeURIComponent(id)}`,
       { signal: options.signal },
     )
-    return result.item
+    return normalizeMediaItem(result.item)
   }
 
   async update(id: string, changes: MediaUpdate, options: RequestOptions = {}): Promise {
@@ -91,7 +96,7 @@ export class MediaService {
       `/api/album-items/${encodeURIComponent(id)}`,
       { method: 'PATCH', body: changes, signal: options.signal },
     )
-    return result.item
+    return normalizeMediaItem(result.item)
   }
 
   async updateMany(
@@ -105,11 +110,12 @@ export class MediaService {
       }
       validateMediaId(item.id)
     }
-    return await this.http.request('/api/album-items', {
+    const result = await this.http.request('/api/album-items', {
       method: 'PATCH',
       body: { items },
       signal: options.signal,
     })
+    return { ...result, items: result.items.map(normalizeMediaItem) }
   }
 
   async delete(id: string, options: RequestOptions = {}): Promise {
diff --git a/src/types.ts b/src/types.ts
index 46cc47f..8457141 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -15,6 +15,9 @@ export interface WhoAmI {
 
 export interface MediaItem {
   id: string
+  /** Media URL for images, videos, and other files. */
+  url: string
+  /** @deprecated Use url for media access. Retained for compatibility. */
   imgUrl: string
   fileName?: string
   type?: string
@@ -26,7 +29,6 @@ export interface MediaItem {
   createdAt?: number
   updatedAt?: number
   originImgUrl?: string
-  url?: string
   extra?: Record
 }
 
diff --git a/src/upload/task.ts b/src/upload/task.ts
index c185185..aeb805e 100644
--- a/src/upload/task.ts
+++ b/src/upload/task.ts
@@ -2,6 +2,8 @@ import { PicGoCloudError } from '../errors.js'
 import { ApiErrorCode } from '../api-error-codes.js'
 import type { HttpClient } from '../http.js'
 import type { MediaItem } from '../types.js'
+import { normalizeMediaItem } from '../media-item.js'
+import type { MediaItemResponse } from '../media-item.js'
 import { digest, fingerprint, ResumeStore, throwIfAborted, withUploadLock } from './storage.js'
 import { putBlob } from './transport.js'
 import type { MultipartSession, PresignResult, SignedPart, StoredUpload, UploadConfig, UploadOptions, UploadPhase, UploadStatus } from './types.js'
@@ -215,9 +217,9 @@ export class UploadTask {
   private async finalize(http: HttpClient, signal: AbortSignal): Promise {
     const state = this.state!
     this.emit('completing', this.file.size)
-    let result: { item: MediaItem }
+    let result: { item: MediaItemResponse }
     try {
-      result = await retry(() => http.request<{ item: MediaItem }>('/api/album-items/complete', {
+      result = await retry(() => http.request<{ item: MediaItemResponse }>('/api/album-items/complete', {
         method: 'POST', signal,
         body: { objectKey: state.objectKey, publicId: state.publicId, filename: this.filename, width: this.options.width, height: this.options.height },
       }), signal)
@@ -225,12 +227,13 @@ export class UploadTask {
       if (state.phase === 'uploaded' && error instanceof PicGoCloudError && error.status === 404) this.clear()
       throw error
     }
-    if (!result?.item || typeof result.item.id !== 'string' || typeof result.item.imgUrl !== 'string') {
+    if (!result?.item) {
       protocol('Invalid media completion response')
     }
+    const item = normalizeMediaItem(result.item)
     this.clear()
     this.emit('completed', this.file.size)
-    return result.item
+    return item
   }
 
   private async single(http: HttpClient, signal: AbortSignal): Promise {
diff --git a/tests/browser/smoke.js b/tests/browser/smoke.js
index 1b58d83..e5b76c8 100644
--- a/tests/browser/smoke.js
+++ b/tests/browser/smoke.js
@@ -12,6 +12,7 @@ async function run() {
     onProgress: event => progress.push(event),
   })
   assert(small.id === '1', 'small upload result')
+  assert(small.url === 'https://media.example/1.png', 'upload provides a media URL')
   const largeFile = new File([new Uint8Array(10 * 1024 * 1024)], 'large.png', { type: 'image/png' })
   let failedRegistration = false
   try { await client.upload(largeFile) }
@@ -22,7 +23,9 @@ async function run() {
   const large = await recreated.upload(largeFile)
   assert(large.id === '2', 'recreated client should register the existing object')
   assert(localStorage.length === 0, 'successful registration clears the session')
-  assert((await client.media.list()).total === 2, 'list result')
+  const page = await client.media.list()
+  assert(page.total === 2, 'list result')
+  assert(page.items.every(item => typeof item.url === 'string'), 'every listed item provides a URL')
   assert((await client.media.update(small.id, { fileName: 'renamed.png' })).fileName === 'renamed.png', 'update result')
   await client.media.delete(small.id)
   assert((await client.media.list()).total === 1, 'delete result')
diff --git a/tests/client.test.ts b/tests/client.test.ts
index 274f7ad..f30b58f 100644
--- a/tests/client.test.ts
+++ b/tests/client.test.ts
@@ -223,7 +223,7 @@ describe('HttpClient', () => {
 
 describe('MediaService', () => {
   it('maps media operations to album-item endpoints and request bodies', async () => {
-    const mediaItem = { id: 'media-id', imgUrl: 'https://img.test/a.png' }
+    const mediaItem = { id: 'media-id', url: 'https://img.test/a.png', imgUrl: 'https://img.test/a.png' }
     const fetchMock = vi.fn()
     fetchMock
       .mockResolvedValueOnce(jsonResponse({
diff --git a/tests/media-item.test.ts b/tests/media-item.test.ts
new file mode 100644
index 0000000..bd84384
--- /dev/null
+++ b/tests/media-item.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, it, vi } from 'vitest'
+import { PicGoCloudClient } from '../src/client.js'
+import { normalizeMediaItem } from '../src/media-item.js'
+
+describe('MediaItem URLs', () => {
+  it('supports video URLs and supplies the compatibility field', () => {
+    expect(normalizeMediaItem({ id: 'video', url: 'https://media.example/video.mp4', contentType: 'video/mp4' }))
+      .toEqual({ id: 'video', url: 'https://media.example/video.mp4', imgUrl: 'https://media.example/video.mp4', contentType: 'video/mp4' })
+  })
+
+  it('uses a legacy address when url is absent and preserves current cloud domains', () => {
+    expect(normalizeMediaItem({ id: 'legacy', imgUrl: 'https://media.example/file.pdf' }).url).toBe('https://media.example/file.pdf')
+    expect(normalizeMediaItem({ id: 'cloud', type: 'picgo-cloud', url: 'https://old.example/a.mp4', imgUrl: 'https://current.example/a.mp4' }).url)
+      .toBe('https://current.example/a.mp4')
+    expect(normalizeMediaItem({ id: 'external', url: 'https://media.example/a.mp4', imgUrl: 'https://media.example/preview.png' }).url)
+      .toBe('https://media.example/a.mp4')
+  })
+
+  it('rejects missing or blank URLs instead of returning an unusable media item', () => {
+    expect(() => normalizeMediaItem({ id: 'missing' })).toThrow('Media item response has no URL')
+    expect(() => normalizeMediaItem({ id: 'blank', url: ' ', imgUrl: '' })).toThrow('Media item response has no URL')
+  })
+
+  it('normalizes list, detail, update, and batch update results consistently', async () => {
+    const legacy = { id: 'legacy', imgUrl: 'https://media.example/legacy.png' }
+    const video = { id: 'video', url: 'https://media.example/video.mp4', contentType: 'video/mp4' }
+    const respond = (data: unknown) => new Response(JSON.stringify({ success: true, data }))
+    const fetchMock = vi.fn()
+      .mockResolvedValueOnce(respond({ items: [legacy, video], total: 2, limit: 20, offset: 0 }))
+      .mockResolvedValueOnce(respond({ item: legacy }))
+      .mockResolvedValueOnce(respond({ item: video }))
+      .mockResolvedValueOnce(respond({ items: [legacy, video], updated: 2, skipped: 0 }))
+    const client = new PicGoCloudClient({ token: 'test-token', fetch: fetchMock })
+    const urls = ['https://media.example/legacy.png', 'https://media.example/video.mp4']
+    expect((await client.media.list()).items.map(item => item.url)).toEqual(urls)
+    expect((await client.media.get('legacy')).url).toBe(urls[0])
+    expect((await client.media.update('video', { fileName: 'renamed.mp4' })).url).toBe(urls[1])
+    const batch = await client.media.updateMany([{ id: 'legacy' }, { id: 'video' }])
+    expect(batch.items.map(item => item.url)).toEqual(urls)
+    expect(batch.updated).toBe(2)
+  })
+})
diff --git a/tests/upload.test.ts b/tests/upload.test.ts
index 861a8f6..3526015 100644
--- a/tests/upload.test.ts
+++ b/tests/upload.test.ts
@@ -13,7 +13,7 @@ vi.mock('../src/upload/transport.js', () => ({ putBlob: putBlobMock }))
 const MIB = 1024 * 1024
 const MULTIPART_THRESHOLD = 10 * MIB
 const API_BASE_URL = 'https://api.example.test'
-const MEDIA = { id: 'media-1', imgUrl: 'https://cdn.example.test/media-1.png' }
+const MEDIA = { id: 'media-1', url: 'https://cdn.example.test/media-1.png', imgUrl: 'https://cdn.example.test/media-1.png' }
 
 interface ApiCall {
   path: string
@@ -136,6 +136,30 @@ afterEach(() => {
 })
 
 describe('PicGoCloud uploads', () => {
+  it.each([
+    { url: 'https://cdn.example.test/video.mp4' },
+    { imgUrl: 'https://cdn.example.test/video.mp4' },
+  ])('returns a required media URL for video uploads with response %j', async address => {
+    putBlobMock.mockResolvedValue({})
+    const { fetchMock } = createFetch(call => {
+      switch (call.path) {
+        case '/api/whoami': return dataResponse({ userId: 'video-user' })
+        case '/api/upload/presign': return dataResponse({
+          objectKey: 'objects/video.mp4', publicId: 'video-id', method: 'PUT',
+          uploadUrl: 'https://uploads.example.test/video.mp4', headers: { 'Content-Type': 'video/mp4' },
+        })
+        case '/api/album-items/complete': return dataResponse({ item: { id: 'video-id', contentType: 'video/mp4', ...address } })
+        default: return unexpected(call)
+      }
+    })
+    const client = new PicGoCloudClient({ token: 'token', fetch: fetchMock, storage: false })
+    const file = new File([new Uint8Array(128)], 'video.mp4', { type: 'video/mp4' })
+    const result = await client.upload(file)
+    expect(result.url).toBe('https://cdn.example.test/video.mp4')
+    expect(result.contentType).toBe('video/mp4')
+    expect(uploadCalls()[0]?.headers['Content-Type']).toBe('video/mp4')
+  })
+
   it('switches at 10 MiB, preserves presign headers, and uses server part sizes and ETags', async () => {
     const largeFile = imageFile(MULTIPART_THRESHOLD, 'large.png')
     const smallFile = imageFile(1024, 'small.png')

From 74d1c2c9a18744b6f4043f69fa7c1aefea44b7b6 Mon Sep 17 00:00:00 2001
From: PiEgg 
Date: Wed, 23 Sep 2026 19:34:29 +0800
Subject: [PATCH 6/8] ci: publish npm releases with trusted publishing

---
 .github/RELEASING.md          | 63 +++++++++++++++++++++++++++++++
 .github/workflows/publish.yml | 71 +++++++++++++++++++++++++++++++++++
 .gitignore                    |  1 +
 package.json                  |  3 +-
 scripts/release.mjs           | 35 +++++++++++++++++
 scripts/release.test.mjs      | 31 +++++++++++++++
 6 files changed, 203 insertions(+), 1 deletion(-)
 create mode 100644 .github/RELEASING.md
 create mode 100644 .github/workflows/publish.yml
 create mode 100644 scripts/release.mjs
 create mode 100644 scripts/release.test.mjs

diff --git a/.github/RELEASING.md b/.github/RELEASING.md
new file mode 100644
index 0000000..5bf74fb
--- /dev/null
+++ b/.github/RELEASING.md
@@ -0,0 +1,63 @@
+# Publishing @picgo/cloud-sdk
+
+This document is for package maintainers. Application integration is documented in the root README.
+
+## First publication
+
+The npm package must exist before configuring its trusted publisher. If `@picgo/cloud-sdk` has not been published yet, a maintainer with publish access to the npm `@picgo` organization can publish the first version locally. A manually created access token is not required: interactive `npm login` and account 2FA work. If the first publication must run non-interactively, use an appropriately scoped granular access token with publishing access and bypass 2FA enabled, then remove that credential after setting up OIDC.
+
+Make the GitHub repository public and merge the SDK and publishing workflow into `main` before releasing. From a clean checkout of `main`, using Node.js 24:
+
+```sh
+pnpm install --frozen-lockfile
+npm login --registry=https://registry.npmjs.org
+npm publish --access public
+```
+
+The existing `prepublishOnly` script runs the full checks and build before this manual publication. Complete npm's 2FA prompt. This creates the package; it does not need a GitHub secret or a bootstrap workflow. Do not push a release tag for this already-published version to trigger another publication of the same version. Local publication does not carry the GitHub Actions provenance that subsequent OIDC releases will provide.
+
+## Configure the npm trusted publisher
+
+Open the npm package settings for `@picgo/cloud-sdk`, add a GitHub Actions trusted publisher, and use these exact values:
+
+| Field | Value |
+| --- | --- |
+| Organization or user | `PicGo` |
+| Repository | `picgo-cloud-sdk` |
+| Workflow filename | `publish.yml` |
+| Environment name | Leave blank; this workflow does not specify one |
+| Allowed actions | Enable direct publishing with `npm publish` |
+
+The workflow filename must include the extension but not `.github/workflows/`. Current npm configurations default to allowing staged publishing; explicitly enable direct `npm publish` for this workflow. No `NPM_TOKEN` or `NODE_AUTH_TOKEN` secret is needed. The publish job requests `id-token: write` and runs on a GitHub-hosted runner. It installs npm 11, which supports OIDC (minimum supported version: 11.5.1).
+
+After verifying the first OIDC publication, remove unused publishing tokens. You may also enable npm's “Require two-factor authentication and disallow tokens” setting; trusted publishing continues to work with that setting.
+
+## Subsequent releases
+
+Update `package.json` to a new version, merge the version change into `main`, and tag that exact commit. For example, after merging version `0.1.1`:
+
+```sh
+git switch main
+git pull --ff-only
+git tag v0.1.1
+git push origin v0.1.1
+```
+
+Pushing a `v*` tag starts `.github/workflows/publish.yml`. It checks that the repository is public, the tag exactly matches `v`, and the tagged commit is already in `main`. It then installs locked dependencies, runs type checking, lint, tests, and the build, and packs the result. A separate job publishes that verified tarball using OIDC and provenance, without rerunning package scripts in the publishing job.
+
+Stable versions publish to the `latest` dist-tag. Prerelease versions such as `0.2.0-beta.1` publish to `next`. Ordinary pushes and pull requests run CI but do not publish. Existing npm versions cannot be overwritten; increment the version for another release. To retry a transient failure before the package was published, rerun the failed workflow in GitHub Actions.
+
+## Local validation without publishing
+
+```sh
+pnpm check
+pnpm pack --pack-destination .release
+npm publish .release/picgo-cloud-sdk-0.1.0.tgz --dry-run --ignore-scripts --access public
+```
+
+Replace the tarball version with the version being tested. These commands build and inspect the package without publishing it. The package allowlist contains only `dist`, the two READMEs, and the license, plus npm's required package metadata; local `.env` files, examples, and maintenance scripts are not shipped.
+
+## References
+
+- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers)
+- [Publishing scoped public packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..c09432a
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,71 @@
+name: Publish to npm
+
+on:
+  push:
+    tags:
+      - 'v*'
+
+permissions:
+  contents: read
+
+concurrency:
+  group: npm-publish
+  cancel-in-progress: false
+
+jobs:
+  verify:
+    name: Verify and pack
+    runs-on: ubuntu-latest
+    outputs:
+      dist-tag: ${{ steps.release.outputs.dist-tag }}
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+          package-manager-cache: false
+      - uses: pnpm/action-setup@v4
+      - name: Validate release tag and source
+        id: release
+        run: node scripts/release.mjs
+        env:
+          RELEASE_TAG: ${{ github.ref_name }}
+          RELEASE_REPOSITORY: ${{ github.repository }}
+          RELEASE_REPOSITORY_PRIVATE: ${{ github.event.repository.private }}
+      - run: pnpm install --frozen-lockfile
+      - run: pnpm check
+      - name: Pack the verified build
+        run: pnpm pack --pack-destination .release
+      - uses: actions/upload-artifact@v4
+        with:
+          name: npm-package
+          path: .release/*.tgz
+          if-no-files-found: error
+          retention-days: 7
+
+  publish:
+    name: Publish with npm trusted publishing
+    needs: verify
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      id-token: write
+    steps:
+      - uses: actions/checkout@v6
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+          registry-url: https://registry.npmjs.org
+          package-manager-cache: false
+      - name: Install npm with OIDC support
+        run: npm install --global npm@11
+      - uses: actions/download-artifact@v4
+        with:
+          name: npm-package
+          path: .release
+      - name: Publish the verified tarball
+        run: npm publish .release/*.tgz --access public --provenance --ignore-scripts --tag "$DIST_TAG"
+        env:
+          DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
diff --git a/.gitignore b/.gitignore
index 7b6c976..beceba9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 node_modules/
 dist/
 coverage/
+.release/
 *.tgz
 .DS_Store
 .env*
diff --git a/package.json b/package.json
index 93c2530..2325c74 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
   "type": "module",
   "license": "MIT",
   "repository": { "type": "git", "url": "git+https://github.com/PicGo/picgo-cloud-sdk.git" },
+  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
   "sideEffects": false,
   "files": ["dist", "README.md", "README_ZH.md", "LICENSE"],
   "main": "./dist/index.js",
@@ -16,7 +17,7 @@
     "build": "rolldown -c && tsc -p tsconfig.build.json",
     "typecheck": "tsc --noEmit",
     "lint": "eslint .",
-    "test": "vitest run && node --test scripts/dev-server.test.mjs",
+    "test": "vitest run && node --test scripts/*.test.mjs",
     "test:browser:serve": "node scripts/browser-smoke.mjs",
     "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build",
     "prepublishOnly": "pnpm check"
diff --git a/scripts/release.mjs b/scripts/release.mjs
new file mode 100644
index 0000000..db67708
--- /dev/null
+++ b/scripts/release.mjs
@@ -0,0 +1,35 @@
+import { execFileSync } from 'node:child_process'
+import { appendFileSync, readFileSync } from 'node:fs'
+import { pathToFileURL } from 'node:url'
+
+const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?$/
+
+export function validateRelease({ manifest, tag, repository, isPrivate }) {
+  if (repository !== 'PicGo/picgo-cloud-sdk') throw new Error('Releases must originate from PicGo/picgo-cloud-sdk')
+  if (isPrivate !== 'false') throw new Error('Make the GitHub repository public before publishing with provenance')
+  if (manifest.name !== '@picgo/cloud-sdk') throw new Error('Unexpected npm package name')
+  if (manifest.repository?.url !== 'git+https://github.com/PicGo/picgo-cloud-sdk.git') {
+    throw new Error('package.json repository.url must match the trusted publisher repository')
+  }
+  if (manifest.private) throw new Error('Cannot publish a private package')
+  if (typeof manifest.version !== 'string' || !SEMVER.test(manifest.version)) {
+    throw new Error('Use a valid package version without build metadata')
+  }
+  if (tag !== `v${manifest.version}`) throw new Error('The release tag must exactly match v')
+  return { version: manifest.version, distTag: manifest.version.includes('-') ? 'next' : 'latest' }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+  const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
+  const result = validateRelease({
+    manifest,
+    tag: process.env.RELEASE_TAG,
+    repository: process.env.RELEASE_REPOSITORY,
+    isPrivate: process.env.RELEASE_REPOSITORY_PRIVATE,
+  })
+  // Checkout fetches full history. A tag must point to a commit already merged into main.
+  execFileSync('git', ['merge-base', '--is-ancestor', 'HEAD', 'origin/main'], { stdio: 'inherit' })
+  if (!process.env.GITHUB_OUTPUT) throw new Error('GITHUB_OUTPUT is required')
+  appendFileSync(process.env.GITHUB_OUTPUT, `version=${result.version}\ndist-tag=${result.distTag}\n`)
+  console.log(`Validated @picgo/cloud-sdk@${result.version} → ${result.distTag}`)
+}
diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs
new file mode 100644
index 0000000..4e787cd
--- /dev/null
+++ b/scripts/release.test.mjs
@@ -0,0 +1,31 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import { validateRelease } from './release.mjs'
+
+const release = {
+  manifest: { name: '@picgo/cloud-sdk', version: '0.1.0', repository: { url: 'git+https://github.com/PicGo/picgo-cloud-sdk.git' } },
+  tag: 'v0.1.0', repository: 'PicGo/picgo-cloud-sdk', isPrivate: 'false',
+}
+
+test('stable releases use latest and prereleases use next', () => {
+  assert.deepEqual(validateRelease(release), { version: '0.1.0', distTag: 'latest' })
+  assert.deepEqual(validateRelease({ ...release, tag: 'v0.2.0-beta.1', manifest: { ...release.manifest, version: '0.2.0-beta.1' } }), {
+    version: '0.2.0-beta.1', distTag: 'next',
+  })
+})
+
+test('release tag must match a valid package version exactly', () => {
+  for (const tag of ['0.1.0', 'v0.2.0', 'v0.1.0\n']) assert.throws(() => validateRelease({ ...release, tag }))
+  for (const version of ['01.0.0', '0.1.0-beta.01', '0.1.0+build', '0.1.0\n']) {
+    assert.throws(() => validateRelease({ ...release, tag: `v${version}`, manifest: { ...release.manifest, version } }))
+  }
+})
+
+test('only the public canonical repository can release this package', () => {
+  assert.throws(() => validateRelease({ ...release, repository: 'someone/picgo-cloud-sdk' }))
+  assert.throws(() => validateRelease({ ...release, isPrivate: 'true' }))
+  assert.throws(() => validateRelease({ ...release, isPrivate: undefined }))
+  assert.throws(() => validateRelease({ ...release, manifest: { ...release.manifest, private: true } }))
+  assert.throws(() => validateRelease({ ...release, manifest: { ...release.manifest, name: '@someone/cloud-sdk' } }))
+  assert.throws(() => validateRelease({ ...release, manifest: { ...release.manifest, repository: { url: 'https://github.com/someone/other' } } }))
+})

From 2389a04dceb505a3ec22be275eca950c647bb972 Mon Sep 17 00:00:00 2001
From: PiEgg 
Date: Wed, 23 Sep 2026 19:44:46 +0800
Subject: [PATCH 7/8] :package: Chore: configure PicGo release tooling

---
 .github/RELEASING.md |   27 +-
 .husky/commit-msg    |    1 +
 .husky/pre-commit    |    1 +
 package.json         |   21 +-
 pnpm-lock.yaml       | 2972 +++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 2969 insertions(+), 53 deletions(-)
 create mode 100644 .husky/commit-msg
 create mode 100644 .husky/pre-commit

diff --git a/.github/RELEASING.md b/.github/RELEASING.md
index 5bf74fb..72f1b50 100644
--- a/.github/RELEASING.md
+++ b/.github/RELEASING.md
@@ -34,15 +34,36 @@ After verifying the first OIDC publication, remove unused publishing tokens. You
 
 ## Subsequent releases
 
-Update `package.json` to a new version, merge the version change into `main`, and tag that exact commit. For example, after merging version `0.1.1`:
+This repository uses `@picgo/bump-version`, matching PicGo-Core. Use `pnpm cz` to write commits in the PicGo format; the commit-message hook validates them. For example:
+
+```text
+:sparkles: Feature(upload): add resumable uploads
+:bug: Fix(media): preserve the current media URL
+```
+
+The changelog generator recognizes this convention. Earlier Angular-style commits are not automatically converted. Use a PicGo-format title when squash-merging a feature PR so its change appears in the changelog.
+
+Preview the next release without changing files, creating a commit, or tagging:
+
+```sh
+pnpm release:dry
+pnpm release:dry --type minor
+```
+
+From a clean, up-to-date `main`, run `pnpm release` to update the version, generate `CHANGELOG.md`, create a release commit, and create the annotated `v` tag. For example, when the current version is `0.1.0`:
 
 ```sh
 git switch main
 git pull --ff-only
-git tag v0.1.1
+pnpm release --type patch
+git push origin main
 git push origin v0.1.1
 ```
 
+Use `pnpm release --version 0.2.0-beta.1` for an exact prerelease version, or add `--yes` to skip the version confirmation. Review the resulting version and changelog before pushing. The release command prepares the Git release; the tag push triggers npm publication. Do not use bump-version's `--push` here: version 3.0.0 hardcodes `origin master`, while this repository uses `main`.
+
+If changes to `main` require a pull request, run `pnpm release --no-tag` on a release branch, merge the version/changelog commit, and create the corresponding tag on the merged `main` commit. The tag must point to the commit that contains the published version.
+
 Pushing a `v*` tag starts `.github/workflows/publish.yml`. It checks that the repository is public, the tag exactly matches `v`, and the tagged commit is already in `main`. It then installs locked dependencies, runs type checking, lint, tests, and the build, and packs the result. A separate job publishes that verified tarball using OIDC and provenance, without rerunning package scripts in the publishing job.
 
 Stable versions publish to the `latest` dist-tag. Prerelease versions such as `0.2.0-beta.1` publish to `next`. Ordinary pushes and pull requests run CI but do not publish. Existing npm versions cannot be overwritten; increment the version for another release. To retry a transient failure before the package was published, rerun the failed workflow in GitHub Actions.
@@ -55,7 +76,7 @@ pnpm pack --pack-destination .release
 npm publish .release/picgo-cloud-sdk-0.1.0.tgz --dry-run --ignore-scripts --access public
 ```
 
-Replace the tarball version with the version being tested. These commands build and inspect the package without publishing it. The package allowlist contains only `dist`, the two READMEs, and the license, plus npm's required package metadata; local `.env` files, examples, and maintenance scripts are not shipped.
+Replace the tarball version with the version being tested. These commands build and inspect the package without publishing it. The package allowlist contains only `dist`, the two READMEs, `CHANGELOG.md` once generated, and the license, plus npm's required package metadata; local `.env` files, examples, and maintenance scripts are not shipped.
 
 ## References
 
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 0000000..2e6b87e
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+pnpm exec commitlint --edit "$1"
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..009b3f8
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+pnpm lint
diff --git a/package.json b/package.json
index 2325c74..280f7ad 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
   "repository": { "type": "git", "url": "git+https://github.com/PicGo/picgo-cloud-sdk.git" },
   "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
   "sideEffects": false,
-  "files": ["dist", "README.md", "README_ZH.md", "LICENSE"],
+  "files": ["dist", "README.md", "README_ZH.md", "CHANGELOG.md", "LICENSE"],
   "main": "./dist/index.js",
   "module": "./dist/index.js",
   "types": "./dist/index.d.ts",
@@ -20,13 +20,30 @@
     "test": "vitest run && node --test scripts/*.test.mjs",
     "test:browser:serve": "node scripts/browser-smoke.mjs",
     "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build",
-    "prepublishOnly": "pnpm check"
+    "prepublishOnly": "pnpm check",
+    "cz": "git-cz",
+    "release": "bump-version",
+    "release:dry": "bump-version --dry --yes",
+    "commitlint": "commitlint --edit",
+    "prepare": "husky"
+  },
+  "config": {
+    "commitizen": { "path": "./node_modules/cz-customizable" },
+    "cz-customizable": { "config": "./node_modules/@picgo/bump-version/.cz-config.cjs" }
+  },
+  "commitlint": {
+    "extends": ["./node_modules/@picgo/bump-version/commitlint-picgo/index.cjs"]
   },
   "packageManager": "pnpm@11.7.0",
   "devDependencies": {
+    "@commitlint/cli": "^21.2.3",
     "@eslint/js": "^9.39.1",
+    "@picgo/bump-version": "^3.0.0",
+    "commitizen": "^4.3.2",
+    "cz-customizable": "^7.5.4",
     "eslint": "^9.39.1",
     "globals": "^16.5.0",
+    "husky": "^9.1.7",
     "rolldown": "^1.2.9",
     "typescript": "^5.9.3",
     "typescript-eslint": "^8.49.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9a53585..05b0c9f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,15 +8,30 @@ importers:
 
   .:
     devDependencies:
+      '@commitlint/cli':
+        specifier: ^21.2.3
+        version: 21.2.3(@types/node@26.6.2)(conventional-commits-parser@7.1.2)(typescript@5.9.3)
       '@eslint/js':
         specifier: ^9.39.1
         version: 9.39.5
+      '@picgo/bump-version':
+        specifier: ^3.0.0
+        version: 3.0.0(@types/node@26.6.2)
+      commitizen:
+        specifier: ^4.3.2
+        version: 4.3.2(@types/node@26.6.2)(typescript@5.9.3)
+      cz-customizable:
+        specifier: ^7.5.4
+        version: 7.5.4
       eslint:
         specifier: ^9.39.1
-        version: 9.39.5
+        version: 9.39.5(jiti@2.6.1)
       globals:
         specifier: ^16.5.0
         version: 16.5.0
+      husky:
+        specifier: ^9.1.7
+        version: 9.1.7
       rolldown:
         specifier: ^1.2.9
         version: 1.2.9
@@ -25,13 +40,106 @@ importers:
         version: 5.9.3
       typescript-eslint:
         specifier: ^8.49.0
-        version: 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+        version: 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
       vitest:
         specifier: ~4.1.10
-        version: 4.1.11(vite@8.3.0)
+        version: 4.1.11(@types/node@26.6.2)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1))
 
 packages:
 
+  '@babel/code-frame@7.29.7':
+    resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-identifier@7.29.7':
+    resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+    engines: {node: '>=6.9.0'}
+
+  '@commitlint/cli@21.2.3':
+    resolution: {integrity: sha512-5yOX6IRjcrIReCxpyAd04hgI/yhUMpVBJ/ouwFV0wk9yx5+fYRDWZWJ5h55Tbedgfol+5m2mU5nqzbB8g3v0vQ==}
+    engines: {node: '>=22.12.0'}
+    hasBin: true
+
+  '@commitlint/config-conventional@21.2.3':
+    resolution: {integrity: sha512-EP2n5DN2GwRWNVrLX0Jx3mpQlW5PfFi9HnD9OJ8hN/H0xFqpCFyHeGDhg+0P8/21iMMcdkcQgbNqmVklebqYsQ==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/config-validator@21.2.3':
+    resolution: {integrity: sha512-JQ13v0GafSM4fBYX8UowSy++07Was20auq0NcTdnzSEq6/05Cq5GBUMIfKkqjzyBwC9sJFmPpKWgLLhH7cygkw==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/ensure@21.2.3':
+    resolution: {integrity: sha512-gEjNKgRocbODI5/sMx2Qc9XOf2/zFTNV2aEK1QZ4gKDKxDC/VR7us3QtIX1C5d5xxHxx0I/tEbYbdSP1Am2LwA==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/execute-rule@21.0.1':
+    resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/format@21.2.3':
+    resolution: {integrity: sha512-KKwCjXYqA8r295uhLnulDXL9yekdXmj2yp2Q0XsBPhxQ2iPuvRE8F43j7g7yexo69wTdmUQmFz15jpderluzUQ==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/is-ignored@21.2.3':
+    resolution: {integrity: sha512-Ng4nRj/LBAOvc+GQu7bsFW4/fL89LvZ14OOC/ERLbUpnG6QBwWRzY/1oXGwPs3C8YHWjJObin4kLYo2YeEn70w==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/lint@21.2.3':
+    resolution: {integrity: sha512-lUZcSuVLLEDkhge9ERk8Rp+FXQtrUhCK+h328BFnzKXrTuXGdqiYMEpTlNWdZaOrjyMhMJaz354ZaWEGJxcLeg==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/load@21.2.3':
+    resolution: {integrity: sha512-cDrL8lOo23Lk7knGzQdjbFcq7z2JgvjwV6PhbESVvEc+Fe21MeLjPKBJi8nKjg7j+jh79CVbVSUbZc104thXRw==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/message@21.2.0':
+    resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/parse@21.2.3':
+    resolution: {integrity: sha512-Jt61QpF1xA88damCELe8I0FlDZPSpXNc8IhN7z5kZ0EcDYPjdZ+ZHPvsSXl69r2MIlLnNKeHhOn2cmVpziFE9w==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/read@21.2.3':
+    resolution: {integrity: sha512-szAepuLA1vTTr7fuudEg7jYyVHudf+FH+vptU91bhxE1WnE8TKrfTAi44HVllz8XpI8Pvp4GjR/iP5Nshgh+og==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/resolve-extends@21.2.3':
+    resolution: {integrity: sha512-bqJdmAtXgAQel+l02uIsF7rHgKgx1AGZdGBjam7uK4XW54ZAQtB6XAIUXezYprQiCcRMpBh4fHs5JodXK4mVZA==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/rules@21.2.3':
+    resolution: {integrity: sha512-5+YnI/LuTx3JDl9GjqfP2z9x96M8KKDrd+4jOel/T0L1BB3oAmBlSN8xppf63QsudiVEgR8C0gRoAMrPVrms6g==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/to-lines@21.0.1':
+    resolution: {integrity: sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/top-level@21.2.0':
+    resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==}
+    engines: {node: '>=22.12.0'}
+
+  '@commitlint/types@21.2.3':
+    resolution: {integrity: sha512-sGEA473TlCvhCGNoTBY9OXZVWdscIXdGt0dm8EpB+2MRfmt8LNh/X56P/T3nCtEB+eH5qGEa3odphSAoUxSfSQ==}
+    engines: {node: '>=22.12.0'}
+
+  '@conventional-changelog/git-client@3.1.2':
+    resolution: {integrity: sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==}
+    engines: {node: '>=22'}
+    peerDependencies:
+      conventional-commits-filter: ^6.0.1
+      conventional-commits-parser: ^7.1.2
+    peerDependenciesMeta:
+      conventional-commits-filter:
+        optional: true
+      conventional-commits-parser:
+        optional: true
+
+  '@conventional-changelog/template@1.4.0':
+    resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==}
+    engines: {node: '>=22'}
+
   '@eslint-community/eslint-utils@4.10.1':
     resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -90,12 +198,164 @@ packages:
     resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
     engines: {node: '>=18.18'}
 
+  '@hutson/parse-repository-url@3.0.2':
+    resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==}
+    engines: {node: '>=6.9.0'}
+
+  '@inquirer/ansi@2.0.8':
+    resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+
+  '@inquirer/checkbox@5.2.5':
+    resolution: {integrity: sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/confirm@6.3.2':
+    resolution: {integrity: sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/core@12.0.3':
+    resolution: {integrity: sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/editor@5.3.3':
+    resolution: {integrity: sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/expand@5.1.5':
+    resolution: {integrity: sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/external-editor@1.0.3':
+    resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/external-editor@3.0.5':
+    resolution: {integrity: sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/figures@2.0.9':
+    resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+
+  '@inquirer/input@5.1.6':
+    resolution: {integrity: sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/number@4.2.3':
+    resolution: {integrity: sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/password@5.2.2':
+    resolution: {integrity: sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/prompts@8.7.2':
+    resolution: {integrity: sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/rawlist@5.3.5':
+    resolution: {integrity: sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/search@4.3.3':
+    resolution: {integrity: sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/select@5.2.5':
+    resolution: {integrity: sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/type@4.1.1':
+    resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
   '@jridgewell/sourcemap-codec@1.6.0':
     resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
 
   '@oxc-project/types@0.150.0':
     resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
 
+  '@picgo/bump-version@3.0.0':
+    resolution: {integrity: sha512-1KxMxxizfoOvSEy6a04XkybQ2FvUrwJGZ8Fo8J+y23ggukbyX+FRG06A3pcOmGBx6CeVJupFeloZjKsvFHXHVQ==}
+    engines: {node: '>=22.0.0'}
+    hasBin: true
+
   '@rolldown/binding-android-arm-eabi@1.2.9':
     resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==}
     engines: {node: ^20.19.0 || >=22.12.0}
@@ -195,6 +455,14 @@ packages:
   '@rolldown/pluginutils@1.0.1':
     resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
 
+  '@simple-libs/child-process-utils@2.0.0':
+    resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==}
+    engines: {node: '>=22'}
+
+  '@simple-libs/stream-utils@2.0.0':
+    resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==}
+    engines: {node: '>=22'}
+
   '@standard-schema/spec@1.1.0':
     resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
 
@@ -210,6 +478,15 @@ packages:
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
+  '@types/minimist@1.2.5':
+    resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
+
+  '@types/node@26.6.2':
+    resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==}
+
+  '@types/normalize-package-data@2.4.4':
+    resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
+
   '@typescript-eslint/eslint-plugin@8.70.1':
     resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -298,6 +575,10 @@ packages:
   '@vitest/utils@4.1.11':
     resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
 
+  JSONStream@1.3.5:
+    resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
+    hasBin: true
+
   acorn-jsx@5.3.2:
     resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
     peerDependencies:
@@ -308,20 +589,73 @@ packages:
     engines: {node: '>=0.4.0'}
     hasBin: true
 
+  add-stream@1.0.0:
+    resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
+
   ajv@6.15.0:
     resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
 
+  ajv@8.20.0:
+    resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
+  ansi-escapes@3.2.0:
+    resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
+    engines: {node: '>=4'}
+
+  ansi-escapes@4.3.2:
+    resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
+    engines: {node: '>=8'}
+
+  ansi-regex@3.0.1:
+    resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
+    engines: {node: '>=4'}
+
+  ansi-regex@4.1.1:
+    resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
+    engines: {node: '>=6'}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+    engines: {node: '>=8'}
+
+  ansi-regex@6.3.0:
+    resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
+    engines: {node: '>=12'}
+
+  ansi-styles@3.2.1:
+    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+    engines: {node: '>=4'}
+
   ansi-styles@4.3.0:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
 
+  ansi-styles@6.2.3:
+    resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+    engines: {node: '>=12'}
+
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  argue-cli@3.2.0:
+    resolution: {integrity: sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==}
+    engines: {node: '>=22'}
+
+  array-ify@1.0.0:
+    resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
+
+  arrify@1.0.1:
+    resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
+    engines: {node: '>=0.10.0'}
+
   assertion-error@2.0.1:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
     engines: {node: '>=12'}
 
+  at-least-node@1.0.0:
+    resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
+    engines: {node: '>= 4.0.0'}
+
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
@@ -329,6 +663,15 @@ packages:
     resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
     engines: {node: 18 || 20 || >=22}
 
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  bl@4.1.0:
+    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
+  bl@5.1.0:
+    resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
+
   brace-expansion@1.1.21:
     resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
 
@@ -336,35 +679,248 @@ packages:
     resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
     engines: {node: 20 || >=22}
 
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  buffer@5.7.1:
+    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+  cachedir@2.4.0:
+    resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==}
+    engines: {node: '>=6'}
+
   callsites@3.1.0:
     resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
     engines: {node: '>=6'}
 
+  camelcase-keys@6.2.2:
+    resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
+    engines: {node: '>=8'}
+
+  camelcase@5.3.1:
+    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+    engines: {node: '>=6'}
+
   chai@6.2.2:
     resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
     engines: {node: '>=18'}
 
+  chalk@2.4.2:
+    resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+    engines: {node: '>=4'}
+
   chalk@4.1.2:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
+  chalk@5.6.2:
+    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+  chalk@6.0.0:
+    resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==}
+    engines: {node: '>=22'}
+
+  chardet@0.7.0:
+    resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
+
+  chardet@2.2.0:
+    resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==}
+
+  cli-cursor@2.1.0:
+    resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
+    engines: {node: '>=4'}
+
+  cli-cursor@3.1.0:
+    resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
+    engines: {node: '>=8'}
+
+  cli-cursor@4.0.0:
+    resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  cli-spinners@2.9.2:
+    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+    engines: {node: '>=6'}
+
+  cli-width@2.2.1:
+    resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==}
+
+  cli-width@3.0.0:
+    resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
+    engines: {node: '>= 10'}
+
+  cli-width@4.1.0:
+    resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+    engines: {node: '>= 12'}
+
+  cliui@7.0.4:
+    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
+  cliui@9.0.1:
+    resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+    engines: {node: '>=20'}
+
+  clone@1.0.4:
+    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
+    engines: {node: '>=0.8'}
+
+  color-convert@1.9.3:
+    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+
   color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
 
+  color-name@1.1.3:
+    resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+
   color-name@1.1.4:
     resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
 
+  commitizen@4.3.2:
+    resolution: {integrity: sha512-1Zs37z9JPvAcuTSSricZZwBhOPVNNxJouuY4yDEt+eD70EoxT2TU9kViG8CuB/PmVg2G4XsAGQiK4YCst97aDQ==}
+    engines: {node: '>= 18'}
+    hasBin: true
+
+  compare-func@2.0.0:
+    resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
+
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
+  conventional-changelog-angular@5.0.13:
+    resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==}
+    engines: {node: '>=10'}
+
+  conventional-changelog-angular@9.4.0:
+    resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==}
+    engines: {node: '>=22'}
+
+  conventional-changelog-atom@2.0.8:
+    resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-codemirror@2.0.8:
+    resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-conventionalcommits@10.4.0:
+    resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==}
+    engines: {node: '>=22'}
+
+  conventional-changelog-conventionalcommits@4.6.3:
+    resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==}
+    engines: {node: '>=10'}
+
+  conventional-changelog-core@4.2.4:
+    resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==}
+    engines: {node: '>=10'}
+    deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
+
+  conventional-changelog-ember@2.0.9:
+    resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-eslint@3.0.9:
+    resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-express@2.0.6:
+    resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-jquery@3.0.11:
+    resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-jshint@2.0.9:
+    resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==}
+    engines: {node: '>=10'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
+
+  conventional-changelog-preset-loader@2.3.4:
+    resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==}
+    engines: {node: '>=10'}
+
+  conventional-changelog-writer@5.0.1:
+    resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  conventional-changelog@3.1.25:
+    resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==}
+    engines: {node: '>=10'}
+
+  conventional-commit-types@3.0.0:
+    resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==}
+
+  conventional-commits-filter@2.0.7:
+    resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==}
+    engines: {node: '>=10'}
+
+  conventional-commits-parser@3.2.4:
+    resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  conventional-commits-parser@7.1.2:
+    resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==}
+    engines: {node: '>=22'}
+    hasBin: true
+
   convert-source-map@2.0.0:
     resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 
+  core-util-is@1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
+  cosmiconfig-typescript-loader@6.3.0:
+    resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==}
+    engines: {node: '>=v18'}
+    peerDependencies:
+      '@types/node': '*'
+      cosmiconfig: '>=9'
+      typescript: '>=5'
+
+  cosmiconfig@9.0.2:
+    resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      typescript: '>=4.9.5'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   cross-spawn@7.0.6:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
 
+  cz-conventional-changelog@3.3.0:
+    resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==}
+    engines: {node: '>= 10'}
+
+  cz-customizable@7.5.4:
+    resolution: {integrity: sha512-vkNc7kLrWYBkr8VQKSZdDfy8athDZ2huIAWJwVxCrymBXRhB7q8hamkKd6JwAoLPwReS8fKi/kAjRpmy7VvL2w==}
+    hasBin: true
+
+  dargs@7.0.0:
+    resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
+    engines: {node: '>=8'}
+
+  dateformat@3.0.3:
+    resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
+
   debug@4.4.3:
     resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
     engines: {node: '>=6.0'}
@@ -374,16 +930,73 @@ packages:
       supports-color:
         optional: true
 
+  decamelize-keys@1.1.1:
+    resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
+    engines: {node: '>=0.10.0'}
+
+  decamelize@1.2.0:
+    resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+    engines: {node: '>=0.10.0'}
+
+  dedent@0.7.0:
+    resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
+
   deep-is@0.1.4:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
 
+  defaults@1.0.4:
+    resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
+
+  detect-file@1.0.0:
+    resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
+    engines: {node: '>=0.10.0'}
+
+  detect-indent@6.1.0:
+    resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+    engines: {node: '>=8'}
+
   detect-libc@2.1.2:
     resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
     engines: {node: '>=8'}
 
+  dot-prop@5.3.0:
+    resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
+    engines: {node: '>=8'}
+
+  editor@1.0.0:
+    resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==}
+
+  emoji-regex@10.6.0:
+    resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+  emoji-regex@8.0.0:
+    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+  env-paths@2.2.1:
+    resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+    engines: {node: '>=6'}
+
+  error-ex@1.3.4:
+    resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
   es-module-lexer@2.3.2:
     resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
 
+  es-toolkit@1.52.0:
+    resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==}
+
+  escalade@3.2.0:
+    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+    engines: {node: '>=6'}
+
+  escape-string-regexp@1.0.5:
+    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+    engines: {node: '>=0.8.0'}
+
   escape-string-regexp@4.0.0:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
     engines: {node: '>=10'}
@@ -438,10 +1051,18 @@ packages:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
     engines: {node: '>=0.10.0'}
 
+  expand-tilde@2.0.2:
+    resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
+    engines: {node: '>=0.10.0'}
+
   expect-type@1.4.0:
     resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
     engines: {node: '>=12.0.0'}
 
+  external-editor@3.1.0:
+    resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
+    engines: {node: '>=4'}
+
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
@@ -451,23 +1072,69 @@ packages:
   fast-levenshtein@2.0.6:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
 
-  fdir@6.5.0:
-    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
-    engines: {node: '>=12.0.0'}
+  fast-string-truncated-width@3.0.3:
+    resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+  fast-string-width@3.0.2:
+    resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
+
+  fast-uri@3.1.8:
+    resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
+
+  fast-wrap-ansi@0.2.2:
+    resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
+
+  fdir@6.5.0:
+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+    engines: {node: '>=12.0.0'}
     peerDependencies:
       picomatch: ^3 || ^4
     peerDependenciesMeta:
       picomatch:
         optional: true
 
+  figures@2.0.0:
+    resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
+    engines: {node: '>=4'}
+
+  figures@3.2.0:
+    resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
+    engines: {node: '>=8'}
+
   file-entry-cache@8.0.0:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
 
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  find-config@1.0.0:
+    resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==}
+    engines: {node: '>= 0.12'}
+
+  find-node-modules@2.1.3:
+    resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
+
+  find-root@1.1.0:
+    resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
+
+  find-up@2.1.0:
+    resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==}
+    engines: {node: '>=4'}
+
+  find-up@4.1.0:
+    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+    engines: {node: '>=8'}
+
   find-up@5.0.0:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
 
+  findup-sync@4.0.0:
+    resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
+    engines: {node: '>= 8'}
+
   flat-cache@4.0.1:
     resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
     engines: {node: '>=16'}
@@ -475,15 +1142,73 @@ packages:
   flatted@3.4.4:
     resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
 
+  fs-extra@9.1.0:
+    resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
+    engines: {node: '>=10'}
+
+  fs.realpath@1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
   fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
 
+  function-bind@1.1.2:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  get-caller-file@2.0.5:
+    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+    engines: {node: 6.* || 8.* || >= 10.*}
+
+  get-east-asian-width@1.7.0:
+    resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
+    engines: {node: '>=18'}
+
+  get-pkg-repo@4.2.1:
+    resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==}
+    engines: {node: '>=6.9.0'}
+    hasBin: true
+
+  git-raw-commits@2.0.11:
+    resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
+    engines: {node: '>=10'}
+    deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
+    hasBin: true
+
+  git-remote-origin-url@2.0.0:
+    resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==}
+    engines: {node: '>=4'}
+
+  git-semver-tags@4.1.1:
+    resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==}
+    engines: {node: '>=10'}
+    deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
+    hasBin: true
+
+  gitconfiglocal@1.0.0:
+    resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==}
+
   glob-parent@6.0.2:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
 
+  glob@7.2.3:
+    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+
+  global-directory@5.0.0:
+    resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==}
+    engines: {node: '>=20'}
+
+  global-modules@1.0.0:
+    resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
+    engines: {node: '>=0.10.0'}
+
+  global-prefix@1.0.2:
+    resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==}
+    engines: {node: '>=0.10.0'}
+
   globals@14.0.0:
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
     engines: {node: '>=18'}
@@ -492,10 +1217,57 @@ packages:
     resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
     engines: {node: '>=18'}
 
+  graceful-fs@4.2.11:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+  handlebars@4.7.9:
+    resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
+    engines: {node: '>=0.4.7'}
+    hasBin: true
+
+  hard-rejection@2.1.0:
+    resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
+    engines: {node: '>=6'}
+
+  has-flag@3.0.0:
+    resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+    engines: {node: '>=4'}
+
   has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
 
+  hasown@2.0.4:
+    resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+    engines: {node: '>= 0.4'}
+
+  homedir-polyfill@1.0.3:
+    resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
+    engines: {node: '>=0.10.0'}
+
+  hosted-git-info@2.8.9:
+    resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+
+  hosted-git-info@4.1.0:
+    resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
+    engines: {node: '>=10'}
+
+  husky@9.1.7:
+    resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  iconv-lite@0.4.24:
+    resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+    engines: {node: '>=0.10.0'}
+
+  iconv-lite@0.7.3:
+    resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==}
+    engines: {node: '>=0.10.0'}
+
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
   ignore@5.3.2:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
@@ -512,17 +1284,120 @@ packages:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
 
+  indent-string@4.0.0:
+    resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+    engines: {node: '>=8'}
+
+  inflight@1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  ini@1.3.8:
+    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+  ini@6.0.0:
+    resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==}
+    engines: {node: ^20.17.0 || >=22.9.0}
+
+  inquirer@14.2.2:
+    resolution: {integrity: sha512-WGerZIIb7mTtaFFskkD/P0europ8xBWf1OXACMiugzKPSmVzoqhcPSuSUzbzMch/t3rf1ncBT3v/0WU73cLqJQ==}
+    engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  inquirer@6.5.2:
+    resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==}
+    engines: {node: '>=6.0.0'}
+
+  inquirer@8.2.7:
+    resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==}
+    engines: {node: '>=12.0.0'}
+
+  is-arrayish@0.2.1:
+    resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+  is-core-module@2.17.0:
+    resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==}
+    engines: {node: '>= 0.4'}
+
   is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
 
+  is-fullwidth-code-point@2.0.0:
+    resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
+    engines: {node: '>=4'}
+
+  is-fullwidth-code-point@3.0.0:
+    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+    engines: {node: '>=8'}
+
   is-glob@4.0.3:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
 
+  is-interactive@1.0.0:
+    resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
+    engines: {node: '>=8'}
+
+  is-interactive@2.0.0:
+    resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
+    engines: {node: '>=12'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  is-obj@2.0.0:
+    resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
+    engines: {node: '>=8'}
+
+  is-plain-obj@1.1.0:
+    resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
+    engines: {node: '>=0.10.0'}
+
+  is-plain-obj@4.1.0:
+    resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+    engines: {node: '>=12'}
+
+  is-text-path@1.0.1:
+    resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
+    engines: {node: '>=0.10.0'}
+
+  is-unicode-supported@0.1.0:
+    resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+    engines: {node: '>=10'}
+
+  is-unicode-supported@1.3.0:
+    resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
+    engines: {node: '>=12'}
+
+  is-utf8@0.2.1:
+    resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
+
+  is-windows@1.0.2:
+    resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
+    engines: {node: '>=0.10.0'}
+
+  isarray@1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
   isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
+  jiti@2.6.1:
+    resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+    hasBin: true
+
+  js-tokens@4.0.0:
+    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
   js-yaml@4.3.2:
     resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
     hasBin: true
@@ -530,15 +1405,38 @@ packages:
   json-buffer@3.0.1:
     resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
 
+  json-parse-better-errors@1.0.2:
+    resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
+
+  json-parse-even-better-errors@2.3.1:
+    resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
   json-schema-traverse@0.4.1:
     resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
 
+  json-schema-traverse@1.0.0:
+    resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
   json-stable-stringify-without-jsonify@1.0.1:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
 
+  json-stringify-safe@5.0.1:
+    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
+  jsonfile@6.2.1:
+    resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
+
+  jsonparse@1.3.1:
+    resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
+    engines: {'0': node >= 0.2.0}
+
   keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 
+  kind-of@6.0.3:
+    resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+    engines: {node: '>=0.10.0'}
+
   levn@0.4.1:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
@@ -617,16 +1515,87 @@ packages:
     resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
     engines: {node: '>= 12.0.0'}
 
+  lines-and-columns@1.2.4:
+    resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+  load-json-file@4.0.0:
+    resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
+    engines: {node: '>=4'}
+
+  locate-path@2.0.0:
+    resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
+    engines: {node: '>=4'}
+
+  locate-path@5.0.0:
+    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+    engines: {node: '>=8'}
+
   locate-path@6.0.0:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
 
+  lodash.ismatch@4.4.0:
+    resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==}
+
+  lodash.map@4.6.0:
+    resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
+
   lodash.merge@4.6.2:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
 
+  lodash@4.18.1:
+    resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+  log-symbols@4.1.0:
+    resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+    engines: {node: '>=10'}
+
+  log-symbols@5.1.0:
+    resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
+    engines: {node: '>=12'}
+
+  longest@2.0.1:
+    resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
+    engines: {node: '>=0.10.0'}
+
+  lru-cache@6.0.0:
+    resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+    engines: {node: '>=10'}
+
   magic-string@0.30.21:
     resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
 
+  map-obj@1.0.1:
+    resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
+    engines: {node: '>=0.10.0'}
+
+  map-obj@4.3.0:
+    resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
+    engines: {node: '>=8'}
+
+  meow@8.1.2:
+    resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
+    engines: {node: '>=10'}
+
+  merge@2.1.1:
+    resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
+
+  micromatch@4.0.8:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+    engines: {node: '>=8.6'}
+
+  mimic-fn@1.2.0:
+    resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
+    engines: {node: '>=4'}
+
+  mimic-fn@2.1.0:
+    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+    engines: {node: '>=6'}
+
+  min-indent@1.0.1:
+    resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+    engines: {node: '>=4'}
+
   minimatch@10.2.6:
     resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
     engines: {node: 18 || 20 || >=22}
@@ -634,9 +1603,34 @@ packages:
   minimatch@3.1.5:
     resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
 
+  minimist-options@4.1.0:
+    resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
+    engines: {node: '>= 6'}
+
+  minimist@1.2.8:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+  mkdirp@0.5.6:
+    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+    hasBin: true
+
+  modify-values@1.0.1:
+    resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
+    engines: {node: '>=0.10.0'}
+
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
+  mute-stream@0.0.7:
+    resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==}
+
+  mute-stream@0.0.8:
+    resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
+
+  mute-stream@3.0.0:
+    resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
+    engines: {node: ^20.17.0 || >=22.9.0}
+
   nanoid@3.3.19:
     resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -645,44 +1639,144 @@ packages:
   natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
+  neo-async@2.6.2:
+    resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+
+  normalize-package-data@2.5.0:
+    resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
+
+  normalize-package-data@3.0.3:
+    resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
+    engines: {node: '>=10'}
+
   obug@2.2.1:
     resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
     engines: {node: '>=12.20.0'}
 
+  once@1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+  onetime@2.0.1:
+    resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
+    engines: {node: '>=4'}
+
+  onetime@5.1.2:
+    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+    engines: {node: '>=6'}
+
   optionator@0.9.4:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
 
+  ora@5.4.1:
+    resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
+    engines: {node: '>=10'}
+
+  ora@6.3.1:
+    resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  os-homedir@1.0.2:
+    resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==}
+    engines: {node: '>=0.10.0'}
+
+  os-tmpdir@1.0.2:
+    resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
+    engines: {node: '>=0.10.0'}
+
+  p-limit@1.3.0:
+    resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==}
+    engines: {node: '>=4'}
+
+  p-limit@2.3.0:
+    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+    engines: {node: '>=6'}
+
   p-limit@3.1.0:
     resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
     engines: {node: '>=10'}
 
+  p-locate@2.0.0:
+    resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==}
+    engines: {node: '>=4'}
+
+  p-locate@4.1.0:
+    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+    engines: {node: '>=8'}
+
   p-locate@5.0.0:
     resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
     engines: {node: '>=10'}
 
+  p-try@1.0.0:
+    resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
+    engines: {node: '>=4'}
+
+  p-try@2.2.0:
+    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+    engines: {node: '>=6'}
+
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
 
+  parse-json@4.0.0:
+    resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
+    engines: {node: '>=4'}
+
+  parse-json@5.2.0:
+    resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+    engines: {node: '>=8'}
+
+  parse-passwd@1.0.0:
+    resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
+    engines: {node: '>=0.10.0'}
+
+  path-exists@3.0.0:
+    resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
+    engines: {node: '>=4'}
+
   path-exists@4.0.0:
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
     engines: {node: '>=8'}
 
+  path-is-absolute@1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+    engines: {node: '>=0.10.0'}
+
   path-key@3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
 
+  path-parse@1.0.7:
+    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+  path-type@3.0.0:
+    resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
+    engines: {node: '>=4'}
+
   pathe@2.0.3:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
 
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
+  picomatch@2.3.2:
+    resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+    engines: {node: '>=8.6'}
+
   picomatch@4.0.7:
     resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
     engines: {node: '>=12'}
 
+  pify@2.3.0:
+    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+    engines: {node: '>=0.10.0'}
+
+  pify@3.0.0:
+    resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
+    engines: {node: '>=4'}
+
   postcss@8.5.28:
     resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
     engines: {node: ^10 || ^12 || >=14}
@@ -691,19 +1785,131 @@ packages:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
 
+  process-nextick-args@2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  q@1.5.1:
+    resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
+    engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
+    deprecated: |-
+      You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
+
+      (For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
+
+  quick-lru@4.0.1:
+    resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
+    engines: {node: '>=8'}
+
+  read-pkg-up@3.0.0:
+    resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==}
+    engines: {node: '>=4'}
+
+  read-pkg-up@7.0.1:
+    resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
+    engines: {node: '>=8'}
+
+  read-pkg@3.0.0:
+    resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
+    engines: {node: '>=4'}
+
+  read-pkg@5.2.0:
+    resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
+    engines: {node: '>=8'}
+
+  readable-stream@2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
+  readable-stream@3.6.2:
+    resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+    engines: {node: '>= 6'}
+
+  redent@3.0.0:
+    resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+    engines: {node: '>=8'}
+
+  require-directory@2.1.1:
+    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+    engines: {node: '>=0.10.0'}
+
+  require-from-string@2.0.2:
+    resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+    engines: {node: '>=0.10.0'}
+
+  resolve-dir@1.0.1:
+    resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
+    engines: {node: '>=0.10.0'}
+
   resolve-from@4.0.0:
     resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
     engines: {node: '>=4'}
 
+  resolve-from@5.0.0:
+    resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+    engines: {node: '>=8'}
+
+  resolve@1.22.12:
+    resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+    engines: {node: '>= 0.4'}
+    hasBin: true
+
+  restore-cursor@2.0.0:
+    resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
+    engines: {node: '>=4'}
+
+  restore-cursor@3.1.0:
+    resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
+    engines: {node: '>=8'}
+
+  restore-cursor@4.0.0:
+    resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  rimraf@2.6.3:
+    resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
+    deprecated: Rimraf versions prior to v4 are no longer supported
+    hasBin: true
+
   rolldown@1.2.9:
     resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==}
     engines: {node: ^20.19.0 || >=22.12.0}
     hasBin: true
 
+  run-async@2.4.1:
+    resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
+    engines: {node: '>=0.12.0'}
+
+  run-async@4.0.6:
+    resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==}
+    engines: {node: '>=0.12.0'}
+
+  rxjs@6.6.7:
+    resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
+    engines: {npm: '>=2.0.0'}
+
+  rxjs@7.8.2:
+    resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+
+  safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+  safer-buffer@2.1.2:
+    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+  semver@5.7.2:
+    resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
+    hasBin: true
+
+  semver@6.3.1:
+    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+    hasBin: true
+
   semver@7.8.5:
     resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
     engines: {node: '>=10'}
@@ -720,24 +1926,132 @@ packages:
   siginfo@2.0.0:
     resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
 
+  signal-exit@3.0.7:
+    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+  signal-exit@4.1.0:
+    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+    engines: {node: '>=14'}
+
   source-map-js@1.2.1:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
 
+  source-map@0.6.1:
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+    engines: {node: '>=0.10.0'}
+
+  spdx-correct@3.2.0:
+    resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
+
+  spdx-exceptions@2.5.0:
+    resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
+
+  spdx-expression-parse@3.0.1:
+    resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
+
+  spdx-license-ids@3.0.24:
+    resolution: {integrity: sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==}
+
+  split2@3.2.2:
+    resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
+
+  split@1.0.1:
+    resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
+
   stackback@0.0.2:
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 
   std-env@4.2.0:
     resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
 
+  stdin-discarder@0.1.0:
+    resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  string-width@2.1.1:
+    resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
+    engines: {node: '>=4'}
+
+  string-width@4.2.3:
+    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+    engines: {node: '>=8'}
+
+  string-width@7.2.0:
+    resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+    engines: {node: '>=18'}
+
+  string-width@8.2.2:
+    resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
+    engines: {node: '>=20'}
+
+  string_decoder@1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
+  string_decoder@1.3.0:
+    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+  strip-ansi@4.0.0:
+    resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==}
+    engines: {node: '>=4'}
+
+  strip-ansi@5.2.0:
+    resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
+    engines: {node: '>=6'}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+    engines: {node: '>=8'}
+
+  strip-ansi@7.2.0:
+    resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+    engines: {node: '>=12'}
+
+  strip-bom@3.0.0:
+    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+    engines: {node: '>=4'}
+
+  strip-bom@4.0.0:
+    resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
+    engines: {node: '>=8'}
+
+  strip-indent@3.0.0:
+    resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+    engines: {node: '>=8'}
+
   strip-json-comments@3.1.1:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
+  supports-color@5.5.0:
+    resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+    engines: {node: '>=4'}
+
   supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
 
+  supports-preserve-symlinks-flag@1.0.0:
+    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+    engines: {node: '>= 0.4'}
+
+  temp@0.9.4:
+    resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
+    engines: {node: '>=6.0.0'}
+
+  text-extensions@1.9.0:
+    resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==}
+    engines: {node: '>=0.10'}
+
+  through2@2.0.5:
+    resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
+
+  through2@4.0.2:
+    resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
+
+  through@2.3.8:
+    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
@@ -753,16 +2067,50 @@ packages:
     resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
     engines: {node: '>=14.0.0'}
 
+  tmp@0.0.33:
+    resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
+    engines: {node: '>=0.6.0'}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  trim-newlines@3.0.1:
+    resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
+    engines: {node: '>=8'}
+
   ts-api-utils@2.5.0:
     resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
     engines: {node: '>=18.12'}
     peerDependencies:
       typescript: '>=4.8.4'
 
+  tslib@1.14.1:
+    resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+
+  tslib@2.8.1:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
 
+  type-fest@0.18.1:
+    resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
+    engines: {node: '>=10'}
+
+  type-fest@0.21.3:
+    resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
+    engines: {node: '>=10'}
+
+  type-fest@0.6.0:
+    resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
+    engines: {node: '>=8'}
+
+  type-fest@0.8.1:
+    resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
+    engines: {node: '>=8'}
+
   typescript-eslint@8.70.1:
     resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -775,9 +2123,31 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  uglify-js@3.19.3:
+    resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
+    engines: {node: '>=0.8.0'}
+    hasBin: true
+
+  undici-types@8.9.0:
+    resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
+
+  universalify@2.0.1:
+    resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+    engines: {node: '>= 10.0.0'}
+
   uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
 
+  user-home@2.0.0:
+    resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==}
+    engines: {node: '>=0.10.0'}
+
+  util-deprecate@1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+  validate-npm-package-license@3.0.4:
+    resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+
   vite@8.3.0:
     resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
@@ -862,6 +2232,13 @@ packages:
       jsdom:
         optional: true
 
+  wcwidth@1.0.1:
+    resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
+
+  which@1.3.1:
+    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+    hasBin: true
+
   which@2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
@@ -876,15 +2253,187 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
+  wordwrap@1.0.0:
+    resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
+
+  wrap-ansi@6.2.0:
+    resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+    engines: {node: '>=8'}
+
+  wrap-ansi@7.0.0:
+    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+    engines: {node: '>=10'}
+
+  wrap-ansi@9.0.2:
+    resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+    engines: {node: '>=18'}
+
+  wrappy@1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+  xtend@4.0.2:
+    resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+    engines: {node: '>=0.4'}
+
+  y18n@5.0.8:
+    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+    engines: {node: '>=10'}
+
+  yallist@4.0.0:
+    resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+  yargs-parser@20.2.9:
+    resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+    engines: {node: '>=10'}
+
+  yargs-parser@22.0.0:
+    resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+    engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+  yargs@16.2.2:
+    resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==}
+    engines: {node: '>=10'}
+
+  yargs@18.2.0:
+    resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==}
+    engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
   yocto-queue@0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
 snapshots:
 
-  '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
+  '@babel/code-frame@7.29.7':
+    dependencies:
+      '@babel/helper-validator-identifier': 7.29.7
+      js-tokens: 4.0.0
+      picocolors: 1.1.1
+
+  '@babel/helper-validator-identifier@7.29.7': {}
+
+  '@commitlint/cli@21.2.3(@types/node@26.6.2)(conventional-commits-parser@7.1.2)(typescript@5.9.3)':
+    dependencies:
+      '@commitlint/config-conventional': 21.2.3
+      '@commitlint/format': 21.2.3
+      '@commitlint/lint': 21.2.3
+      '@commitlint/load': 21.2.3(@types/node@26.6.2)(typescript@5.9.3)
+      '@commitlint/read': 21.2.3(conventional-commits-parser@7.1.2)
+      '@commitlint/types': 21.2.3
+      tinyexec: 1.3.1
+      yargs: 18.2.0
+    transitivePeerDependencies:
+      - '@types/node'
+      - conventional-commits-filter
+      - conventional-commits-parser
+      - typescript
+
+  '@commitlint/config-conventional@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      conventional-changelog-conventionalcommits: 10.4.0
+
+  '@commitlint/config-validator@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      ajv: 8.20.0
+
+  '@commitlint/ensure@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      es-toolkit: 1.52.0
+
+  '@commitlint/execute-rule@21.0.1': {}
+
+  '@commitlint/format@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      picocolors: 1.1.1
+
+  '@commitlint/is-ignored@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      semver: 7.8.5
+
+  '@commitlint/lint@21.2.3':
+    dependencies:
+      '@commitlint/is-ignored': 21.2.3
+      '@commitlint/parse': 21.2.3
+      '@commitlint/rules': 21.2.3
+      '@commitlint/types': 21.2.3
+
+  '@commitlint/load@21.2.3(@types/node@26.6.2)(typescript@5.9.3)':
+    dependencies:
+      '@commitlint/config-validator': 21.2.3
+      '@commitlint/execute-rule': 21.0.1
+      '@commitlint/resolve-extends': 21.2.3
+      '@commitlint/types': 21.2.3
+      cosmiconfig: 9.0.2(typescript@5.9.3)
+      cosmiconfig-typescript-loader: 6.3.0(@types/node@26.6.2)(cosmiconfig@9.0.2(typescript@5.9.3))(typescript@5.9.3)
+      es-toolkit: 1.52.0
+      is-plain-obj: 4.1.0
+      picocolors: 1.1.1
+    transitivePeerDependencies:
+      - '@types/node'
+      - typescript
+
+  '@commitlint/message@21.2.0': {}
+
+  '@commitlint/parse@21.2.3':
+    dependencies:
+      '@commitlint/types': 21.2.3
+      conventional-changelog-angular: 9.4.0
+      conventional-commits-parser: 7.1.2
+
+  '@commitlint/read@21.2.3(conventional-commits-parser@7.1.2)':
+    dependencies:
+      '@commitlint/top-level': 21.2.0
+      '@commitlint/types': 21.2.3
+      '@conventional-changelog/git-client': 3.1.2(conventional-commits-parser@7.1.2)
+      tinyexec: 1.3.1
+    transitivePeerDependencies:
+      - conventional-commits-filter
+      - conventional-commits-parser
+
+  '@commitlint/resolve-extends@21.2.3':
+    dependencies:
+      '@commitlint/config-validator': 21.2.3
+      '@commitlint/types': 21.2.3
+      es-toolkit: 1.52.0
+      global-directory: 5.0.0
+      resolve-from: 5.0.0
+
+  '@commitlint/rules@21.2.3':
+    dependencies:
+      '@commitlint/ensure': 21.2.3
+      '@commitlint/message': 21.2.0
+      '@commitlint/to-lines': 21.0.1
+      '@commitlint/types': 21.2.3
+
+  '@commitlint/to-lines@21.0.1': {}
+
+  '@commitlint/top-level@21.2.0':
+    dependencies:
+      escalade: 3.2.0
+
+  '@commitlint/types@21.2.3':
+    dependencies:
+      conventional-commits-parser: 7.1.2
+      picocolors: 1.1.1
+
+  '@conventional-changelog/git-client@3.1.2(conventional-commits-parser@7.1.2)':
     dependencies:
-      eslint: 9.39.5
+      '@simple-libs/child-process-utils': 2.0.0
+      '@simple-libs/stream-utils': 2.0.0
+      semver: 7.8.5
+    optionalDependencies:
+      conventional-commits-parser: 7.1.2
+
+  '@conventional-changelog/template@1.4.0': {}
+
+  '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.6.1))':
+    dependencies:
+      eslint: 9.39.5(jiti@2.6.1)
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.2': {}
@@ -919,35 +2468,176 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.39.5': {}
+  '@eslint/js@9.39.5': {}
+
+  '@eslint/object-schema@2.1.7': {}
+
+  '@eslint/plugin-kit@0.4.1':
+    dependencies:
+      '@eslint/core': 0.17.0
+      levn: 0.4.1
+
+  '@humanfs/core@0.19.2':
+    dependencies:
+      '@humanfs/types': 0.15.0
+
+  '@humanfs/node@0.16.8':
+    dependencies:
+      '@humanfs/core': 0.19.2
+      '@humanfs/types': 0.15.0
+      '@humanwhocodes/retry': 0.4.3
+
+  '@humanfs/types@0.15.0': {}
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/retry@0.4.3': {}
+
+  '@hutson/parse-repository-url@3.0.2': {}
+
+  '@inquirer/ansi@2.0.8': {}
+
+  '@inquirer/checkbox@5.2.5(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/ansi': 2.0.8
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/figures': 2.0.9
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/confirm@6.3.2(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/core@12.0.3(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/ansi': 2.0.8
+      '@inquirer/figures': 2.0.9
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+      cli-width: 4.1.0
+      fast-wrap-ansi: 0.2.2
+      mute-stream: 3.0.0
+      signal-exit: 4.1.0
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/editor@5.3.3(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/external-editor': 3.0.5(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/expand@5.1.5(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/external-editor@1.0.3(@types/node@26.6.2)':
+    dependencies:
+      chardet: 2.2.0
+      iconv-lite: 0.7.3
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@eslint/object-schema@2.1.7': {}
+  '@inquirer/external-editor@3.0.5(@types/node@26.6.2)':
+    dependencies:
+      chardet: 2.2.0
+      iconv-lite: 0.7.3
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@eslint/plugin-kit@0.4.1':
+  '@inquirer/figures@2.0.9': {}
+
+  '@inquirer/input@5.1.6(@types/node@26.6.2)':
     dependencies:
-      '@eslint/core': 0.17.0
-      levn: 0.4.1
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@humanfs/core@0.19.2':
+  '@inquirer/number@4.2.3(@types/node@26.6.2)':
     dependencies:
-      '@humanfs/types': 0.15.0
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@humanfs/node@0.16.8':
+  '@inquirer/password@5.2.2(@types/node@26.6.2)':
     dependencies:
-      '@humanfs/core': 0.19.2
-      '@humanfs/types': 0.15.0
-      '@humanwhocodes/retry': 0.4.3
+      '@inquirer/ansi': 2.0.8
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@humanfs/types@0.15.0': {}
+  '@inquirer/prompts@8.7.2(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/checkbox': 5.2.5(@types/node@26.6.2)
+      '@inquirer/confirm': 6.3.2(@types/node@26.6.2)
+      '@inquirer/editor': 5.3.3(@types/node@26.6.2)
+      '@inquirer/expand': 5.1.5(@types/node@26.6.2)
+      '@inquirer/input': 5.1.6(@types/node@26.6.2)
+      '@inquirer/number': 4.2.3(@types/node@26.6.2)
+      '@inquirer/password': 5.2.2(@types/node@26.6.2)
+      '@inquirer/rawlist': 5.3.5(@types/node@26.6.2)
+      '@inquirer/search': 4.3.3(@types/node@26.6.2)
+      '@inquirer/select': 5.2.5(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@humanwhocodes/module-importer@1.0.1': {}
+  '@inquirer/rawlist@5.3.5(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
 
-  '@humanwhocodes/retry@0.4.3': {}
+  '@inquirer/search@4.3.3(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/figures': 2.0.9
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/select@5.2.5(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/ansi': 2.0.8
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/figures': 2.0.9
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  '@inquirer/type@4.1.1(@types/node@26.6.2)':
+    optionalDependencies:
+      '@types/node': 26.6.2
 
   '@jridgewell/sourcemap-codec@1.6.0': {}
 
   '@oxc-project/types@0.150.0': {}
 
+  '@picgo/bump-version@3.0.0(@types/node@26.6.2)':
+    dependencies:
+      '@inquirer/prompts': 8.7.2(@types/node@26.6.2)
+      chalk: 6.0.0
+      compare-func: 2.0.0
+      conventional-changelog: 3.1.25
+      inquirer: 14.2.2(@types/node@26.6.2)
+      minimist: 1.2.8
+      ora: 6.3.1
+      semver: 7.8.5
+    transitivePeerDependencies:
+      - '@types/node'
+
   '@rolldown/binding-android-arm-eabi@1.2.9':
     optional: true
 
@@ -995,6 +2685,12 @@ snapshots:
 
   '@rolldown/pluginutils@1.0.1': {}
 
+  '@simple-libs/child-process-utils@2.0.0':
+    dependencies:
+      '@simple-libs/stream-utils': 2.0.0
+
+  '@simple-libs/stream-utils@2.0.0': {}
+
   '@standard-schema/spec@1.1.0': {}
 
   '@types/chai@5.2.3':
@@ -1008,15 +2704,23 @@ snapshots:
 
   '@types/json-schema@7.0.15': {}
 
-  '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)':
+  '@types/minimist@1.2.5': {}
+
+  '@types/node@26.6.2':
+    dependencies:
+      undici-types: 8.9.0
+
+  '@types/normalize-package-data@2.4.4': {}
+
+  '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
-      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
       '@typescript-eslint/scope-manager': 8.70.1
-      '@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
       '@typescript-eslint/visitor-keys': 8.70.1
-      eslint: 9.39.5
+      eslint: 9.39.5(jiti@2.6.1)
       ignore: 7.0.9
       natural-compare: 1.4.0
       ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -1024,14 +2728,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+  '@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.70.1
       '@typescript-eslint/types': 8.70.1
       '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
       '@typescript-eslint/visitor-keys': 8.70.1
       debug: 4.4.3
-      eslint: 9.39.5
+      eslint: 9.39.5(jiti@2.6.1)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
@@ -1054,13 +2758,13 @@ snapshots:
     dependencies:
       typescript: 5.9.3
 
-  '@typescript-eslint/type-utils@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+  '@typescript-eslint/type-utils@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)':
     dependencies:
       '@typescript-eslint/types': 8.70.1
       '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
       debug: 4.4.3
-      eslint: 9.39.5
+      eslint: 9.39.5(jiti@2.6.1)
       ts-api-utils: 2.5.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
@@ -1083,13 +2787,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.70.1(eslint@9.39.5)(typescript@5.9.3)':
+  '@typescript-eslint/utils@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.6.1))
       '@typescript-eslint/scope-manager': 8.70.1
       '@typescript-eslint/types': 8.70.1
       '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
-      eslint: 9.39.5
+      eslint: 9.39.5(jiti@2.6.1)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
@@ -1108,13 +2812,13 @@ snapshots:
       chai: 6.2.2
       tinyrainbow: 3.1.1
 
-  '@vitest/mocker@4.1.11(vite@8.3.0)':
+  '@vitest/mocker@4.1.11(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1))':
     dependencies:
       '@vitest/spy': 4.1.11
       estree-walker: 3.0.3
       magic-string: 0.30.21
     optionalDependencies:
-      vite: 8.3.0
+      vite: 8.3.0(@types/node@26.6.2)(jiti@2.6.1)
 
   '@vitest/pretty-format@4.1.11':
     dependencies:
@@ -1140,12 +2844,19 @@ snapshots:
       convert-source-map: 2.0.0
       tinyrainbow: 3.1.1
 
+  JSONStream@1.3.5:
+    dependencies:
+      jsonparse: 1.3.1
+      through: 2.3.8
+
   acorn-jsx@5.3.2(acorn@8.18.0):
     dependencies:
       acorn: 8.18.0
 
   acorn@8.18.0: {}
 
+  add-stream@1.0.0: {}
+
   ajv@6.15.0:
     dependencies:
       fast-deep-equal: 3.1.3
@@ -1153,18 +2864,67 @@ snapshots:
       json-schema-traverse: 0.4.1
       uri-js: 4.4.1
 
+  ajv@8.20.0:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-uri: 3.1.8
+      json-schema-traverse: 1.0.0
+      require-from-string: 2.0.2
+
+  ansi-escapes@3.2.0: {}
+
+  ansi-escapes@4.3.2:
+    dependencies:
+      type-fest: 0.21.3
+
+  ansi-regex@3.0.1: {}
+
+  ansi-regex@4.1.1: {}
+
+  ansi-regex@5.0.1: {}
+
+  ansi-regex@6.3.0: {}
+
+  ansi-styles@3.2.1:
+    dependencies:
+      color-convert: 1.9.3
+
   ansi-styles@4.3.0:
     dependencies:
       color-convert: 2.0.1
 
+  ansi-styles@6.2.3: {}
+
   argparse@2.0.1: {}
 
+  argue-cli@3.2.0: {}
+
+  array-ify@1.0.0: {}
+
+  arrify@1.0.1: {}
+
   assertion-error@2.0.1: {}
 
+  at-least-node@1.0.0: {}
+
   balanced-match@1.0.2: {}
 
   balanced-match@4.0.4: {}
 
+  base64-js@1.5.1: {}
+
+  bl@4.1.0:
+    dependencies:
+      buffer: 5.7.1
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+
+  bl@5.1.0:
+    dependencies:
+      buffer: 6.0.3
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+
   brace-expansion@1.1.21:
     dependencies:
       balanced-match: 1.0.2
@@ -1174,41 +2934,344 @@ snapshots:
     dependencies:
       balanced-match: 4.0.4
 
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  buffer@5.7.1:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  cachedir@2.4.0: {}
+
   callsites@3.1.0: {}
 
+  camelcase-keys@6.2.2:
+    dependencies:
+      camelcase: 5.3.1
+      map-obj: 4.3.0
+      quick-lru: 4.0.1
+
+  camelcase@5.3.1: {}
+
   chai@6.2.2: {}
 
+  chalk@2.4.2:
+    dependencies:
+      ansi-styles: 3.2.1
+      escape-string-regexp: 1.0.5
+      supports-color: 5.5.0
+
   chalk@4.1.2:
     dependencies:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
+  chalk@5.6.2: {}
+
+  chalk@6.0.0: {}
+
+  chardet@0.7.0: {}
+
+  chardet@2.2.0: {}
+
+  cli-cursor@2.1.0:
+    dependencies:
+      restore-cursor: 2.0.0
+
+  cli-cursor@3.1.0:
+    dependencies:
+      restore-cursor: 3.1.0
+
+  cli-cursor@4.0.0:
+    dependencies:
+      restore-cursor: 4.0.0
+
+  cli-spinners@2.9.2: {}
+
+  cli-width@2.2.1: {}
+
+  cli-width@3.0.0: {}
+
+  cli-width@4.1.0: {}
+
+  cliui@7.0.4:
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 7.0.0
+
+  cliui@9.0.1:
+    dependencies:
+      string-width: 7.2.0
+      strip-ansi: 7.2.0
+      wrap-ansi: 9.0.2
+
+  clone@1.0.4: {}
+
+  color-convert@1.9.3:
+    dependencies:
+      color-name: 1.1.3
+
   color-convert@2.0.1:
     dependencies:
       color-name: 1.1.4
 
+  color-name@1.1.3: {}
+
   color-name@1.1.4: {}
 
+  commitizen@4.3.2(@types/node@26.6.2)(typescript@5.9.3):
+    dependencies:
+      cachedir: 2.4.0
+      cz-conventional-changelog: 3.3.0(@types/node@26.6.2)(typescript@5.9.3)
+      dedent: 0.7.0
+      detect-indent: 6.1.0
+      find-node-modules: 2.1.3
+      find-root: 1.1.0
+      fs-extra: 9.1.0
+      glob: 7.2.3
+      inquirer: 8.2.7(@types/node@26.6.2)
+      is-utf8: 0.2.1
+      lodash: 4.18.1
+      minimist: 1.2.8
+      strip-bom: 4.0.0
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - '@types/node'
+      - typescript
+
+  compare-func@2.0.0:
+    dependencies:
+      array-ify: 1.0.0
+      dot-prop: 5.3.0
+
   concat-map@0.0.1: {}
 
+  conventional-changelog-angular@5.0.13:
+    dependencies:
+      compare-func: 2.0.0
+      q: 1.5.1
+
+  conventional-changelog-angular@9.4.0:
+    dependencies:
+      '@conventional-changelog/template': 1.4.0
+
+  conventional-changelog-atom@2.0.8:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-codemirror@2.0.8:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-conventionalcommits@10.4.0:
+    dependencies:
+      '@conventional-changelog/template': 1.4.0
+
+  conventional-changelog-conventionalcommits@4.6.3:
+    dependencies:
+      compare-func: 2.0.0
+      lodash: 4.18.1
+      q: 1.5.1
+
+  conventional-changelog-core@4.2.4:
+    dependencies:
+      add-stream: 1.0.0
+      conventional-changelog-writer: 5.0.1
+      conventional-commits-parser: 3.2.4
+      dateformat: 3.0.3
+      get-pkg-repo: 4.2.1
+      git-raw-commits: 2.0.11
+      git-remote-origin-url: 2.0.0
+      git-semver-tags: 4.1.1
+      lodash: 4.18.1
+      normalize-package-data: 3.0.3
+      q: 1.5.1
+      read-pkg: 3.0.0
+      read-pkg-up: 3.0.0
+      through2: 4.0.2
+
+  conventional-changelog-ember@2.0.9:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-eslint@3.0.9:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-express@2.0.6:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-jquery@3.0.11:
+    dependencies:
+      q: 1.5.1
+
+  conventional-changelog-jshint@2.0.9:
+    dependencies:
+      compare-func: 2.0.0
+      q: 1.5.1
+
+  conventional-changelog-preset-loader@2.3.4: {}
+
+  conventional-changelog-writer@5.0.1:
+    dependencies:
+      conventional-commits-filter: 2.0.7
+      dateformat: 3.0.3
+      handlebars: 4.7.9
+      json-stringify-safe: 5.0.1
+      lodash: 4.18.1
+      meow: 8.1.2
+      semver: 6.3.1
+      split: 1.0.1
+      through2: 4.0.2
+
+  conventional-changelog@3.1.25:
+    dependencies:
+      conventional-changelog-angular: 5.0.13
+      conventional-changelog-atom: 2.0.8
+      conventional-changelog-codemirror: 2.0.8
+      conventional-changelog-conventionalcommits: 4.6.3
+      conventional-changelog-core: 4.2.4
+      conventional-changelog-ember: 2.0.9
+      conventional-changelog-eslint: 3.0.9
+      conventional-changelog-express: 2.0.6
+      conventional-changelog-jquery: 3.0.11
+      conventional-changelog-jshint: 2.0.9
+      conventional-changelog-preset-loader: 2.3.4
+
+  conventional-commit-types@3.0.0: {}
+
+  conventional-commits-filter@2.0.7:
+    dependencies:
+      lodash.ismatch: 4.4.0
+      modify-values: 1.0.1
+
+  conventional-commits-parser@3.2.4:
+    dependencies:
+      JSONStream: 1.3.5
+      is-text-path: 1.0.1
+      lodash: 4.18.1
+      meow: 8.1.2
+      split2: 3.2.2
+      through2: 4.0.2
+
+  conventional-commits-parser@7.1.2:
+    dependencies:
+      '@simple-libs/stream-utils': 2.0.0
+      argue-cli: 3.2.0
+
   convert-source-map@2.0.0: {}
 
+  core-util-is@1.0.3: {}
+
+  cosmiconfig-typescript-loader@6.3.0(@types/node@26.6.2)(cosmiconfig@9.0.2(typescript@5.9.3))(typescript@5.9.3):
+    dependencies:
+      '@types/node': 26.6.2
+      cosmiconfig: 9.0.2(typescript@5.9.3)
+      jiti: 2.6.1
+      typescript: 5.9.3
+
+  cosmiconfig@9.0.2(typescript@5.9.3):
+    dependencies:
+      env-paths: 2.2.1
+      import-fresh: 3.3.1
+      js-yaml: 4.3.2
+      parse-json: 5.2.0
+    optionalDependencies:
+      typescript: 5.9.3
+
   cross-spawn@7.0.6:
     dependencies:
       path-key: 3.1.1
       shebang-command: 2.0.0
       which: 2.0.2
 
+  cz-conventional-changelog@3.3.0(@types/node@26.6.2)(typescript@5.9.3):
+    dependencies:
+      chalk: 2.4.2
+      commitizen: 4.3.2(@types/node@26.6.2)(typescript@5.9.3)
+      conventional-commit-types: 3.0.0
+      lodash.map: 4.6.0
+      longest: 2.0.1
+      word-wrap: 1.2.5
+    optionalDependencies:
+      '@commitlint/load': 21.2.3(@types/node@26.6.2)(typescript@5.9.3)
+    transitivePeerDependencies:
+      - '@types/node'
+      - typescript
+
+  cz-customizable@7.5.4:
+    dependencies:
+      editor: 1.0.0
+      find-config: 1.0.0
+      inquirer: 6.5.2
+      lodash: 4.18.1
+      temp: 0.9.4
+      word-wrap: 1.2.5
+
+  dargs@7.0.0: {}
+
+  dateformat@3.0.3: {}
+
   debug@4.4.3:
     dependencies:
       ms: 2.1.3
 
+  decamelize-keys@1.1.1:
+    dependencies:
+      decamelize: 1.2.0
+      map-obj: 1.0.1
+
+  decamelize@1.2.0: {}
+
+  dedent@0.7.0: {}
+
   deep-is@0.1.4: {}
 
+  defaults@1.0.4:
+    dependencies:
+      clone: 1.0.4
+
+  detect-file@1.0.0: {}
+
+  detect-indent@6.1.0: {}
+
   detect-libc@2.1.2: {}
 
+  dot-prop@5.3.0:
+    dependencies:
+      is-obj: 2.0.0
+
+  editor@1.0.0: {}
+
+  emoji-regex@10.6.0: {}
+
+  emoji-regex@8.0.0: {}
+
+  env-paths@2.2.1: {}
+
+  error-ex@1.3.4:
+    dependencies:
+      is-arrayish: 0.2.1
+
+  es-errors@1.3.0: {}
+
   es-module-lexer@2.3.2: {}
 
+  es-toolkit@1.52.0: {}
+
+  escalade@3.2.0: {}
+
+  escape-string-regexp@1.0.5: {}
+
   escape-string-regexp@4.0.0: {}
 
   eslint-scope@8.4.0:
@@ -1222,9 +3285,9 @@ snapshots:
 
   eslint-visitor-keys@5.0.1: {}
 
-  eslint@9.39.5:
+  eslint@9.39.5(jiti@2.6.1):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.6.1))
       '@eslint-community/regexpp': 4.12.2
       '@eslint/config-array': 0.21.2
       '@eslint/config-helpers': 0.4.2
@@ -1258,6 +3321,8 @@ snapshots:
       minimatch: 3.1.5
       natural-compare: 1.4.0
       optionator: 0.9.4
+    optionalDependencies:
+      jiti: 2.6.1
     transitivePeerDependencies:
       - supports-color
 
@@ -1283,27 +3348,88 @@ snapshots:
 
   esutils@2.0.3: {}
 
+  expand-tilde@2.0.2:
+    dependencies:
+      homedir-polyfill: 1.0.3
+
   expect-type@1.4.0: {}
 
+  external-editor@3.1.0:
+    dependencies:
+      chardet: 0.7.0
+      iconv-lite: 0.4.24
+      tmp: 0.0.33
+
   fast-deep-equal@3.1.3: {}
 
   fast-json-stable-stringify@2.1.0: {}
 
   fast-levenshtein@2.0.6: {}
 
+  fast-string-truncated-width@3.0.3: {}
+
+  fast-string-width@3.0.2:
+    dependencies:
+      fast-string-truncated-width: 3.0.3
+
+  fast-uri@3.1.8: {}
+
+  fast-wrap-ansi@0.2.2:
+    dependencies:
+      fast-string-width: 3.0.2
+
   fdir@6.5.0(picomatch@4.0.7):
     optionalDependencies:
       picomatch: 4.0.7
 
+  figures@2.0.0:
+    dependencies:
+      escape-string-regexp: 1.0.5
+
+  figures@3.2.0:
+    dependencies:
+      escape-string-regexp: 1.0.5
+
   file-entry-cache@8.0.0:
     dependencies:
       flat-cache: 4.0.1
 
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-config@1.0.0:
+    dependencies:
+      user-home: 2.0.0
+
+  find-node-modules@2.1.3:
+    dependencies:
+      findup-sync: 4.0.0
+      merge: 2.1.1
+
+  find-root@1.1.0: {}
+
+  find-up@2.1.0:
+    dependencies:
+      locate-path: 2.0.0
+
+  find-up@4.1.0:
+    dependencies:
+      locate-path: 5.0.0
+      path-exists: 4.0.0
+
   find-up@5.0.0:
     dependencies:
       locate-path: 6.0.0
       path-exists: 4.0.0
 
+  findup-sync@4.0.0:
+    dependencies:
+      detect-file: 1.0.0
+      is-glob: 4.0.3
+      micromatch: 4.0.8
+      resolve-dir: 1.0.1
+
   flat-cache@4.0.1:
     dependencies:
       flatted: 3.4.4
@@ -1311,19 +3437,131 @@ snapshots:
 
   flatted@3.4.4: {}
 
+  fs-extra@9.1.0:
+    dependencies:
+      at-least-node: 1.0.0
+      graceful-fs: 4.2.11
+      jsonfile: 6.2.1
+      universalify: 2.0.1
+
+  fs.realpath@1.0.0: {}
+
   fsevents@2.3.3:
     optional: true
 
+  function-bind@1.1.2: {}
+
+  get-caller-file@2.0.5: {}
+
+  get-east-asian-width@1.7.0: {}
+
+  get-pkg-repo@4.2.1:
+    dependencies:
+      '@hutson/parse-repository-url': 3.0.2
+      hosted-git-info: 4.1.0
+      through2: 2.0.5
+      yargs: 16.2.2
+
+  git-raw-commits@2.0.11:
+    dependencies:
+      dargs: 7.0.0
+      lodash: 4.18.1
+      meow: 8.1.2
+      split2: 3.2.2
+      through2: 4.0.2
+
+  git-remote-origin-url@2.0.0:
+    dependencies:
+      gitconfiglocal: 1.0.0
+      pify: 2.3.0
+
+  git-semver-tags@4.1.1:
+    dependencies:
+      meow: 8.1.2
+      semver: 6.3.1
+
+  gitconfiglocal@1.0.0:
+    dependencies:
+      ini: 1.3.8
+
   glob-parent@6.0.2:
     dependencies:
       is-glob: 4.0.3
 
+  glob@7.2.3:
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.5
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+
+  global-directory@5.0.0:
+    dependencies:
+      ini: 6.0.0
+
+  global-modules@1.0.0:
+    dependencies:
+      global-prefix: 1.0.2
+      is-windows: 1.0.2
+      resolve-dir: 1.0.1
+
+  global-prefix@1.0.2:
+    dependencies:
+      expand-tilde: 2.0.2
+      homedir-polyfill: 1.0.3
+      ini: 1.3.8
+      is-windows: 1.0.2
+      which: 1.3.1
+
   globals@14.0.0: {}
 
   globals@16.5.0: {}
 
+  graceful-fs@4.2.11: {}
+
+  handlebars@4.7.9:
+    dependencies:
+      minimist: 1.2.8
+      neo-async: 2.6.2
+      source-map: 0.6.1
+      wordwrap: 1.0.0
+    optionalDependencies:
+      uglify-js: 3.19.3
+
+  hard-rejection@2.1.0: {}
+
+  has-flag@3.0.0: {}
+
   has-flag@4.0.0: {}
 
+  hasown@2.0.4:
+    dependencies:
+      function-bind: 1.1.2
+
+  homedir-polyfill@1.0.3:
+    dependencies:
+      parse-passwd: 1.0.0
+
+  hosted-git-info@2.8.9: {}
+
+  hosted-git-info@4.1.0:
+    dependencies:
+      lru-cache: 6.0.0
+
+  husky@9.1.7: {}
+
+  iconv-lite@0.4.24:
+    dependencies:
+      safer-buffer: 2.1.2
+
+  iconv-lite@0.7.3:
+    dependencies:
+      safer-buffer: 2.1.2
+
+  ieee754@1.2.1: {}
+
   ignore@5.3.2: {}
 
   ignore@7.0.9: {}
@@ -1335,28 +3573,146 @@ snapshots:
 
   imurmurhash@0.1.4: {}
 
+  indent-string@4.0.0: {}
+
+  inflight@1.0.6:
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+
+  inherits@2.0.4: {}
+
+  ini@1.3.8: {}
+
+  ini@6.0.0: {}
+
+  inquirer@14.2.2(@types/node@26.6.2):
+    dependencies:
+      '@inquirer/ansi': 2.0.8
+      '@inquirer/core': 12.0.3(@types/node@26.6.2)
+      '@inquirer/prompts': 8.7.2(@types/node@26.6.2)
+      '@inquirer/type': 4.1.1(@types/node@26.6.2)
+      mute-stream: 3.0.0
+      run-async: 4.0.6
+    optionalDependencies:
+      '@types/node': 26.6.2
+
+  inquirer@6.5.2:
+    dependencies:
+      ansi-escapes: 3.2.0
+      chalk: 2.4.2
+      cli-cursor: 2.1.0
+      cli-width: 2.2.1
+      external-editor: 3.1.0
+      figures: 2.0.0
+      lodash: 4.18.1
+      mute-stream: 0.0.7
+      run-async: 2.4.1
+      rxjs: 6.6.7
+      string-width: 2.1.1
+      strip-ansi: 5.2.0
+      through: 2.3.8
+
+  inquirer@8.2.7(@types/node@26.6.2):
+    dependencies:
+      '@inquirer/external-editor': 1.0.3(@types/node@26.6.2)
+      ansi-escapes: 4.3.2
+      chalk: 4.1.2
+      cli-cursor: 3.1.0
+      cli-width: 3.0.0
+      figures: 3.2.0
+      lodash: 4.18.1
+      mute-stream: 0.0.8
+      ora: 5.4.1
+      run-async: 2.4.1
+      rxjs: 7.8.2
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      through: 2.3.8
+      wrap-ansi: 6.2.0
+    transitivePeerDependencies:
+      - '@types/node'
+
+  is-arrayish@0.2.1: {}
+
+  is-core-module@2.17.0:
+    dependencies:
+      hasown: 2.0.4
+
   is-extglob@2.1.1: {}
 
+  is-fullwidth-code-point@2.0.0: {}
+
+  is-fullwidth-code-point@3.0.0: {}
+
   is-glob@4.0.3:
     dependencies:
       is-extglob: 2.1.1
 
+  is-interactive@1.0.0: {}
+
+  is-interactive@2.0.0: {}
+
+  is-number@7.0.0: {}
+
+  is-obj@2.0.0: {}
+
+  is-plain-obj@1.1.0: {}
+
+  is-plain-obj@4.1.0: {}
+
+  is-text-path@1.0.1:
+    dependencies:
+      text-extensions: 1.9.0
+
+  is-unicode-supported@0.1.0: {}
+
+  is-unicode-supported@1.3.0: {}
+
+  is-utf8@0.2.1: {}
+
+  is-windows@1.0.2: {}
+
+  isarray@1.0.0: {}
+
   isexe@2.0.0: {}
 
+  jiti@2.6.1: {}
+
+  js-tokens@4.0.0: {}
+
   js-yaml@4.3.2:
     dependencies:
       argparse: 2.0.1
 
   json-buffer@3.0.1: {}
 
+  json-parse-better-errors@1.0.2: {}
+
+  json-parse-even-better-errors@2.3.1: {}
+
   json-schema-traverse@0.4.1: {}
 
+  json-schema-traverse@1.0.0: {}
+
   json-stable-stringify-without-jsonify@1.0.1: {}
 
+  json-stringify-safe@5.0.1: {}
+
+  jsonfile@6.2.1:
+    dependencies:
+      universalify: 2.0.1
+    optionalDependencies:
+      graceful-fs: 4.2.11
+
+  jsonparse@1.3.1: {}
+
   keyv@4.5.4:
     dependencies:
       json-buffer: 3.0.1
 
+  kind-of@6.0.3: {}
+
   levn@0.4.1:
     dependencies:
       prelude-ls: 1.2.1
@@ -1411,16 +3767,87 @@ snapshots:
       lightningcss-win32-arm64-msvc: 1.33.0
       lightningcss-win32-x64-msvc: 1.33.0
 
+  lines-and-columns@1.2.4: {}
+
+  load-json-file@4.0.0:
+    dependencies:
+      graceful-fs: 4.2.11
+      parse-json: 4.0.0
+      pify: 3.0.0
+      strip-bom: 3.0.0
+
+  locate-path@2.0.0:
+    dependencies:
+      p-locate: 2.0.0
+      path-exists: 3.0.0
+
+  locate-path@5.0.0:
+    dependencies:
+      p-locate: 4.1.0
+
   locate-path@6.0.0:
     dependencies:
       p-locate: 5.0.0
 
+  lodash.ismatch@4.4.0: {}
+
+  lodash.map@4.6.0: {}
+
   lodash.merge@4.6.2: {}
 
+  lodash@4.18.1: {}
+
+  log-symbols@4.1.0:
+    dependencies:
+      chalk: 4.1.2
+      is-unicode-supported: 0.1.0
+
+  log-symbols@5.1.0:
+    dependencies:
+      chalk: 5.6.2
+      is-unicode-supported: 1.3.0
+
+  longest@2.0.1: {}
+
+  lru-cache@6.0.0:
+    dependencies:
+      yallist: 4.0.0
+
   magic-string@0.30.21:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.6.0
 
+  map-obj@1.0.1: {}
+
+  map-obj@4.3.0: {}
+
+  meow@8.1.2:
+    dependencies:
+      '@types/minimist': 1.2.5
+      camelcase-keys: 6.2.2
+      decamelize-keys: 1.1.1
+      hard-rejection: 2.1.0
+      minimist-options: 4.1.0
+      normalize-package-data: 3.0.3
+      read-pkg-up: 7.0.1
+      redent: 3.0.0
+      trim-newlines: 3.0.1
+      type-fest: 0.18.1
+      yargs-parser: 20.2.9
+
+  merge@2.1.1: {}
+
+  micromatch@4.0.8:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.2
+
+  mimic-fn@1.2.0: {}
+
+  mimic-fn@2.1.0: {}
+
+  min-indent@1.0.1: {}
+
   minimatch@10.2.6:
     dependencies:
       brace-expansion: 5.0.12
@@ -1429,14 +3856,62 @@ snapshots:
     dependencies:
       brace-expansion: 1.1.21
 
+  minimist-options@4.1.0:
+    dependencies:
+      arrify: 1.0.1
+      is-plain-obj: 1.1.0
+      kind-of: 6.0.3
+
+  minimist@1.2.8: {}
+
+  mkdirp@0.5.6:
+    dependencies:
+      minimist: 1.2.8
+
+  modify-values@1.0.1: {}
+
   ms@2.1.3: {}
 
+  mute-stream@0.0.7: {}
+
+  mute-stream@0.0.8: {}
+
+  mute-stream@3.0.0: {}
+
   nanoid@3.3.19: {}
 
   natural-compare@1.4.0: {}
 
+  neo-async@2.6.2: {}
+
+  normalize-package-data@2.5.0:
+    dependencies:
+      hosted-git-info: 2.8.9
+      resolve: 1.22.12
+      semver: 5.7.2
+      validate-npm-package-license: 3.0.4
+
+  normalize-package-data@3.0.3:
+    dependencies:
+      hosted-git-info: 4.1.0
+      is-core-module: 2.17.0
+      semver: 7.8.5
+      validate-npm-package-license: 3.0.4
+
   obug@2.2.1: {}
 
+  once@1.4.0:
+    dependencies:
+      wrappy: 1.0.2
+
+  onetime@2.0.1:
+    dependencies:
+      mimic-fn: 1.2.0
+
+  onetime@5.1.2:
+    dependencies:
+      mimic-fn: 2.1.0
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -1446,28 +3921,106 @@ snapshots:
       type-check: 0.4.0
       word-wrap: 1.2.5
 
+  ora@5.4.1:
+    dependencies:
+      bl: 4.1.0
+      chalk: 4.1.2
+      cli-cursor: 3.1.0
+      cli-spinners: 2.9.2
+      is-interactive: 1.0.0
+      is-unicode-supported: 0.1.0
+      log-symbols: 4.1.0
+      strip-ansi: 6.0.1
+      wcwidth: 1.0.1
+
+  ora@6.3.1:
+    dependencies:
+      chalk: 5.6.2
+      cli-cursor: 4.0.0
+      cli-spinners: 2.9.2
+      is-interactive: 2.0.0
+      is-unicode-supported: 1.3.0
+      log-symbols: 5.1.0
+      stdin-discarder: 0.1.0
+      strip-ansi: 7.2.0
+      wcwidth: 1.0.1
+
+  os-homedir@1.0.2: {}
+
+  os-tmpdir@1.0.2: {}
+
+  p-limit@1.3.0:
+    dependencies:
+      p-try: 1.0.0
+
+  p-limit@2.3.0:
+    dependencies:
+      p-try: 2.2.0
+
   p-limit@3.1.0:
     dependencies:
       yocto-queue: 0.1.0
 
+  p-locate@2.0.0:
+    dependencies:
+      p-limit: 1.3.0
+
+  p-locate@4.1.0:
+    dependencies:
+      p-limit: 2.3.0
+
   p-locate@5.0.0:
     dependencies:
       p-limit: 3.1.0
 
+  p-try@1.0.0: {}
+
+  p-try@2.2.0: {}
+
   parent-module@1.0.1:
     dependencies:
       callsites: 3.1.0
 
+  parse-json@4.0.0:
+    dependencies:
+      error-ex: 1.3.4
+      json-parse-better-errors: 1.0.2
+
+  parse-json@5.2.0:
+    dependencies:
+      '@babel/code-frame': 7.29.7
+      error-ex: 1.3.4
+      json-parse-even-better-errors: 2.3.1
+      lines-and-columns: 1.2.4
+
+  parse-passwd@1.0.0: {}
+
+  path-exists@3.0.0: {}
+
   path-exists@4.0.0: {}
 
+  path-is-absolute@1.0.1: {}
+
   path-key@3.1.1: {}
 
+  path-parse@1.0.7: {}
+
+  path-type@3.0.0:
+    dependencies:
+      pify: 3.0.0
+
   pathe@2.0.3: {}
 
   picocolors@1.1.1: {}
 
+  picomatch@2.3.2: {}
+
   picomatch@4.0.7: {}
 
+  pify@2.3.0: {}
+
+  pify@3.0.0: {}
+
   postcss@8.5.28:
     dependencies:
       nanoid: 3.3.19
@@ -1476,10 +4029,98 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
+  process-nextick-args@2.0.1: {}
+
   punycode@2.3.1: {}
 
+  q@1.5.1: {}
+
+  quick-lru@4.0.1: {}
+
+  read-pkg-up@3.0.0:
+    dependencies:
+      find-up: 2.1.0
+      read-pkg: 3.0.0
+
+  read-pkg-up@7.0.1:
+    dependencies:
+      find-up: 4.1.0
+      read-pkg: 5.2.0
+      type-fest: 0.8.1
+
+  read-pkg@3.0.0:
+    dependencies:
+      load-json-file: 4.0.0
+      normalize-package-data: 2.5.0
+      path-type: 3.0.0
+
+  read-pkg@5.2.0:
+    dependencies:
+      '@types/normalize-package-data': 2.4.4
+      normalize-package-data: 2.5.0
+      parse-json: 5.2.0
+      type-fest: 0.6.0
+
+  readable-stream@2.3.8:
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+
+  readable-stream@3.6.2:
+    dependencies:
+      inherits: 2.0.4
+      string_decoder: 1.3.0
+      util-deprecate: 1.0.2
+
+  redent@3.0.0:
+    dependencies:
+      indent-string: 4.0.0
+      strip-indent: 3.0.0
+
+  require-directory@2.1.1: {}
+
+  require-from-string@2.0.2: {}
+
+  resolve-dir@1.0.1:
+    dependencies:
+      expand-tilde: 2.0.2
+      global-modules: 1.0.0
+
   resolve-from@4.0.0: {}
 
+  resolve-from@5.0.0: {}
+
+  resolve@1.22.12:
+    dependencies:
+      es-errors: 1.3.0
+      is-core-module: 2.17.0
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+
+  restore-cursor@2.0.0:
+    dependencies:
+      onetime: 2.0.1
+      signal-exit: 3.0.7
+
+  restore-cursor@3.1.0:
+    dependencies:
+      onetime: 5.1.2
+      signal-exit: 3.0.7
+
+  restore-cursor@4.0.0:
+    dependencies:
+      onetime: 5.1.2
+      signal-exit: 3.0.7
+
+  rimraf@2.6.3:
+    dependencies:
+      glob: 7.2.3
+
   rolldown@1.2.9:
     dependencies:
       '@oxc-project/types': 0.150.0
@@ -1501,6 +4142,28 @@ snapshots:
       '@rolldown/binding-win32-arm64-msvc': 1.2.9
       '@rolldown/binding-win32-x64-msvc': 1.2.9
 
+  run-async@2.4.1: {}
+
+  run-async@4.0.6: {}
+
+  rxjs@6.6.7:
+    dependencies:
+      tslib: 1.14.1
+
+  rxjs@7.8.2:
+    dependencies:
+      tslib: 2.8.1
+
+  safe-buffer@5.1.2: {}
+
+  safe-buffer@5.2.1: {}
+
+  safer-buffer@2.1.2: {}
+
+  semver@5.7.2: {}
+
+  semver@6.3.1: {}
+
   semver@7.8.5: {}
 
   shebang-command@2.0.0:
@@ -1511,18 +4174,128 @@ snapshots:
 
   siginfo@2.0.0: {}
 
+  signal-exit@3.0.7: {}
+
+  signal-exit@4.1.0: {}
+
   source-map-js@1.2.1: {}
 
+  source-map@0.6.1: {}
+
+  spdx-correct@3.2.0:
+    dependencies:
+      spdx-expression-parse: 3.0.1
+      spdx-license-ids: 3.0.24
+
+  spdx-exceptions@2.5.0: {}
+
+  spdx-expression-parse@3.0.1:
+    dependencies:
+      spdx-exceptions: 2.5.0
+      spdx-license-ids: 3.0.24
+
+  spdx-license-ids@3.0.24: {}
+
+  split2@3.2.2:
+    dependencies:
+      readable-stream: 3.6.2
+
+  split@1.0.1:
+    dependencies:
+      through: 2.3.8
+
   stackback@0.0.2: {}
 
   std-env@4.2.0: {}
 
+  stdin-discarder@0.1.0:
+    dependencies:
+      bl: 5.1.0
+
+  string-width@2.1.1:
+    dependencies:
+      is-fullwidth-code-point: 2.0.0
+      strip-ansi: 4.0.0
+
+  string-width@4.2.3:
+    dependencies:
+      emoji-regex: 8.0.0
+      is-fullwidth-code-point: 3.0.0
+      strip-ansi: 6.0.1
+
+  string-width@7.2.0:
+    dependencies:
+      emoji-regex: 10.6.0
+      get-east-asian-width: 1.7.0
+      strip-ansi: 7.2.0
+
+  string-width@8.2.2:
+    dependencies:
+      get-east-asian-width: 1.7.0
+      strip-ansi: 7.2.0
+
+  string_decoder@1.1.1:
+    dependencies:
+      safe-buffer: 5.1.2
+
+  string_decoder@1.3.0:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  strip-ansi@4.0.0:
+    dependencies:
+      ansi-regex: 3.0.1
+
+  strip-ansi@5.2.0:
+    dependencies:
+      ansi-regex: 4.1.1
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-ansi@7.2.0:
+    dependencies:
+      ansi-regex: 6.3.0
+
+  strip-bom@3.0.0: {}
+
+  strip-bom@4.0.0: {}
+
+  strip-indent@3.0.0:
+    dependencies:
+      min-indent: 1.0.1
+
   strip-json-comments@3.1.1: {}
 
+  supports-color@5.5.0:
+    dependencies:
+      has-flag: 3.0.0
+
   supports-color@7.2.0:
     dependencies:
       has-flag: 4.0.0
 
+  supports-preserve-symlinks-flag@1.0.0: {}
+
+  temp@0.9.4:
+    dependencies:
+      mkdirp: 0.5.6
+      rimraf: 2.6.3
+
+  text-extensions@1.9.0: {}
+
+  through2@2.0.5:
+    dependencies:
+      readable-stream: 2.3.8
+      xtend: 4.0.2
+
+  through2@4.0.2:
+    dependencies:
+      readable-stream: 3.6.2
+
+  through@2.3.8: {}
+
   tinybench@2.9.0: {}
 
   tinyexec@1.3.1: {}
@@ -1534,32 +4307,72 @@ snapshots:
 
   tinyrainbow@3.1.1: {}
 
+  tmp@0.0.33:
+    dependencies:
+      os-tmpdir: 1.0.2
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  trim-newlines@3.0.1: {}
+
   ts-api-utils@2.5.0(typescript@5.9.3):
     dependencies:
       typescript: 5.9.3
 
+  tslib@1.14.1: {}
+
+  tslib@2.8.1: {}
+
   type-check@0.4.0:
     dependencies:
       prelude-ls: 1.2.1
 
-  typescript-eslint@8.70.1(eslint@9.39.5)(typescript@5.9.3):
+  type-fest@0.18.1: {}
+
+  type-fest@0.21.3: {}
+
+  type-fest@0.6.0: {}
+
+  type-fest@0.8.1: {}
+
+  typescript-eslint@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)
-      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
+      '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
       '@typescript-eslint/typescript-estree': 8.70.1(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5)(typescript@5.9.3)
-      eslint: 9.39.5
+      '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.6.1))(typescript@5.9.3)
+      eslint: 9.39.5(jiti@2.6.1)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
   typescript@5.9.3: {}
 
+  uglify-js@3.19.3:
+    optional: true
+
+  undici-types@8.9.0: {}
+
+  universalify@2.0.1: {}
+
   uri-js@4.4.1:
     dependencies:
       punycode: 2.3.1
 
-  vite@8.3.0:
+  user-home@2.0.0:
+    dependencies:
+      os-homedir: 1.0.2
+
+  util-deprecate@1.0.2: {}
+
+  validate-npm-package-license@3.0.4:
+    dependencies:
+      spdx-correct: 3.2.0
+      spdx-expression-parse: 3.0.1
+
+  vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1):
     dependencies:
       lightningcss: 1.33.0
       picomatch: 4.0.7
@@ -1567,12 +4380,14 @@ snapshots:
       rolldown: 1.2.9
       tinyglobby: 0.2.17
     optionalDependencies:
+      '@types/node': 26.6.2
       fsevents: 2.3.3
+      jiti: 2.6.1
 
-  vitest@4.1.11(vite@8.3.0):
+  vitest@4.1.11(@types/node@26.6.2)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)):
     dependencies:
       '@vitest/expect': 4.1.11
-      '@vitest/mocker': 4.1.11(vite@8.3.0)
+      '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1))
       '@vitest/pretty-format': 4.1.11
       '@vitest/runner': 4.1.11
       '@vitest/snapshot': 4.1.11
@@ -1589,11 +4404,21 @@ snapshots:
       tinyexec: 1.3.1
       tinyglobby: 0.2.17
       tinyrainbow: 3.1.1
-      vite: 8.3.0
+      vite: 8.3.0(@types/node@26.6.2)(jiti@2.6.1)
       why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/node': 26.6.2
     transitivePeerDependencies:
       - msw
 
+  wcwidth@1.0.1:
+    dependencies:
+      defaults: 1.0.4
+
+  which@1.3.1:
+    dependencies:
+      isexe: 2.0.0
+
   which@2.0.2:
     dependencies:
       isexe: 2.0.0
@@ -1605,4 +4430,55 @@ snapshots:
 
   word-wrap@1.2.5: {}
 
+  wordwrap@1.0.0: {}
+
+  wrap-ansi@6.2.0:
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+
+  wrap-ansi@7.0.0:
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+
+  wrap-ansi@9.0.2:
+    dependencies:
+      ansi-styles: 6.2.3
+      string-width: 7.2.0
+      strip-ansi: 7.2.0
+
+  wrappy@1.0.2: {}
+
+  xtend@4.0.2: {}
+
+  y18n@5.0.8: {}
+
+  yallist@4.0.0: {}
+
+  yargs-parser@20.2.9: {}
+
+  yargs-parser@22.0.0: {}
+
+  yargs@16.2.2:
+    dependencies:
+      cliui: 7.0.4
+      escalade: 3.2.0
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      string-width: 4.2.3
+      y18n: 5.0.8
+      yargs-parser: 20.2.9
+
+  yargs@18.2.0:
+    dependencies:
+      cliui: 9.0.1
+      escalade: 3.2.0
+      get-caller-file: 2.0.5
+      string-width: 8.2.2
+      y18n: 5.0.8
+      yargs-parser: 22.0.0
+
   yocto-queue@0.1.0: {}

From d2b565ac91254442d876d6a3779ff767d6dde5ff Mon Sep 17 00:00:00 2001
From: PiEgg 
Date: Wed, 23 Sep 2026 19:54:51 +0800
Subject: [PATCH 8/8] :pencil: Docs: remove release guide

---
 .github/RELEASING.md | 84 --------------------------------------------
 1 file changed, 84 deletions(-)
 delete mode 100644 .github/RELEASING.md

diff --git a/.github/RELEASING.md b/.github/RELEASING.md
deleted file mode 100644
index 72f1b50..0000000
--- a/.github/RELEASING.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Publishing @picgo/cloud-sdk
-
-This document is for package maintainers. Application integration is documented in the root README.
-
-## First publication
-
-The npm package must exist before configuring its trusted publisher. If `@picgo/cloud-sdk` has not been published yet, a maintainer with publish access to the npm `@picgo` organization can publish the first version locally. A manually created access token is not required: interactive `npm login` and account 2FA work. If the first publication must run non-interactively, use an appropriately scoped granular access token with publishing access and bypass 2FA enabled, then remove that credential after setting up OIDC.
-
-Make the GitHub repository public and merge the SDK and publishing workflow into `main` before releasing. From a clean checkout of `main`, using Node.js 24:
-
-```sh
-pnpm install --frozen-lockfile
-npm login --registry=https://registry.npmjs.org
-npm publish --access public
-```
-
-The existing `prepublishOnly` script runs the full checks and build before this manual publication. Complete npm's 2FA prompt. This creates the package; it does not need a GitHub secret or a bootstrap workflow. Do not push a release tag for this already-published version to trigger another publication of the same version. Local publication does not carry the GitHub Actions provenance that subsequent OIDC releases will provide.
-
-## Configure the npm trusted publisher
-
-Open the npm package settings for `@picgo/cloud-sdk`, add a GitHub Actions trusted publisher, and use these exact values:
-
-| Field | Value |
-| --- | --- |
-| Organization or user | `PicGo` |
-| Repository | `picgo-cloud-sdk` |
-| Workflow filename | `publish.yml` |
-| Environment name | Leave blank; this workflow does not specify one |
-| Allowed actions | Enable direct publishing with `npm publish` |
-
-The workflow filename must include the extension but not `.github/workflows/`. Current npm configurations default to allowing staged publishing; explicitly enable direct `npm publish` for this workflow. No `NPM_TOKEN` or `NODE_AUTH_TOKEN` secret is needed. The publish job requests `id-token: write` and runs on a GitHub-hosted runner. It installs npm 11, which supports OIDC (minimum supported version: 11.5.1).
-
-After verifying the first OIDC publication, remove unused publishing tokens. You may also enable npm's “Require two-factor authentication and disallow tokens” setting; trusted publishing continues to work with that setting.
-
-## Subsequent releases
-
-This repository uses `@picgo/bump-version`, matching PicGo-Core. Use `pnpm cz` to write commits in the PicGo format; the commit-message hook validates them. For example:
-
-```text
-:sparkles: Feature(upload): add resumable uploads
-:bug: Fix(media): preserve the current media URL
-```
-
-The changelog generator recognizes this convention. Earlier Angular-style commits are not automatically converted. Use a PicGo-format title when squash-merging a feature PR so its change appears in the changelog.
-
-Preview the next release without changing files, creating a commit, or tagging:
-
-```sh
-pnpm release:dry
-pnpm release:dry --type minor
-```
-
-From a clean, up-to-date `main`, run `pnpm release` to update the version, generate `CHANGELOG.md`, create a release commit, and create the annotated `v` tag. For example, when the current version is `0.1.0`:
-
-```sh
-git switch main
-git pull --ff-only
-pnpm release --type patch
-git push origin main
-git push origin v0.1.1
-```
-
-Use `pnpm release --version 0.2.0-beta.1` for an exact prerelease version, or add `--yes` to skip the version confirmation. Review the resulting version and changelog before pushing. The release command prepares the Git release; the tag push triggers npm publication. Do not use bump-version's `--push` here: version 3.0.0 hardcodes `origin master`, while this repository uses `main`.
-
-If changes to `main` require a pull request, run `pnpm release --no-tag` on a release branch, merge the version/changelog commit, and create the corresponding tag on the merged `main` commit. The tag must point to the commit that contains the published version.
-
-Pushing a `v*` tag starts `.github/workflows/publish.yml`. It checks that the repository is public, the tag exactly matches `v`, and the tagged commit is already in `main`. It then installs locked dependencies, runs type checking, lint, tests, and the build, and packs the result. A separate job publishes that verified tarball using OIDC and provenance, without rerunning package scripts in the publishing job.
-
-Stable versions publish to the `latest` dist-tag. Prerelease versions such as `0.2.0-beta.1` publish to `next`. Ordinary pushes and pull requests run CI but do not publish. Existing npm versions cannot be overwritten; increment the version for another release. To retry a transient failure before the package was published, rerun the failed workflow in GitHub Actions.
-
-## Local validation without publishing
-
-```sh
-pnpm check
-pnpm pack --pack-destination .release
-npm publish .release/picgo-cloud-sdk-0.1.0.tgz --dry-run --ignore-scripts --access public
-```
-
-Replace the tarball version with the version being tested. These commands build and inspect the package without publishing it. The package allowlist contains only `dist`, the two READMEs, `CHANGELOG.md` once generated, and the license, plus npm's required package metadata; local `.env` files, examples, and maintenance scripts are not shipped.
-
-## References
-
-- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers)
-- [Publishing scoped public packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)