diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fd450d4 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Backend used by the local browser playground (pnpm dev). +PICGO_API_URL=https://api.picgo.app +# Optional local port. +PICGO_DEV_PORT=5175 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/.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 new file mode 100644 index 0000000..beceba9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +coverage/ +.release/ +*.tgz +.DS_Store +.env* +!*.example 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/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..f29ecea --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# @picgo/cloud-sdk + +[中文文档](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 +``` + +```ts +import { PicGoCloudClient } from '@picgo/cloud-sdk' + +const client = new PicGoCloudClient({ + token: userToken, +}) + +const media = await client.upload(file) +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' }) +await client.media.delete(media.id) +``` + +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', // 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 +}) +``` + +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, { + onProgress({ phase, loaded, total, fraction, resumed }) { + console.log(phase, loaded, total, fraction, resumed) + }, +}) + +// A Blob has no filename, so provide one explicitly. +await client.upload(blob, { filename: 'photo.png', contentType: 'image/png' }) +``` + +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 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 + +```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) // Call cancel() again to retry cleanup after a network failure + } +}) + +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()` 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; use `cancel()` to abandon the upload. An already-aborted external signal cannot be reused to restart an upload; create a new task. + +### Resumable uploads + +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. + +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. + +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 + +Use `client.media` to browse and manage the media in the user's PicGo Cloud account. + +| Method | Returns | +| --- | --- | +| `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` | +| `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 }` | + +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: 'Screenshot' } }, +]) +await client.media.deleteMany([firstId, secondId]) +``` + +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 + +```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` 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. + +## Try the example locally + +Clone this repository and use Node.js 22.18+ (Node.js 24 recommended). Copy `.env.example` to `.env`: + +```dotenv +PICGO_API_URL=https://api.picgo.app +PICGO_DEV_PORT=5175 +``` + +```sh +pnpm install +pnpm dev +``` + +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. + +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 new file mode 100644 index 0000000..66b58c9 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,189 @@ +# @picgo/cloud-sdk + +[English](README.md) + +面向现代浏览器的 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.url) + +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', // 默认 PicGo Cloud API 地址 + 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' }) +``` + +SDK 会自动选择上传方式,大于等于 10 MiB 的文件支持分片上传。默认并发 3,可通过 `concurrency` 设置为 1–6。支持的文件大小为 1 byte–1 GiB,实际可上传的格式和大小还取决于你的 PicGo Cloud 套餐及剩余配额。文件不会被转换、压缩或重新编码。可通过 `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()` 停止上传并释放未完成任务的资源,失败时可以再次调用。取消后需新建任务。取消不会删除已经上传成功的媒体,删除请使用 `client.media.delete()`。 + +`task.status` 可读取 `idle`、`running`、`paused`、`cancelled`、`failed`、`completed`。如应用已有 AbortController,可选传入 `signal`。外部 signal 中断会保留续传信息;要放弃上传,请使用 `cancel()`。已经中断的外部 signal 不能用于重新开始,应创建新任务。 + +### 断点续传 + +分片上传会在当前站点的 localStorage 中保存最多 24 小时的恢复信息。刷新页面后重新选择同一文件,再调用 `upload()` 或 `createUpload().start()`,即可自动恢复。恢复记录按账户和文件隔离,不包含 token 或文件内容。续传时请保持同一账户和 API 地址。 + +恢复仅适用于同一站点、同一浏览器中仍有效的上传记录,不支持跨站点或跨设备恢复。浏览器存储不可用时,页面保持打开期间仍可暂停和继续当前任务。在客户端设置 `storage: false` 可关闭持久化,在上传选项中设置 `resume: false` 可忽略已有记录。小于 10 MiB 的文件若在传输过程中暂停,继续时会重新传输。请避免在多个标签页同时上传同一文件。 + +临时上传失败会在安全的情况下自动重试。任务失败后,可以捕获错误并再次调用 `start()` 重试。媒体更新和删除不会自动重试。 + +## 媒体管理 + +使用 `client.media` 浏览和管理用户 PicGo Cloud 账户中的媒体。 + +| 方法 | 返回值 | +| --- | --- | +| `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` | +| `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]) +``` + +上传和媒体查询返回 `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 不提供恢复操作。 + +## 错误处理 + +```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`。请根据 `kind`、`status`、`code` 处理错误,不要匹配错误文案。401 通常表示需要用户提供有效的 token。遇到网络错误时,检查网络连接和配置的 API 地址。SDK 不会自动清除用户 token。 + +## 本地试用示例 + +克隆本仓库,使用 Node.js 22.18+,推荐 Node.js 24。将 `.env.example` 复制为 `.env`: + +```dotenv +PICGO_API_URL=https://api.picgo.app +PICGO_DEV_PORT=5175 +``` + +```sh +pnpm install +pnpm dev +``` + +打开 `http://localhost:5175`,输入你的 PicGo Cloud token。示例页支持验证 token、上传、暂停/继续/取消、媒体列表、详情、重命名和删除。上传成功后会自动填写媒体 ID,方便继续操作。这些操作会作用于 token 所属账户的媒体数据。 + +建议先验证 token,再分别上传小于 10 MiB 和大于等于 10 MiB 的文件。对分片上传测试暂停与继续,再刷新页面、重新选择同一文件测试恢复。通过媒体操作按钮验证上传后的条目能查询、重命名和删除。网速太快不方便中断时,可在浏览器开发者工具中启用网络限速。 + +可以修改 `PICGO_API_URL` 来测试其他 PicGo Cloud API 地址,修改 `.env` 后需重启 `pnpm dev`。已有的 shell 环境变量优先于 `.env`。修改源码后会自动重新构建,刷新页面即可加载。`.env` 已被 Git 忽略,token 仅保留在页面内存中。这些环境设置只作用于示例页;在自己的应用中覆盖默认 API 地址时,请向 `PicGoCloudClient` 传入 `baseUrl`。 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..e1cb997 --- /dev/null +++ b/examples/basic.html @@ -0,0 +1,71 @@ + + + + +PicGo Cloud SDK 示例 + +

PicGo Cloud SDK

+

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

+ + + + + + + + +

+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..280f7ad
--- /dev/null
+++ b/package.json
@@ -0,0 +1,52 @@
+{
+  "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" },
+  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
+  "sideEffects": false,
+  "files": ["dist", "README.md", "README_ZH.md", "CHANGELOG.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": {
+    "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 && 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",
+    "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",
+    "vitest": "~4.1.10"
+  }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..05b0c9f
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,4484 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+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(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
+      typescript:
+        specifier: ^5.9.3
+        version: 5.9.3
+      typescript-eslint:
+        specifier: ^8.49.0
+        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(@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}
+    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'}
+
+  '@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}
+    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==}
+
+  '@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==}
+
+  '@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==}
+
+  '@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}
+    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==}
+
+  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:
+      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
+
+  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==}
+
+  balanced-match@4.0.4:
+    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==}
+
+  brace-expansion@5.0.12:
+    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'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      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'}
+
+  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'}
+
+  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==}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  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'}
+
+  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'}
+
+  globals@16.5.0:
+    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'}
+
+  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'}
+
+  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
+
+  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'}
+
+  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'}
+
+  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}
+
+  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}
+    hasBin: true
+
+  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}
+
+  prelude-ls@1.2.1:
+    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'}
+    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==}
+
+  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==}
+
+  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'}
+
+  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}
+    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
+
+  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}
+    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
+
+  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'}
+    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'}
+
+  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:
+
+  '@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:
+      '@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': {}
+
+  '@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': {}
+
+  '@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
+
+  '@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
+
+  '@inquirer/figures@2.0.9': {}
+
+  '@inquirer/input@5.1.6(@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/number@4.2.3(@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/password@5.2.2(@types/node@26.6.2)':
+    dependencies:
+      '@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
+
+  '@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
+
+  '@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
+
+  '@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
+
+  '@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': {}
+
+  '@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':
+    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': {}
+
+  '@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(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(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(jiti@2.6.1)
+      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(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(jiti@2.6.1)
+      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(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(jiti@2.6.1))(typescript@5.9.3)
+      debug: 4.4.3
+      eslint: 9.39.5(jiti@2.6.1)
+      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(jiti@2.6.1))(typescript@5.9.3)':
+    dependencies:
+      '@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(jiti@2.6.1)
+      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(@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(@types/node@26.6.2)(jiti@2.6.1)
+
+  '@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
+
+  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
+      fast-json-stable-stringify: 2.1.0
+      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
+      concat-map: 0.0.1
+
+  brace-expansion@5.0.12:
+    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:
+    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(jiti@2.6.1):
+    dependencies:
+      '@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
+      '@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
+    optionalDependencies:
+      jiti: 2.6.1
+    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: {}
+
+  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
+      keyv: 4.5.4
+
+  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: {}
+
+  import-fresh@3.3.1:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  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
+      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
+
+  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
+
+  minimatch@3.1.5:
+    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
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      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
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  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
+      '@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
+
+  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:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  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: {}
+
+  tinyglobby@0.2.17:
+    dependencies:
+      fdir: 6.5.0(picomatch@4.0.7)
+      picomatch: 4.0.7
+
+  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
+
+  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(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(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
+
+  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
+      postcss: 8.5.28
+      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(@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(@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
+      '@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(@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
+
+  why-is-node-running@2.3.0:
+    dependencies:
+      siginfo: 2.0.0
+      stackback: 0.0.2
+
+  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: {}
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/scripts/dev-server.mjs b/scripts/dev-server.mjs
new file mode 100644
index 0000000..12f6482
--- /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://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..f34ad24
--- /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://preview.example.com/', PICGO_DEV_PORT: '5176', SECRET: 'private' }), {
+    apiUrl: 'https://preview.example.com', port: 5176,
+  })
+  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 }))
+  }
+  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)
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' } } }))
+})
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-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
new file mode 100644
index 0000000..ea0ab11
--- /dev/null
+++ b/src/media.ts
@@ -0,0 +1,152 @@
+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,
+  MediaBulkUpdateResult,
+  MediaDeleteResult,
+  MediaFilters,
+  MediaItem,
+  MediaListQuery,
+  MediaListResult,
+  MediaStats,
+  MediaUpdate,
+  RequestOptions,
+} from './types.js'
+
+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 {
+  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()
+    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 {
+    validateMediaId(id)
+    const result = await this.http.request(
+      `/api/album-items/${encodeURIComponent(id)}`,
+      { signal: options.signal },
+    )
+    return normalizeMediaItem(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 normalizeMediaItem(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)
+    }
+    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 {
+    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..8457141
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,101 @@
+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
+  /** 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
+  contentType?: string
+  size?: number
+  width?: number
+  height?: number
+  extname?: string
+  createdAt?: number
+  updatedAt?: number
+  originImgUrl?: 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..aeb805e
--- /dev/null
+++ b/src/upload/task.ts
@@ -0,0 +1,388 @@
+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'
+
+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: MediaItemResponse }
+    try {
+      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)
+    } catch (error) {
+      if (state.phase === 'uploaded' && error instanceof PicGoCloudError && error.status === 404) this.clear()
+      throw error
+    }
+    if (!result?.item) {
+      protocol('Invalid media completion response')
+    }
+    const item = normalizeMediaItem(result.item)
+    this.clear()
+    this.emit('completed', this.file.size)
+    return 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..e5b76c8 --- /dev/null +++ b/tests/browser/smoke.js @@ -0,0 +1,50 @@ +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') + 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) } + 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') + 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') + 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..f30b58f --- /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', url: 'https://img.test/a.png', 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/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/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..3526015 --- /dev/null +++ b/tests/upload.test.ts @@ -0,0 +1,807 @@ +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', url: 'https://cdn.example.test/media-1.png', 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.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') + 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'] } })