Prechádzať zdrojové kódy

fix(desktop): upload releases with Tencent COS SDK

winewill 1 mesiac pred
rodič
commit
e6c507d65f
27 zmenil súbory, kde vykonal 1061 pridanie a 400 odobranie
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md
  4. 6 0
      .agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.i18n.yaml
  5. 43 0
      .agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.md
  6. 43 0
      .agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.zh.md
  7. 1 1
      THIRD_PARTY_NOTICES.md
  8. 2 2
      apps/desktop/README.i18n.yaml
  9. 2 2
      apps/desktop/README.md
  10. 2 2
      apps/desktop/README.zh.md
  11. 1 1
      apps/desktop/package.json
  12. 44 0
      apps/desktop/scripts/cos-operation.ts
  13. 53 0
      apps/desktop/scripts/desktop-cos.ts
  14. 31 18
      apps/desktop/scripts/desktop-upload-run.ts
  15. 44 30
      apps/desktop/scripts/installed-update-cos.ts
  16. 4 4
      apps/desktop/scripts/installed-update-publication.ts
  17. 1 4
      apps/desktop/scripts/publish-installed-update.ts
  18. 5 15
      apps/desktop/scripts/upload-target.ts
  19. 110 0
      apps/desktop/tests/cos-loopback.ts
  20. 105 0
      apps/desktop/tests/cos-operation.spec.ts
  21. 128 86
      apps/desktop/tests/desktop-upload-run.spec.ts
  22. 169 79
      apps/desktop/tests/installed-update-cos.spec.ts
  23. 2 1
      apps/desktop/tests/installed-update-publication.spec.ts
  24. 2 2
      apps/desktop/tests/installed-update/publication/README.i18n.yaml
  25. 2 0
      apps/desktop/tests/installed-update/publication/README.md
  26. 2 0
      apps/desktop/tests/installed-update/publication/README.zh.md
  27. 255 149
      pnpm-lock.yaml

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md
-2026-08-25-electron-desktop-packaging-and-updates.md: f8825b652919147aef271accd5f4e521d7d5fc77
-2026-08-25-electron-desktop-packaging-and-updates.zh.md: 2eb42f74dd8574afdf1348bc6abd5f5af1b979b9
+2026-08-25-electron-desktop-packaging-and-updates.md: 7d5463440acbf63b8a84df145a6501ee3396a954
+2026-08-25-electron-desktop-packaging-and-updates.zh.md: 5603b017bbec8e938d8ab31cc109af5a1e55829a

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md

@@ -107,7 +107,7 @@ Windows updater verification pins the public release certificate's `CN`, `O`, an
 
 ## Implementation
 
-The release [upload runner](../../../../apps/desktop/scripts/desktop-upload-run.ts) requires durable local evidence for both deployments. It saves the validated destination, artifact hashes and feed bytes before sending requests, flushes intent before each PUT and the available response metadata afterward, and stops on upload or evidence failure. SDK retries are disabled because an uncertain write must remain one inspectable attempt rather than silently repeat a mutable feed update. Terminal output alone cannot establish which operation survived a disconnected terminal; raw SDK errors are excluded because they can expose signed request data. Records survive independently of artifact cleanup, but local storage is not a remote audit service and an absent final result cannot establish remote failure. Upload receipts do not certify CDN propagation. Release operators retain the records and independently verify public bytes.
+The release [upload runner](../../../../apps/desktop/scripts/desktop-upload-run.ts) requires durable local evidence for both deployments. It saves the validated destination, artifact hashes and feed bytes before sending requests, flushes intent before each PUT and the available response metadata afterward, and stops on upload or evidence failure. Every object is one streamed Tencent COS PUT carrying an explicit length and Content-MD5, so the SDK's retry path — which requires a non-stream body — never repeats an uncertain write; an uncertain write must remain one inspectable attempt rather than silently repeat a mutable feed update. Terminal output alone cannot establish which operation survived a disconnected terminal; raw SDK errors are excluded because they can expose signed request data. Records survive independently of artifact cleanup, but local storage is not a remote audit service and an absent final result cannot establish remote failure. Upload receipts do not certify CDN propagation. Release operators retain the records and independently verify public bytes.
 
 Windows packaging retains per-run redacted output and timestamped stage/signing events through the [supervisor](../../../../apps/desktop/scripts/packaging-run.mjs). The [hardware interlock](../../../../apps/desktop/scripts/windows-signing-state.mjs) records intent before invoking the command interpreter and survives failures or interrupted runs across processes under one account. A fatal notification terminates the owned stage tree; a failed run never promotes a release record. Successful signatures alone release the interlock. Operator-approved recovery is explicit because a process-local rejected promise cannot protect the token after restarting a build. These records establish application-level operations, not the CSP's internal PIN-attempt count. Tests use fake signing and real isolated process trees, never the release token.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md

@@ -107,7 +107,7 @@ Windows updater 在 `win.signtoolOptions.publisherName` 中固定公开发布证
 
 ## 实现
 
-发布[上传执行器](../../../../apps/desktop/scripts/desktop-upload-run.ts)要求两种部署环境都保留持久化本地证据。它在发送请求前保存已验证目标、产物哈希和 feed 字节,每次 PUT 前刷盘意图、之后刷盘可用响应元数据,上传或证据写入失败即停止。SDK 禁用重试,因为结果不确定的写入必须保留为一次可检查的尝试,不能静默重复更新可变 feed。终端断连后,仅有终端输出无法确定哪次操作已完成;原始 SDK 错误可能暴露签名请求数据,因此不予记录。记录独立于产物清理保留,但本地存储不是远端审计服务,缺少最终结果不能证明远端失败。上传回执不认证 CDN 传播。发布者保留记录并独立验证公网字节。
+发布[上传执行器](../../../../apps/desktop/scripts/desktop-upload-run.ts)要求两种部署环境都保留持久化本地证据。它在发送请求前保存已验证目标、产物哈希和 feed 字节,每次 PUT 前刷盘意图、之后刷盘可用响应元数据,上传或证据写入失败即停止。每个对象都以一次流式腾讯 COS PUT 发送,并携带显式长度与 Content-MD5;SDK 的重试路径要求请求体不是流,因此不会重复结果不确定的写入;结果不确定的写入必须保留为一次可检查的尝试,不能静默重复更新可变 feed。终端断连后,仅有终端输出无法确定哪次操作已完成;原始 SDK 错误可能暴露签名请求数据,因此不予记录。记录独立于产物清理保留,但本地存储不是远端审计服务,缺少最终结果不能证明远端失败。上传回执不认证 CDN 传播。发布者保留记录并独立验证公网字节。
 
 Windows 打包通过[监督程序](../../../../apps/desktop/scripts/packaging-run.mjs)保留每次运行的脱敏输出和带时间戳的阶段/签名事件。[硬件锁定机制](../../../../apps/desktop/scripts/windows-signing-state.mjs)在调用命令解释器前记录意图,失败或运行中断后仍在同一账户的不同进程间保留。致命通知会终止所属阶段的进程树;失败运行绝不生成发布完成记录。只有签名成功才释放锁定状态。恢复必须由操作者明确批准,因为进程内的已拒绝 Promise 无法在重新启动构建后保护令牌。这些记录证明应用级操作,不代表 CSP 内部的 PIN 尝试次数。测试使用假签名和真实隔离进程树,绝不使用发布令牌。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.md
+2026-09-16-desktop-cos-upload-transport.md: 84504b32fd02d1fbaf8276b4f1baf7d85f7dcbd5
+2026-09-16-desktop-cos-upload-transport.zh.md: 2a2080f95b39ab433b4ccb3bec60e5ba61f52dc9

+ 43 - 0
.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.md

@@ -0,0 +1,43 @@
+# Agent Note: Upload Desktop releases through the Tencent COS SDK
+
+Status: implemented
+
+English | [中文](2026-09-16-desktop-cos-upload-transport.zh.md)
+
+## Problem
+
+Release objects were uploaded with the AWS S3 client pointed at the Tencent COS endpoint. That client's default checksum configuration can send a streamed request trailer under `Content-Encoding: aws-chunked`, a marker S3 removes before storing but the compatibility path can retain as object metadata. A user-visible application download failed with an HTTP/2 `RST_STREAM` immediately after response headers while an adjacent download on the same connection completed, and objects reported to carry the marker failed segmented downloads. Neither observation identifies an exclusive cause, but the upload transport is the part this repository owns, and a CDN serving metadata no origin request produced is a defect worth removing before chasing it further.
+
+## Decision
+
+[desktop-cos.ts](../../../../apps/desktop/scripts/desktop-cos.ts) builds every Desktop COS client from the official `cos-nodejs-sdk-v5` package: HTTPS, no keep-alive, no redirect following, no backup-host switching, no clock-offset correction, and a 900-second inactivity timeout. [upload-target.ts](../../../../apps/desktop/scripts/upload-target.ts) and [installed-update-cos.ts](../../../../apps/desktop/scripts/installed-update-cos.ts) obtain their client from that factory, and `@aws-sdk/client-s3` is no longer a Desktop dependency.
+
+Every uploaded object — binary, blockmap, and YAML feed, including the small string manifests — travels as one `putObject` whose body is a stream carrying an explicit `ContentLength` and a precomputed `Content-MD5`. The stream is what makes the write unrepeatable: the SDK repeats a request only while the body lacks `pipe`, so a streamed PUT is attempted once and neither uploader adds a retry of its own. The SDK also injects an empty `Cache-Control` header when the caller names none; the factory removes that header so the release uploader still leaves cache policy to deployment infrastructure.
+
+The qualification transport keeps the same store interface and namespace check as before. It reads objects through `getObject` with a `Writable` output that hashes received bytes, so an object is never held in memory, and a confirmed `NoSuchKey` is the only absence result; every other status, and a transfer that ends before its declared length, fails the operation.
+
+## Testing
+
+[cos-loopback.ts](../../../../apps/desktop/tests/cos-loopback.ts) redirects a real SDK instance's `before-send` URL to a per-test loopback origin that records the received bytes, so the transport's serialization — not a mock of it — is what the tests observe. [desktop-upload-run.spec.ts](../../../../apps/desktop/tests/desktop-upload-run.spec.ts) and [installed-update-cos.spec.ts](../../../../apps/desktop/tests/installed-update-cos.spec.ts) assert exact body bytes, `Content-Length`, `Content-MD5`, the absence of transfer and content encodings, the signed `x-cos-forbid-overwrite` header, one request per object on an HTTP 500 and on a dropped connection, read 404/403/truncation handling, and that retained records exclude credentials and raw server messages.
+
+[cos-operation.spec.ts](../../../../apps/desktop/tests/cos-operation.spec.ts) verifies total deadlines across retries, active response streams, cancellation of unconfirmed PUTs, and closure without interfering with another operation. Virtual deadline timers are combined with real socket and stream observations.
+
+## Alternatives considered
+
+**Keep the S3 client with `requestChecksumCalculation: WHEN_REQUIRED` on both paths.** The qualification transport already used that setting, and it suppresses the trailer. It still writes through a compatibility layer for a product whose vendor ships a supported client, and the release path would depend on a configuration staying correct rather than on an encoding never being produced. The official SDK removes the layer instead of tuning it.
+
+**Send a buffer body for the small YAML manifests.** A buffer is cheaper for a few hundred bytes and needs no stream handling. It also re-enables the SDK's fixed four-attempt retry, so an uncertain feed write could silently repeat a mutable object. Uniform streamed PUTs keep one write path with one guarantee.
+
+**Use the SDK's `uploadFile` queue or multipart upload.** Those paths parallelize large objects and add progress reporting. They also split one object into parts with per-part retries and upload state, widening the repeat surface for an artifact that must be written exactly once. The release uploader sends one request per object.
+
+**Replace only the release uploader and leave the qualification transport on S3.** The qualification transport writes to the same bucket family, and leaving it behind would keep the compatibility path alive for the objects most likely to be compared against production behavior.
+
+## Consequences
+
+Object writes cannot repeat, which is the property the incident needed, and the transport no longer depends on S3-compatibility behavior. In exchange, each object is one request: a large installer is not parallelized and has no progress reporting, matching the single-request behavior it replaces. Qualification version queries share a 30-second total deadline across all SDK attempts; object reads and PUTs have a 15-minute total deadline. Each operation owns its client and abort signal. [cos-operation.ts](../../../../apps/desktop/scripts/cos-operation.ts) passes that signal to native HTTP requests through the SDK transport and waits for their close events before returning, including on timeout. Continuous response data does not extend the deadline, and an expired signal prevents later retries from opening connections. The release uploader retains its separate inactivity timeout.
+
+The SDK's dependency tree is a fork of the retired `request` package, pulling in older `http-signature`, `tough-cookie`, and `form-data` releases; the lockfile supply-chain check accepts it, and the alternative was hand-writing COS request signing.
+
+Existing objects that already carry the retained encoding marker, and their cached copies, are unaffected by this change. Removing or re-uploading them, and any CDN cache purge, remain separate operator actions; this note records no cloud operation.
+
+The release decision that an uncertain write must stay one inspectable attempt lives in the [packaging and updates decision](2026-08-25-electron-desktop-packaging-and-updates.md); this note changes the transport that implements it.

+ 43 - 0
.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.zh.md

@@ -0,0 +1,43 @@
+# Agent Note: 通过腾讯 COS SDK 上传 Desktop 发布产物
+
+Status: implemented
+
+[English](2026-09-16-desktop-cos-upload-transport.md) | 中文
+
+## 问题
+
+发布对象此前通过指向腾讯 COS 端点的 AWS S3 客户端上传。该客户端默认的校验和配置可以用 `Content-Encoding: aws-chunked` 发送流式请求尾帧;S3 在存储前会移除该标记,而兼容路径可能把它保留为对象元数据。一次面向用户的应用下载在收到响应头后立即以 HTTP/2 `RST_STREAM` 失败,而同一连接上的相邻下载正常完成;被报告带有该标记的对象分段下载失败。这两项观察都不能证明唯一原因,但上传传输是本仓库自己拥有的部分,而 CDN 提供任何源请求都未产生的元数据本身就是缺陷,值得在继续追查之前先消除。
+
+## 决策
+
+[desktop-cos.ts](../../../../apps/desktop/scripts/desktop-cos.ts)使用官方 `cos-nodejs-sdk-v5` 包构造每个 Desktop COS 客户端:HTTPS、不保持连接、不跟随重定向、不切换备用域名、不做时钟偏移校正,无活动超时为 900 秒。[upload-target.ts](../../../../apps/desktop/scripts/upload-target.ts)与[installed-update-cos.ts](../../../../apps/desktop/scripts/installed-update-cos.ts)都从该工厂获取客户端,`@aws-sdk/client-s3` 不再是 Desktop 依赖。
+
+所有上传对象——安装包、blockmap 和 YAML 清单,包括很小的字符串清单——都以一次 `putObject` 发送,其请求体是流,并携带显式 `ContentLength` 与预先算好的 `Content-MD5`。正是流使写入不可重复:SDK 只在请求体没有 `pipe` 时才重发请求,因此流式 PUT 只会尝试一次,两个上传器自身也不再添加重试。当调用方未指定时,SDK 还会注入空的 `Cache-Control` 头;工厂会移除该头,因此发布上传器仍然把缓存策略留给部署基础设施。
+
+qualification 传输保持原有 store 接口与命名空间检查不变。它通过 `getObject` 配合一个对已接收字节计算哈希的 `Writable` 输出读取对象,因此对象从不整体驻留内存;只有确认的 `NoSuchKey` 表示对象不存在,其他任何状态码,以及早于声明长度结束的传输,都会让操作失败。
+
+## 测试
+
+[cos-loopback.ts](../../../../apps/desktop/tests/cos-loopback.ts)把真实 SDK 实例 `before-send` 中的 URL 重定向到每个测试独立的 loopback 源,并记录实际收到的字节,因此测试观察的是传输的序列化结果,而不是它的 mock。[desktop-upload-run.spec.ts](../../../../apps/desktop/tests/desktop-upload-run.spec.ts)与[installed-update-cos.spec.ts](../../../../apps/desktop/tests/installed-update-cos.spec.ts)断言确切的请求体字节、`Content-Length`、`Content-MD5`、不存在传输编码与内容编码、已签名的 `x-cos-forbid-overwrite` 头、HTTP 500 与连接中断时每个对象只发一次请求、读取 404/403/截断的处理,以及保留记录不包含凭据与原始服务端消息。
+
+[cos-operation.spec.ts](../../../../apps/desktop/tests/cos-operation.spec.ts)验证跨重试的总截止时间、持续返回数据的响应流、未确认 PUT 的取消,以及关闭请求不影响其他操作。测试组合虚拟截止时间计时器与真实 socket 和流观测。
+
+## 考虑过的替代方案
+
+**保留 S3 客户端并在两条路径上都设置 `requestChecksumCalculation: WHEN_REQUIRED`。** qualification 传输原本已使用该设置,它能抑制尾帧。但它仍然通过兼容层写入一个厂商已提供受支持客户端的产品,发布路径也会依赖于某项配置始终正确,而不是依赖编码根本不会产生。官方 SDK 消除的是这一层,而不是调整它。
+
+**对小体积 YAML 清单改用 Buffer 请求体。** 对几百字节而言 Buffer 更省事,也不需要流处理。但它会重新启用 SDK 固定的四次重试,使一次结果不确定的清单写入可能静默重复一个可变对象。统一的流式 PUT 保持单一写入路径和单一保证。
+
+**使用 SDK 的 `uploadFile` 队列或分块上传。** 这些路径可并行处理大对象并提供进度上报。它们也会把单个对象拆成多个分块,带来分块级重试与上传状态,扩大一个必须恰好写入一次的产物的重复面。发布上传器对每个对象只发一次请求。
+
+**只替换发布上传器,qualification 传输继续使用 S3。** qualification 传输写入同一系列 bucket,若把它留下,兼容路径就会继续存在于最可能用于与生产行为对照的对象上。
+
+## 结果
+
+对象写入不可重复,这正是本次故障所需的性质,传输也不再依赖 S3 兼容行为。代价是每个对象只有一次请求:超大安装包不会并行,也没有进度上报,与它替换掉的单请求行为一致。qualification 版本查询的所有 SDK 尝试共享 30 秒总截止时间;对象读取与 PUT 的总截止时间为 15 分钟。每次操作独立拥有客户端与取消信号。[cos-operation.ts](../../../../apps/desktop/scripts/cos-operation.ts)通过 SDK 传输将信号传给底层 HTTP 请求,并在返回前等待请求的关闭事件,超时也不例外。持续返回数据不会延长截止时间,过期信号会阻止后续重试建立连接。发布上传器保留独立的无活动超时。
+
+该 SDK 的依赖树源自已停止维护的 `request` 包,会引入较旧的 `http-signature`、`tough-cookie` 和 `form-data` 版本;lockfile 的供应链检查接受它,替代方案是自行实现 COS 请求签名。
+
+已经带有该保留编码标记的既有对象及其缓存副本不受本次改动影响。删除或重新上传它们,以及任何 CDN 缓存刷新,仍是单独的操作动作;本记录不包含任何云端操作。
+
+「结果不确定的写入必须保持为一次可检查的尝试」这一发布决策由[打包与更新决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)负责;本记录改变的是实现它的传输。

+ 1 - 1
THIRD_PARTY_NOTICES.md

@@ -151,7 +151,6 @@ External packages **directly declared** for development, tests, types, or toolin
 
 | Package | License |
 | --- | --- |
-| [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3) | Apache-2.0 |
 | [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
 | [`@electron/get`](https://github.com/electron/get) | MIT |
 | [`@electron/notarize`](https://github.com/electron/notarize) | MIT |
@@ -187,6 +186,7 @@ External packages **directly declared** for development, tests, types, or toolin
 | [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg) | MIT |
 | [`@yarnpkg/cli-dist`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
 | [`app-builder-lib`](https://github.com/electron-userland/electron-builder) | MIT |
+| [`cos-nodejs-sdk-v5`](https://github.com/tencentyun/cos-nodejs-sdk-v5) | ISC |
 | [`cytoscape`](https://github.com/cytoscape/cytoscape.js) | MIT |
 | [`cytoscape-cose-bilkent`](https://github.com/cytoscape/cytoscape.js-cose-bilkent) | MIT |
 | [`dayjs`](https://github.com/iamkun/dayjs) | MIT |

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 7b3c787e4656c68a9a47d7398611bfa9b8e472e3
-README.zh.md: 9558de1b9b119c0ce3a714241fbec3873b8e377f
+README.md: a4abf7b44e9f48ca3b7d44f7556f7077ad035639
+README.zh.md: 16e488a9789f8c080d32215cb6202afe590c64d6

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2 - 2
apps/desktop/README.md


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2 - 2
apps/desktop/README.zh.md


+ 1 - 1
apps/desktop/package.json

@@ -38,7 +38,6 @@
     "semver": "^7.8.5"
   },
   "devDependencies": {
-    "@aws-sdk/client-s3": "3.1067.0",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@electron/get": "^5.1.0",
@@ -47,6 +46,7 @@
     "@types/node": "^22.20.0",
     "@types/semver": "^7.8.0",
     "app-builder-lib": "26.15.3",
+    "cos-nodejs-sdk-v5": "3.0.0",
     "electron": "^44.0.0",
     "electron-builder": "^26.15.3",
     "extract-zip": "^2.0.1",

+ 44 - 0
apps/desktop/scripts/cos-operation.ts

@@ -0,0 +1,44 @@
+/** Total deadlines and request cleanup for one qualification COS operation. */
+import * as http from 'node:http'
+import * as https from 'node:https'
+import type COS from 'cos-nodejs-sdk-v5'
+
+/**
+ * Run one SDK operation with a shared deadline across its HTTP attempts.
+ * @param cos Client dedicated to this operation, with no concurrent users.
+ * @param timeoutMs Total network-operation budget in milliseconds.
+ * @param operation SDK call whose result is returned after its requests close.
+ * @returns SDK result; expiration rejects with TimeoutError after aborting the transport.
+ */
+export async function cosOperation<T>(cos: COS, timeoutMs: number, operation: () => Promise<T>): Promise<T> {
+  const controller = new AbortController()
+  const timeout = new Error('installed update: COS operation exceeded its deadline')
+  timeout.name = 'TimeoutError'
+  const closed: Promise<void>[] = []
+  const wrap = (transport: typeof http | typeof https) => ({
+    ...transport,
+    request(options: http.RequestOptions) {
+      const request = transport.request({ ...options, signal: controller.signal })
+      closed.push(new Promise<void>((resolve) => { request.once('close', resolve) }))
+      return request
+    },
+  })
+  const modules = { 'http:': wrap(http), 'https:': wrap(https) }
+  // cos-request forwards httpModules to its native request layer, including every SDK retry.
+  const configure = (options: { httpModules?: typeof modules }): void => { options.httpModules = modules }
+  cos.on('before-send', configure)
+  const timer = setTimeout(() => { controller.abort(timeout) }, timeoutMs)
+  try {
+    const result = await operation()
+    if (controller.signal.aborted) throw timeout
+    return result
+  } catch (error) {
+    if (controller.signal.aborted) throw timeout
+    throw error
+  } finally {
+    clearTimeout(timer)
+    controller.abort()
+    await Promise.all(closed)
+    cos.off('before-send', configure)
+  }
+}

+ 53 - 0
apps/desktop/scripts/desktop-cos.ts

@@ -0,0 +1,53 @@
+/** Shared Tencent COS client construction for Desktop update objects. */
+
+import COS from 'cos-nodejs-sdk-v5'
+
+/** Region of every Desktop update bucket, present or future deployment. */
+export const DESKTOP_COS_REGION = 'ap-beijing'
+
+/** Request-level inactivity deadline for COS transfers, in milliseconds. */
+const TRANSFER_TIMEOUT_MS = 900_000
+
+/** Credentials for one COS client; values are never written to retained records. */
+export interface DesktopCosCredentials {
+  readonly secretId: string
+  readonly secretKey: string
+}
+
+/** Request options the SDK exposes to `before-send` listeners. */
+interface CosRequestOptions {
+  headers: Record<string, unknown>
+}
+
+/**
+ * Create a COS client whose writes cannot be repeated by the SDK.
+ *
+ * The SDK retries a failed request only while the request body is not a stream, so every caller
+ * supplies a stream with an explicit `ContentLength` and a precomputed `Content-MD5`. Host
+ * switching, redirect following, and clock-offset correction stay off so an ambiguous write is
+ * never sent to another endpoint. The SDK also injects an empty `Cache-Control` header when the
+ * caller names none; that header is removed here so uploading leaves cache policy to deployment
+ * infrastructure.
+ * @param credentials SecretId and SecretKey for the selected deployment.
+ * @returns A COS client that sends HTTPS requests to the region named by each call.
+ */
+export function createDesktopCos(credentials: DesktopCosCredentials): COS {
+  const cos = new COS({
+    SecretId: credentials.secretId,
+    SecretKey: credentials.secretKey,
+    Protocol: 'https:',
+    KeepAlive: false,
+    FollowRedirect: false,
+    AutoSwitchHost: false,
+    CorrectClockSkew: false,
+    ChunkRetryTimes: 0,
+    Timeout: TRANSFER_TIMEOUT_MS,
+    UploadCheckContentMd5: false,
+  })
+  cos.on('before-send', (options: CosRequestOptions) => {
+    for (const name of Object.keys(options.headers)) {
+      if (name.toLowerCase() === 'cache-control' && options.headers[name] === '') delete options.headers[name]
+    }
+  })
+  return cos
+}

+ 31 - 18
apps/desktop/scripts/desktop-upload-run.ts

@@ -3,13 +3,18 @@ import { createHash } from 'node:crypto'
 import { createReadStream } from 'node:fs'
 import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
 import { join } from 'node:path'
-import { PutObjectCommand, type S3Client } from '@aws-sdk/client-s3'
+import { Readable } from 'node:stream'
+import type COS from 'cos-nodejs-sdk-v5'
 import type { DesktopUploadArtifact, DesktopUploadPlan } from './desktop-upload-plan.ts'
+import { DESKTOP_COS_REGION } from './desktop-cos.ts'
 import { recordPackagingEvent } from './packaging-run.mjs'
 
 const FAILURE_CODES = new Set(['AccessDenied', 'InternalError', 'NoSuchBucket', 'BadDigest', 'SignatureDoesNotMatch',
   'RequestTimeout', 'TimeoutError', 'AbortError', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ENOSPC', 'EACCES', 'EPERM', 'ENOENT'])
 
+/** Object keys fingerprinted into every upload record to bind evidence to this uploader. */
+const UPLOADER_SOURCES = ['desktop-upload-run.ts', 'upload-target.ts', 'desktop-upload-plan.ts', 'desktop-cos.ts']
+
 async function fingerprint(artifact: DesktopUploadArtifact) {
   const sha512 = createHash('sha512')
   const md5 = createHash('md5')
@@ -24,15 +29,12 @@ async function fingerprint(artifact: DesktopUploadArtifact) {
 }
 
 function receipt(value: unknown): object {
-  if (typeof value !== 'object' || value === null || !('$metadata' in value)) return {}
-  const metadata = value.$metadata
-  if (typeof metadata !== 'object' || metadata === null) return {}
+  if (typeof value !== 'object' || value === null) return {}
+  const response = value as { statusCode?: unknown; RequestId?: unknown }
   return {
-    ...('httpStatusCode' in metadata && typeof metadata.httpStatusCode === 'number'
-      ? { httpStatus: metadata.httpStatusCode } : {}),
-    ...('requestId' in metadata && typeof metadata.requestId === 'string' && /^[\w+/=.-]{1,256}$/u.test(metadata.requestId)
-      ? { requestId: metadata.requestId } : {}),
-    ...('attempts' in metadata && typeof metadata.attempts === 'number' ? { attempts: metadata.attempts } : {}),
+    ...(typeof response.statusCode === 'number' ? { httpStatus: response.statusCode } : {}),
+    ...(typeof response.RequestId === 'string' && /^[\w+/=.-]{1,256}$/u.test(response.RequestId)
+      ? { requestId: response.RequestId } : {}),
   }
 }
 
@@ -43,14 +45,25 @@ function failureReceipt(error: unknown): object {
   return { errorCode, ...receipt(error) }
 }
 
+function streamedBody(artifact: DesktopUploadArtifact): Readable {
+  return artifact.contents === undefined
+    ? createReadStream(artifact.path)
+    : Readable.from([Buffer.from(artifact.contents)])
+}
+
 /**
  * Upload an already validated release, flushing intent and response evidence around every PUT.
+ *
+ * Each object is sent as one streamed PUT with an explicit length and Content-MD5, which is also
+ * what keeps the COS SDK's internal retry path unreachable: it repeats a request only when the
+ * body is not a stream. This function never retries either, so every confirmed PUT is the only
+ * write for its key.
  * @param plan Validated release metadata; credential values must not be included.
- * @param client Caller-owned COS client configured with maxAttempts: 1.
+ * @param cos Caller-owned client from the Desktop COS factory in `desktop-cos.ts`.
  * @param recordsRoot Local retained evidence parent, outside disposable artifact directories.
  * @returns Fresh record directory after all PUTs succeed; errors retain partial evidence and stop later PUTs.
  */
-export async function uploadDesktopRelease(plan: DesktopUploadPlan, client: S3Client, recordsRoot: string): Promise<string> {
+export async function uploadDesktopRelease(plan: DesktopUploadPlan, cos: COS, recordsRoot: string): Promise<string> {
   await mkdir(recordsRoot, { recursive: true })
   const directory = await mkdtemp(join(recordsRoot, `${plan.environment}-${plan.target}-`))
   process.stdout.write(`desktop upload: record ${directory}\n`)
@@ -63,7 +76,6 @@ export async function uploadDesktopRelease(plan: DesktopUploadPlan, client: S3Cl
   try {
     await writeFile(join(directory, 'events.jsonl'), '', { flag: 'wx', mode: 0o600, flush: true })
     recordPackagingEvent(directory, { type: 'upload-start', environment: plan.environment, target: plan.target, version: plan.version })
-    if (await client.config.maxAttempts() !== 1) throw new Error('desktop upload: automatic retries must be disabled')
     const artifacts = []
     for (const artifact of plan.artifacts) {
       stage = 'hash-input'
@@ -71,7 +83,7 @@ export async function uploadDesktopRelease(plan: DesktopUploadPlan, client: S3Cl
       artifacts.push({ ...artifact, ...await fingerprint(artifact) })
     }
     const sourceSha256: Record<string, string> = {}
-    for (const filename of ['desktop-upload-run.ts', 'upload-target.ts', 'desktop-upload-plan.ts']) {
+    for (const filename of UPLOADER_SOURCES) {
       sourceSha256[filename] = createHash('sha256').update(await readFile(join(import.meta.dirname, filename))).digest('hex')
     }
     await writeFile(join(directory, 'plan.json'), `${JSON.stringify({ schemaVersion: 1,
@@ -85,15 +97,16 @@ export async function uploadDesktopRelease(plan: DesktopUploadPlan, client: S3Cl
       stage = 'put'
       recordPackagingEvent(directory, { type: 'put-intent', key, size: artifact.size, sha512: artifact.sha512,
         channelMetadata: artifact.channelMetadata })
-      const body = artifact.contents ?? createReadStream(artifact.path)
+      const body = streamedBody(artifact)
       try {
-        const response = await client.send(new PutObjectCommand({ Bucket: plan.bucket, Key: key,
-          Body: body, ContentLength: artifact.size, ContentMD5: artifact.md5, ContentType: artifact.contentType }))
+        const response = await cos.putObject({ Bucket: plan.bucket, Region: DESKTOP_COS_REGION, Key: key,
+          Body: body, ContentLength: artifact.size, ContentType: artifact.contentType,
+          Headers: { 'Content-MD5': artifact.md5 } })
         confirmedPuts++
         stage = 'record-response'
-        recordPackagingEvent(directory, { type: 'put-confirmed', key, ...receipt(response) })
+        recordPackagingEvent(directory, { type: 'put-confirmed', key, attempts: 1, ...receipt(response) })
       } finally {
-        if (typeof body !== 'string') body.destroy()
+        body.destroy()
       }
       process.stdout.write(`desktop upload: uploaded ${key}\n`)
     }

+ 44 - 30
apps/desktop/scripts/installed-update-cos.ts

@@ -1,13 +1,20 @@
 /** Fixed test-COS transport; callers authorize writes separately from local planning. */
 import { createReadStream } from 'node:fs'
 import { createHash } from 'node:crypto'
-import { GetBucketVersioningCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
+import { Readable, Writable } from 'node:stream'
+import { cosOperation } from './cos-operation.ts'
+import { createDesktopCos, DESKTOP_COS_REGION } from './desktop-cos.ts'
 import { loadDesktopPackageEnvironment } from './desktop-package-environment.mjs'
 import type { InstalledUpdatePublicationStore, InstalledUpdateRemoteObject } from './installed-update-publication.ts'
 
 const BUCKET = 'bj-toc-download-test-1320056602'
 const ORIGIN = 'https://download-test.deepseek.com'
 
+/** COS reports a missing key through this error code; no other status means absence. */
+function isMissingObject(error: unknown): boolean {
+  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'NoSuchKey'
+}
+
 async function hashStream(stream: AsyncIterable<Uint8Array>): Promise<InstalledUpdateRemoteObject> {
   const hash = createHash('sha512')
   let size = 0
@@ -17,16 +24,20 @@ async function hashStream(stream: AsyncIterable<Uint8Array>): Promise<InstalledU
 
 /**
  * Create a fixed test transport from .env.windows, passing only test upload credentials to the SDK.
- * @returns Store and explicit disposal; no request is sent by construction and no signing process is launched.
+ * Version queries have a 30-second total deadline; object reads and PUTs have 15 minutes.
+ * Expiration aborts HTTP requests and waits for closure before releasing the publication operation.
+ * @returns Store whose writes are streamed and therefore cannot be repeated by the SDK.
  */
-export function createInstalledUpdateCos(): InstalledUpdatePublicationStore & { dispose(): void } {
+export function createInstalledUpdateCos(): InstalledUpdatePublicationStore {
   const environment = loadDesktopPackageEnvironment('win32')
   if (environment.DSH_DESKTOP_AUTO_UPDATE_ENV !== 'test' || environment.DOWNLOAD_TEST_ORIGIN !== ORIGIN
     || environment.DOWNLOAD_TEST_COS_BUCKET !== BUCKET || !environment.DOWNLOAD_TEST_COS_SECRET_ID?.trim()
     || !environment.DOWNLOAD_TEST_COS_SECRET_KEY?.trim()) throw new Error('installed update: complete test upload settings are required')
-  const client = new S3Client({ region: 'Auto', endpoint: 'https://cos.ap-beijing.myqcloud.com', maxAttempts: 1,
-    requestChecksumCalculation: 'WHEN_REQUIRED', responseChecksumValidation: 'WHEN_REQUIRED',
-    credentials: { accessKeyId: environment.DOWNLOAD_TEST_COS_SECRET_ID, secretAccessKey: environment.DOWNLOAD_TEST_COS_SECRET_KEY } })
+  const credentials = {
+    secretId: environment.DOWNLOAD_TEST_COS_SECRET_ID,
+    secretKey: environment.DOWNLOAD_TEST_COS_SECRET_KEY,
+  }
+  const client = () => createDesktopCos(credentials)
   const keyAllowed = (key: string): void => {
     if (!/^dsh-desk\/(?:bin|feeds)\/qualification\/[a-f0-9]{24}\/win-x64\/[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(key)) {
       throw new Error('installed update: COS key must stay in the Windows qualification namespace')
@@ -34,22 +45,27 @@ export function createInstalledUpdateCos(): InstalledUpdatePublicationStore & {
   }
   return {
     async versioningDisabled() {
-      const response = await client.send(new GetBucketVersioningCommand({ Bucket: BUCKET }), { abortSignal: AbortSignal.timeout(30_000) })
-      return response.$metadata.httpStatusCode === 200 && response.Status === undefined
+      const cos = client()
+      const response = await cosOperation(cos, 30_000, () => cos.getBucketVersioning({ Bucket: BUCKET, Region: DESKTOP_COS_REGION }))
+      const status: 'Enabled' | 'Suspended' | undefined = response.VersioningConfiguration.Status
+      return response.statusCode === 200 && status === undefined
     },
     async read(key) {
       keyAllowed(key)
+      const hash = createHash('sha512')
+      let size = 0
+      // Output keeps the object out of memory and makes the SDK wait for the write to finish.
+      const output = new Writable({
+        write(bytes: Buffer, _encoding, done) { hash.update(bytes); size += bytes.length; done() },
+      })
       try {
-        const response = await client.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }), {
-          abortSignal: AbortSignal.timeout(900_000),
-        })
-        if (!response.Body) throw new Error('installed update: missing COS response body')
-        // This transport uses the Node S3 handler, whose body is an async-iterable stream.
-        return await hashStream(response.Body as AsyncIterable<Uint8Array>)
+        const cos = client()
+        await cosOperation(cos, 900_000, () => cos.getObject({ Bucket: BUCKET, Region: DESKTOP_COS_REGION, Key: key, Output: output }))
       } catch (error) {
-        if (typeof error === 'object' && error !== null && 'name' in error && error.name === 'NoSuchKey') return null
+        if (isMissingObject(error)) return null
         throw error
-      }
+      } finally { output.destroy() }
+      return { sha512: hash.digest('base64'), size }
     },
     async publicRead(url) {
       const parsed = new URL(url)
@@ -78,21 +94,19 @@ export function createInstalledUpdateCos(): InstalledUpdatePublicationStore & {
       if (size !== object.size || sha512.digest('base64') !== object.sha512) {
         throw new Error('installed update: upload input bytes changed')
       }
-      const body = 'path' in object.source ? createReadStream(object.source.path) : object.source.contents
-      const command = new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body, ContentLength: object.size,
-        ContentMD5: md5.digest('base64'), ContentType: key.endsWith('.yml') ? 'application/yaml' : 'application/octet-stream',
-        CacheControl: 'no-store' })
-      if (object.forbidOverwrite) command.middlewareStack.add(next => async (args) => {
-        // S3's serialize step supplies the HTTP request before this build middleware.
-        const request = args.request as { headers: Record<string, string> }
-        request.headers['x-cos-forbid-overwrite'] = 'true'
-        return next(args)
-      }, { step: 'build', name: 'qualificationForbidOverwrite' })
+      const headers: Record<string, string> = { 'Content-MD5': md5.digest('base64') }
+      if (object.forbidOverwrite) headers['x-cos-forbid-overwrite'] = 'true'
+      const body = 'path' in object.source
+        ? createReadStream(object.source.path)
+        : Readable.from([Buffer.from(object.source.contents)])
       try {
-        const response = await client.send(command, { abortSignal: AbortSignal.timeout(900_000) })
-        return response.$metadata.requestId === undefined ? {} : { requestId: response.$metadata.requestId }
-      } finally { if (typeof body !== 'string') body.destroy() }
+        const cos = client()
+        const response = await cosOperation(cos, 900_000, () => cos.putObject({
+          Bucket: BUCKET, Region: DESKTOP_COS_REGION, Key: key, Body: body,
+          ContentLength: object.size, ContentType: key.endsWith('.yml') ? 'application/yaml' : 'application/octet-stream',
+          CacheControl: 'no-store', Headers: headers }))
+        return response.RequestId === undefined ? {} : { requestId: response.RequestId }
+      } finally { body.destroy() }
     },
-    dispose() { client.destroy() },
   }
 }

+ 4 - 4
apps/desktop/scripts/installed-update-publication.ts

@@ -201,10 +201,10 @@ export async function executeInstalledUpdatePublication(
     return join(record, 'result.json')
   } catch (error) {
     result.failure = 'operation stopped; inspect the retained stage and remote state before any further publication'
-    if (typeof error === 'object' && error !== null && '$metadata' in error && typeof error.$metadata === 'object' && error.$metadata !== null) {
-      const metadata = error.$metadata as { httpStatusCode?: unknown }
-      if (typeof metadata.httpStatusCode === 'number' && metadata.httpStatusCode >= 100 && metadata.httpStatusCode <= 599) {
-        result.httpStatus = metadata.httpStatusCode
+    if (typeof error === 'object' && error !== null && 'statusCode' in error) {
+      const statusCode = error.statusCode
+      if (typeof statusCode === 'number' && statusCode >= 100 && statusCode <= 599) {
+        result.httpStatus = statusCode
       }
     }
     throw new Error(`installed update: publication stopped; record: ${record ?? 'not allocated'}`)

+ 1 - 4
apps/desktop/scripts/publish-installed-update.ts

@@ -31,10 +31,7 @@ async function main(): Promise<void> {
     confirmed = (await terminal.question(`请输入 ${expected}:`)) === expected
   } finally { terminal.close() }
   if (!confirmed) throw new Error('operator declined')
-  const store = createInstalledUpdateCos()
-  try {
-    console.log(await executeInstalledUpdatePublication(manifest, version, receipt, action, store, values.journals))
-  } finally { store.dispose() }
+  console.log(await executeInstalledUpdatePublication(manifest, version, receipt, action, createInstalledUpdateCos(), values.journals))
 }
 
 main().catch(() => {

+ 5 - 15
apps/desktop/scripts/upload-target.ts

@@ -2,8 +2,8 @@
 
 import { resolve } from 'node:path'
 import { parseArgs } from 'node:util'
-import { S3Client } from '@aws-sdk/client-s3'
 import type { DesktopPackageTargetName } from './package-target.ts'
+import { createDesktopCos } from './desktop-cos.ts'
 import { loadDesktopPackageEnvironment } from './desktop-package-environment.mjs'
 import { createDesktopUploadPlan } from './desktop-upload-plan.ts'
 import { uploadDesktopRelease } from './desktop-upload-run.ts'
@@ -34,22 +34,12 @@ async function main(): Promise<void> {
   const name = targetName(target)
   const environment = loadDesktopPackageEnvironment(name === 'win-x64' ? 'win32' : 'darwin')
   const plan = await createDesktopUploadPlan(name, { environment })
-  const client = new S3Client({
-    maxAttempts: 1,
-    region: 'Auto',
-    endpoint: 'https://cos.ap-beijing.myqcloud.com',
-    credentials: {
-      accessKeyId: requiredEnvironmentValue(environment, plan.secretIdEnvName),
-      secretAccessKey: requiredEnvironmentValue(environment, plan.secretKeyEnvName),
-    },
+  const cos = createDesktopCos({
+    secretId: requiredEnvironmentValue(environment, plan.secretIdEnvName),
+    secretKey: requiredEnvironmentValue(environment, plan.secretKeyEnvName),
   })
   process.stdout.write(`desktop upload: ${plan.target} ${plan.version} -> ${plan.publicUrl}\n`)
-  try {
-    await uploadDesktopRelease(plan, client, resolve(import.meta.dirname, '../.desktop-build/upload-records'))
-  }
-  finally {
-    client.destroy()
-  }
+  await uploadDesktopRelease(plan, cos, resolve(import.meta.dirname, '../.desktop-build/upload-records'))
 }
 
 if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {

+ 110 - 0
apps/desktop/tests/cos-loopback.ts

@@ -0,0 +1,110 @@
+/** Loopback HTTP capture for tests that exercise real COS SDK request serialization. */
+
+import { createServer, type ServerResponse } from 'node:http'
+import type { AddressInfo } from 'node:net'
+import type COS from 'cos-nodejs-sdk-v5'
+
+/** One request the SDK actually sent, with bytes read from the wire. */
+export interface CosLoopbackRequest {
+  readonly method: string
+  /** Request path including the query string, as sent to the real endpoint. */
+  readonly path: string
+  /** Lowercased request headers exactly as the SDK wrote them. */
+  readonly headers: Record<string, string | string[] | undefined>
+  /** Exact body bytes received. */
+  readonly body: Buffer
+  /** Signed URL the SDK built before the test redirected it to this server. */
+  readonly signedUrl: string | undefined
+}
+
+/** Owns one request's reply; use {@link answer} for a complete response. */
+export type CosLoopbackResponder = (request: CosLoopbackRequest, response: ServerResponse) => void | Promise<void>
+
+/** One isolated HTTP origin that captures SDK requests instead of reaching COS. */
+export interface CosLoopback {
+  readonly requests: CosLoopbackRequest[]
+  /** Redirect every SDK request to this server while keeping the signed headers intact. */
+  redirect(cos: COS): void
+  /** Close the server; rejects with the first responder failure so no reply error stays silent. */
+  close(): Promise<void>
+}
+
+/**
+ * Write a complete response and close the connection so the client cannot reuse its socket.
+ * @param socket Response object owned by the responder.
+ * @param status HTTP status code.
+ * @param body Complete response body.
+ * @param headers Additional response headers.
+ */
+export function answer(socket: ServerResponse, status = 200, body: string | Buffer = '',
+  headers: Record<string, string> = {}): void {
+  socket.writeHead(status, { connection: 'close', ...headers })
+  socket.end(body)
+}
+
+/**
+ * Build a COS error body the SDK maps to its error code.
+ * @param code COS error code, such as `NoSuchKey` or `InternalError`.
+ * @param message Server message; tests use it to prove diagnostics stay out of retained records.
+ * @returns XML error document.
+ */
+export function cosError(code: string, message = 'fixture'): string {
+  return `<Error><Code>${code}</Code><Message>${message}</Message></Error>`
+}
+
+/**
+ * Start a loopback server that records each request before the responder answers it.
+ * @param responder Reply for each captured request; the default is an empty 200.
+ * @returns Recorded requests, the SDK redirect, and an idempotent close.
+ */
+export async function startCosLoopback(
+  responder: CosLoopbackResponder = (_request, response) => { answer(response) },
+): Promise<CosLoopback> {
+  const requests: CosLoopbackRequest[] = []
+  const signedUrls: string[] = []
+  const failures: unknown[] = []
+  const server = createServer((incoming, response) => {
+    const chunks: Buffer[] = []
+    incoming.on('data', (chunk: Buffer) => chunks.push(chunk))
+    incoming.on('end', () => {
+      const request: CosLoopbackRequest = {
+        method: incoming.method ?? '',
+        path: incoming.url ?? '',
+        headers: incoming.headers,
+        body: Buffer.concat(chunks),
+        signedUrl: signedUrls[requests.length],
+      }
+      requests.push(request)
+      void Promise.resolve(responder(request, response)).catch((error: unknown) => {
+        failures.push(error)
+        response.destroy()
+      })
+    })
+  })
+  await new Promise<void>((resolve, reject) => {
+    server.once('error', reject)
+    server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolve() })
+  })
+  const { port } = server.address() as AddressInfo
+  let closed = false
+  return {
+    requests,
+    redirect(cos) {
+      cos.on('before-send', (options: { url: string }) => {
+        signedUrls.push(options.url)
+        const parsed = new URL(options.url)
+        options.url = `http://127.0.0.1:${port}${parsed.pathname}${parsed.search}`
+      })
+    },
+    async close() {
+      if (closed) return
+      closed = true
+      server.closeAllConnections()
+      await new Promise<void>((resolve, reject) => server.close((error) => {
+        if (error === undefined) resolve()
+        else reject(error)
+      }))
+      if (failures.length > 0) throw failures[0]
+    },
+  }
+}

+ 105 - 0
apps/desktop/tests/cos-operation.spec.ts

@@ -0,0 +1,105 @@
+import { Readable, Writable } from 'node:stream'
+import type { ServerResponse } from 'node:http'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cosOperation } from '../scripts/cos-operation.ts'
+import { createDesktopCos } from '../scripts/desktop-cos.ts'
+import { answer, cosError, startCosLoopback, type CosLoopback } from './cos-loopback.ts'
+
+const loopbacks: CosLoopback[] = []
+afterEach(async () => {
+  vi.useRealTimers()
+  await Promise.all(loopbacks.splice(0).map(loopback => loopback.close()))
+})
+
+const object = { Bucket: 'fixture-1250000000', Region: 'ap-beijing', Key: 'object' }
+
+async function fixture(retry = false) {
+  const received = Promise.withResolvers<ServerResponse>()
+  const disconnected = Promise.withResolvers<undefined>()
+  const loopback = await startCosLoopback((_request, response) => {
+    if (retry && loopback.requests.length === 1) {
+      vi.advanceTimersByTime(20_000)
+      answer(response, 500, cosError('InternalError'))
+      return
+    }
+    response.once('close', () => { disconnected.resolve(undefined) })
+    received.resolve(response)
+  })
+  loopbacks.push(loopback)
+  const cos = createDesktopCos({ secretId: 'fixture-id', secretKey: 'fixture-key' })
+  loopback.redirect(cos)
+  // Only deadline timers are virtual; socket delivery and stream close remain real observations.
+  vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
+  return { cos, loopback, received: received.promise, disconnected: disconnected.promise }
+}
+
+describe('COS operation total deadlines', () => {
+  it('aborts a stalled versioning retry within the original operation budget', async () => {
+    const f = await fixture(true)
+    const pending = cosOperation(f.cos, 30_000, () => f.cos.getBucketVersioning(object))
+    const rejected = expect(pending).rejects.toMatchObject({ name: 'TimeoutError' })
+    await f.received
+    vi.advanceTimersByTime(10_000)
+    await rejected
+    await f.disconnected
+    // The aborted signal also prevents the SDK from opening sockets for later retry attempts.
+    expect(f.loopback.requests).toHaveLength(2)
+  })
+
+  it('aborts an active download despite ongoing data and closes its request', async () => {
+    const f = await fixture()
+    let received = Promise.withResolvers<undefined>()
+    let size = 0
+    const output = new Writable({ write(bytes: Buffer, _encoding, done) {
+      size += bytes.length; received.resolve(undefined); done()
+    } })
+    const pending = cosOperation(f.cos, 900_000, () => f.cos.getObject({ ...object, Output: output }))
+    const rejected = expect(pending).rejects.toMatchObject({ name: 'TimeoutError' })
+    try {
+      const response = await f.received
+      response.writeHead(200, { 'content-length': '100' })
+      for (let i = 0; i < 3; i++) {
+        received = Promise.withResolvers<undefined>()
+        response.write('x')
+        await received.promise
+        vi.advanceTimersByTime(300_000)
+      }
+      await rejected
+      await f.disconnected
+      expect(size).toBe(3)
+      expect(f.loopback.requests).toHaveLength(1)
+    } finally { output.destroy() }
+  })
+
+  it('aborts a PUT awaiting confirmation without resending the object', async () => {
+    const f = await fixture()
+    const body = Readable.from(['bytes'])
+    const pending = cosOperation(f.cos, 900_000, () => f.cos.putObject({ ...object, Body: body, ContentLength: 5 }))
+    const rejected = expect(pending).rejects.toMatchObject({ name: 'TimeoutError' })
+    try {
+      await f.received
+      vi.advanceTimersByTime(900_000)
+      await rejected
+      await f.disconnected
+      expect(f.loopback.requests).toHaveLength(1)
+      expect(f.loopback.requests[0]!.body.toString()).toBe('bytes')
+    } finally { body.destroy() }
+  })
+
+  it('clears the deadline after success and leaves another client running', async () => {
+    const first = await fixture()
+    const second = await fixture()
+    const read = (f: Awaited<ReturnType<typeof fixture>>) => cosOperation(f.cos, 30_000, () => f.cos.getBucketVersioning(object))
+    const success = read(first)
+    const pending = read(second)
+    answer(await first.received, 200, '<VersioningConfiguration/>')
+    await success
+    await first.disconnected
+    const response = await second.received
+    vi.advanceTimersByTime(29_999)
+    answer(response, 200, '<VersioningConfiguration/>')
+    await pending
+    await second.disconnected
+    expect(vi.getTimerCount()).toBe(0)
+  })
+})

+ 128 - 86
apps/desktop/tests/desktop-upload-run.spec.ts

@@ -2,57 +2,57 @@ import { createHash } from 'node:crypto'
 import { mkdtemp, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
-import { Readable } from 'node:stream'
-import { S3Client } from '@aws-sdk/client-s3'
+import COS from 'cos-nodejs-sdk-v5'
 import { afterEach, describe, expect, it } from 'vitest'
+import { createDesktopCos, DESKTOP_COS_REGION } from '../scripts/desktop-cos.ts'
 import { uploadDesktopRelease } from '../scripts/desktop-upload-run.ts'
 import type { DesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
+import { answer, cosError, startCosLoopback, type CosLoopback, type CosLoopbackRequest } from './cos-loopback.ts'
 
 const roots: string[] = []
-afterEach(async () => { await Promise.all(roots.splice(0).map(path => rm(path, { recursive: true, force: true }))) })
+const loopbacks: CosLoopback[] = []
+afterEach(async () => {
+  const closers = loopbacks.splice(0)
+  const paths = roots.splice(0)
+  try { await Promise.all(closers.map(loopback => loopback.close())) }
+  finally { await Promise.all(paths.map(path => rm(path, { recursive: true, force: true }))) }
+})
+
+const BINARY_BYTES = 'binary fixture'
+const FEED_BYTES = 'version: 1.2.3\n'
 
 async function fixture(environment: 'test' | 'production' = 'test') {
   const root = await mkdtemp(join(tmpdir(), 'dsh-upload-audit-'))
   roots.push(root)
   const binary = join(root, 'package.exe')
-  await writeFile(binary, 'binary fixture')
+  await writeFile(binary, BINARY_BYTES)
+  const bucket = `${environment}-fixture-1250000000`
   const plan: DesktopUploadPlan = {
-    environment, target: 'win-x64', version: '1.2.3', bucket: `${environment}-bucket`,
+    environment, target: 'win-x64', version: '1.2.3', bucket,
     publicUrl: `https://${environment}.invalid/feed/`, secretIdEnvName: 'UNREAD_ID', secretKeyEnvName: 'UNREAD_KEY',
     artifacts: [
       { path: binary, filename: 'package.exe', key: 'bin/package.exe', contentType: 'application/octet-stream', channelMetadata: false },
       { path: join(root, 'nightly.yml'), filename: 'nightly.yml', key: 'feeds/nightly.yml', contentType: 'application/yaml', channelMetadata: true,
-        contents: 'version: 1.2.3\n' },
+        contents: FEED_BYTES },
     ],
   }
   const records = join(root, 'records')
   const directory = async () => join(records, (await readdir(records))[0]!)
-  return { root, records, plan, directory }
+  return { root, records, plan, bucket, directory }
 }
 
-interface Request {
-  path: string
-  headers: Record<string, string>
-  body?: string | AsyncIterable<Uint8Array>
-}
-
-function client(handle: (request: Request) => Promise<number>, maxAttempts = 1) {
-  return new S3Client({ region: 'Auto', endpoint: 'https://cos.invalid', maxAttempts,
-    credentials: { accessKeyId: 'test-credential-id', secretAccessKey: 'test-credential-secret' },
-    requestHandler: { handle: async (input: unknown) => {
-      const request = input as Request
-      if (request.body && typeof request.body !== 'string') for await (const _bytes of request.body) { /* Drain this request's owned stream. */ }
-      const statusCode = await handle(request)
-      return { response: { statusCode, headers: { 'x-amz-request-id': 'fixture-request' }, body: Readable.from([
-        Buffer.from(statusCode === 200 ? '' : '<Error><Code>InternalError</Code><Message>test-credential-secret</Message></Error>'),
-      ]) } }
-    } },
-  })
+/** Production client redirecting to one isolated loopback origin. */
+async function transport(responder?: Parameters<typeof startCosLoopback>[0]) {
+  const loopback = await startCosLoopback(responder)
+  loopbacks.push(loopback)
+  const cos = createDesktopCos({ secretId: 'test-credential-id', secretKey: 'test-credential-secret' })
+  loopback.redirect(cos)
+  return { cos, loopback }
 }
 
 interface RecordedPlan {
   environment: string
-  artifacts: { contents?: string; md5: string; sha512: string }[]
+  artifacts: ({ contents?: string } & { md5: string; sha512: string; size: number })[]
   sourceSha256: Record<string, string>
 }
 
@@ -62,91 +62,133 @@ async function journal(path: string): Promise<{ type: string; time: string }[]>
   return (await readFile(path, 'utf8')).trim().split('\n').map(line => JSON.parse(line) as { type: string; time: string })
 }
 
-describe('release upload audit with real SDK and isolated HTTP replacement', () => {
+/** Assert the wire form shared by every upload: exact bytes, one content-length, no transfer encoding. */
+function expectExactObject(request: CosLoopbackRequest, expected: string, contentType: string, bucket: string): void {
+  expect(request.method).toBe('PUT')
+  expect(request.body.toString('utf8')).toBe(expected)
+  expect(request.headers['content-length']).toBe(String(Buffer.byteLength(expected)))
+  expect(request.headers['content-type']).toBe(contentType)
+  expect(request.headers['transfer-encoding']).toBeUndefined()
+  expect(request.headers['content-encoding']).toBeUndefined()
+  expect(request.headers['content-md5']).toBe(createHash('md5').update(expected).digest('base64'))
+  const signed = new URL(request.signedUrl!)
+  expect(signed.protocol).toBe('https:')
+  expect(signed.host).toBe(`${bucket}.cos.${DESKTOP_COS_REGION}.myqcloud.com`)
+  expect(signed.search).toBe('')
+}
+
+describe('release upload audit with the real COS SDK and an isolated loopback origin', () => {
   it.each(['test', 'production'] as const)('retains %s plan, feed bytes, intent, receipt and hashes before advancing', async (environment) => {
     const f = await fixture(environment)
-    const requests: Request[] = []
-    const transport = client(async (request) => {
-      requests.push(request)
+    const { cos, loopback } = await transport(async (request, response) => {
       const directory = await f.directory()
       const plan = await json<RecordedPlan>(join(directory, 'plan.json'))
       expect(plan.environment).toBe(environment)
-      expect(plan.artifacts[1]!.contents).toBe('version: 1.2.3\n')
+      expect(plan.artifacts[1]!.contents).toBe(FEED_BYTES)
       const lines = await journal(join(directory, 'events.jsonl'))
-      expect(lines.at(-1)).toMatchObject({ type: 'put-intent', key: f.plan.artifacts[requests.length - 1]!.key })
-      expect(request.headers['content-md5']).toBe(plan.artifacts[requests.length - 1]!.md5)
-      return 200
+      expect(lines.at(-1)).toMatchObject({ type: 'put-intent', key: f.plan.artifacts[loopback.requests.length - 1]!.key })
+      expect(request.headers['content-md5']).toBe(plan.artifacts[loopback.requests.length - 1]!.md5)
+      answer(response, 200, '', { 'x-cos-request-id': 'fixture-request' })
     })
-    try {
-      const directory = await uploadDesktopRelease(f.plan, transport, f.records)
-      expect(requests).toHaveLength(2)
-      const plan = await json<RecordedPlan>(join(directory, 'plan.json'))
-      expect(plan.artifacts[0]!.sha512).toBe(createHash('sha512').update('binary fixture').digest('base64'))
-      expect(plan.sourceSha256['upload-target.ts']).toBe(createHash('sha256')
-        .update(await readFile(join(import.meta.dirname, '../scripts/upload-target.ts'))).digest('hex'))
-      const result = await json(join(directory, 'result.json'))
-      expect(result).toMatchObject({ success: true, stage: 'complete', confirmedPuts: 2, publicReadback: 'not-performed' })
-      const events = await journal(join(directory, 'events.jsonl'))
-      expect(`${events.map(event => event.type).join('\n')}\n`).toBe(await readFile(join(import.meta.dirname, 'expected/desktop-upload-audit.txt'), 'utf8'))
-      expect(events[2]).toMatchObject({ httpStatus: 200, requestId: 'fixture-request', attempts: 1 })
-      for (const event of events) expect(Number.isFinite(Date.parse(event.time))).toBe(true)
-      expect(JSON.stringify({ plan, result, events })).not.toMatch(/test-credential|authorization|UNREAD_KEY/iu)
-    } finally { transport.destroy() }
+    const directory = await uploadDesktopRelease(f.plan, cos, f.records)
+    expect(loopback.requests).toHaveLength(2)
+    expectExactObject(loopback.requests[0]!, BINARY_BYTES, 'application/octet-stream', f.bucket)
+    expectExactObject(loopback.requests[1]!, FEED_BYTES, 'application/yaml', f.bucket)
+    expect(loopback.requests[0]!.path).toBe('/bin/package.exe')
+    expect(loopback.requests[1]!.path).toBe('/feeds/nightly.yml')
+    for (const request of loopback.requests) {
+      const authorization = String(request.headers.authorization)
+      expect(authorization).toContain('q-sign-algorithm=sha1')
+      expect(authorization).toContain('content-md5')
+      expect(authorization).not.toContain('test-credential-secret')
+    }
+    const plan = await json<RecordedPlan>(join(directory, 'plan.json'))
+    expect(plan.artifacts[0]!.sha512).toBe(createHash('sha512').update(BINARY_BYTES).digest('base64'))
+    expect(plan.sourceSha256['upload-target.ts']).toBe(createHash('sha256')
+      .update(await readFile(join(import.meta.dirname, '../scripts/upload-target.ts'))).digest('hex'))
+    expect(plan.sourceSha256['desktop-cos.ts']).toBe(createHash('sha256')
+      .update(await readFile(join(import.meta.dirname, '../scripts/desktop-cos.ts'))).digest('hex'))
+    const result = await json(join(directory, 'result.json'))
+    expect(result).toMatchObject({ success: true, stage: 'complete', confirmedPuts: 2, publicReadback: 'not-performed' })
+    const events = await journal(join(directory, 'events.jsonl'))
+    expect(`${events.map(event => event.type).join('\n')}\n`).toBe(await readFile(join(import.meta.dirname, 'expected/desktop-upload-audit.txt'), 'utf8'))
+    expect(events[2]).toMatchObject({ httpStatus: 200, requestId: 'fixture-request', attempts: 1 })
+    for (const event of events) expect(Number.isFinite(Date.parse(event.time))).toBe(true)
+    expect(JSON.stringify({ plan, result, events })).not.toMatch(/test-credential|authorization|UNREAD_KEY/iu)
   })
 
   it.each([1, 2])('retains failure at PUT %s without retry or later upload, excluding raw SDK diagnostics', async (failAt) => {
     const f = await fixture()
-    let requests = 0
-    const transport = client(async () => { requests++; return requests === failAt ? 500 : 200 })
-    try {
-      await expect(uploadDesktopRelease(f.plan, transport, f.records)).rejects.toThrow('stopped at put')
-      expect(requests).toBe(failAt)
-      const directory = await f.directory()
-      const result = await json(join(directory, 'result.json'))
-      expect(result).toMatchObject({ success: false, confirmedPuts: failAt - 1, stage: 'put', failure: { errorCode: 'InternalError', httpStatus: 500, requestId: 'fixture-request' } })
-      expect(await readFile(join(directory, 'result.json'), 'utf8')).not.toContain('test-credential-secret')
-      expect((await readFile(join(directory, 'events.jsonl'), 'utf8')).trim().split('\n')).toHaveLength(failAt * 2)
-    } finally { transport.destroy() }
+    const { cos, loopback } = await transport((_request, response) => {
+      if (loopback.requests.length === failAt) {
+        answer(response, 500, cosError('InternalError', 'test-credential-secret'),
+          { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
+        return
+      }
+      answer(response, 200, '', { 'x-cos-request-id': 'fixture-request' })
+    })
+    await expect(uploadDesktopRelease(f.plan, cos, f.records)).rejects.toThrow('stopped at put')
+    expect(loopback.requests).toHaveLength(failAt)
+    const directory = await f.directory()
+    const result = await json(join(directory, 'result.json'))
+    expect(result).toMatchObject({ success: false, confirmedPuts: failAt - 1, stage: 'put', failure: { errorCode: 'InternalError', httpStatus: 500, requestId: 'fixture-request' } })
+    expect(await readFile(join(directory, 'result.json'), 'utf8')).not.toContain('test-credential-secret')
+    expect((await readFile(join(directory, 'events.jsonl'), 'utf8')).trim().split('\n')).toHaveLength(failAt * 2)
+  })
+
+  it('cannot repeat the write when a client leaves every SDK retry option at its default', async () => {
+    const f = await fixture()
+    const loopback = await startCosLoopback((_request, response) => {
+      answer(response, 500, cosError('InternalError'), { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
+    })
+    loopbacks.push(loopback)
+    // Deliberately retry-permissive: a buffer body for the manifest would let the SDK send it up to four times.
+    const cos = new COS({ SecretId: 'test-credential-id', SecretKey: 'test-credential-secret', Protocol: 'https:' })
+    loopback.redirect(cos)
+    await expect(uploadDesktopRelease(f.plan, cos, f.records)).rejects.toThrow('stopped at put')
+    expect(loopback.requests.map(request => request.path)).toEqual(['/bin/package.exe'])
+    expect(loopback.requests[0]!.body.toString('utf8')).toBe(BINARY_BYTES)
+  })
+
+  it.each([1, 2])('reports a dropped connection at PUT %s once without sending the object again', async (dropAt) => {
+    const f = await fixture()
+    const { cos, loopback } = await transport((_request, response) => {
+      if (loopback.requests.length === dropAt) { response.destroy(); return }
+      answer(response, 200, '', { 'x-cos-request-id': 'fixture-request' })
+    })
+    await expect(uploadDesktopRelease(f.plan, cos, f.records)).rejects.toThrow('stopped at put')
+    expect(loopback.requests).toHaveLength(dropAt)
+    const result = await json(join(await f.directory(), 'result.json'))
+    expect(result).toMatchObject({ success: false, confirmedPuts: dropAt - 1, stage: 'put', failure: { errorCode: 'ECONNRESET' } })
   })
 
-  it('refuses networking when the audit parent cannot be created or retries are enabled', async () => {
+  it('refuses networking when the audit parent cannot be created', async () => {
     const f = await fixture()
-    let requests = 0
-    const transport = client(async () => { requests++; return 200 }, 3)
-    try {
-      await expect(uploadDesktopRelease(f.plan, transport, f.plan.artifacts[0]!.path)).rejects.toThrow()
-      await expect(uploadDesktopRelease(f.plan, transport, f.records)).rejects.toThrow('stopped at prepare')
-      expect(requests).toBe(0)
-      expect(await json(join(await f.directory(), 'result.json'))).toMatchObject({ success: false, confirmedPuts: 0 })
-    } finally { transport.destroy() }
+    const { cos, loopback } = await transport()
+    await expect(uploadDesktopRelease(f.plan, cos, f.plan.artifacts[0]!.path)).rejects.toThrow()
+    expect(loopback.requests).toHaveLength(0)
   })
 
   it('stops before the next PUT when a response cannot be recorded', async () => {
     const f = await fixture()
-    let requests = 0
-    const transport = client(async () => {
-      requests++
+    const { cos, loopback } = await transport(async (_request, response) => {
       const directory = await f.directory()
       await rename(join(directory, 'events.jsonl'), join(directory, 'events-before-failure.jsonl'))
       await mkdir(join(directory, 'events.jsonl'))
-      return 200
+      answer(response, 200, '', { 'x-cos-request-id': 'fixture-request' })
     })
-    try {
-      await expect(uploadDesktopRelease(f.plan, transport, f.records)).rejects.toThrow('stopped at record-response')
-      expect(requests).toBe(1)
-      expect(await json(join(await f.directory(), 'result.json'))).toMatchObject({ success: false, stage: 'record-response', confirmedPuts: 1 })
-    } finally { transport.destroy() }
+    await expect(uploadDesktopRelease(f.plan, cos, f.records)).rejects.toThrow('stopped at record-response')
+    expect(loopback.requests).toHaveLength(1)
+    expect(await json(join(await f.directory(), 'result.json'))).toMatchObject({ success: false, stage: 'record-response', confirmedPuts: 1 })
   })
 
   it('keeps independent runs without overwriting prior evidence', async () => {
     const f = await fixture()
-    const transport = client(async () => 200)
-    try {
-      const first = await uploadDesktopRelease(f.plan, transport, f.records)
-      const before = await readFile(join(first, 'result.json'), 'utf8')
-      const second = await uploadDesktopRelease(f.plan, transport, f.records)
-      expect(second).not.toBe(first)
-      expect(await readFile(join(first, 'result.json'), 'utf8')).toBe(before)
-    } finally { transport.destroy() }
+    const { cos } = await transport()
+    const first = await uploadDesktopRelease(f.plan, cos, f.records)
+    const before = await readFile(join(first, 'result.json'), 'utf8')
+    const second = await uploadDesktopRelease(f.plan, cos, f.records)
+    expect(second).not.toBe(first)
+    expect(await readFile(join(first, 'result.json'), 'utf8')).toBe(before)
   })
 })

+ 169 - 79
apps/desktop/tests/installed-update-cos.spec.ts

@@ -1,114 +1,204 @@
 import { createHash } from 'node:crypto'
+import { mkdtemp, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import type COS from 'cos-nodejs-sdk-v5'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { createInstalledUpdateCos } from '../scripts/installed-update-cos.ts'
+import { answer, cosError, startCosLoopback, type CosLoopback, type CosLoopbackResponder } from './cos-loopback.ts'
+
+const state = vi.hoisted(() => ({
+  settings: 'test',
+  redirect: undefined as ((cos: COS) => void) | undefined,
+}))
 
-interface Request {
-  method: string
-  path: string
-  headers: Record<string, string>
-  body?: string | AsyncIterable<Uint8Array>
-}
-const state = vi.hoisted(() => ({ requests: [] as Request[], status: 200, versioning: '', settings: 'test', maxAttempts: 0 }))
 vi.mock('../scripts/desktop-package-environment.mjs', () => ({ loadDesktopPackageEnvironment: () => ({
   DSH_DESKTOP_AUTO_UPDATE_ENV: state.settings, DOWNLOAD_TEST_ORIGIN: 'https://download-test.deepseek.com',
   DOWNLOAD_TEST_COS_BUCKET: 'bj-toc-download-test-1320056602', DOWNLOAD_TEST_COS_SECRET_ID: 'fixture-id',
   DOWNLOAD_TEST_COS_SECRET_KEY: 'fixture-key', DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'fixture-pin-not-for-sdk',
 }) }))
-vi.mock('@aws-sdk/client-s3', async (original) => {
-  const actual = await original<typeof import('@aws-sdk/client-s3')>()
-  const { Readable } = await import('node:stream')
-  return { ...actual, S3Client: class extends actual.S3Client {
-    constructor(options: ConstructorParameters<typeof actual.S3Client>[0]) {
-      expect(options?.credentials).toEqual({ accessKeyId: 'fixture-id', secretAccessKey: 'fixture-key' })
-      state.maxAttempts = options?.maxAttempts as number
-      super({ ...options, requestHandler: { handle: async (request: unknown) => {
-        const req = request as Request
-        state.requests.push(req)
-        if (req.body && typeof req.body !== 'string') for await (const _bytes of req.body) { /* Consume the owned request stream. */ }
-        const text = state.status === 200 ? req.method === 'PUT' ? ''
-          : req.path.includes('/dsh-desk/') ? 'object bytes'
-            : `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">${state.versioning}</VersioningConfiguration>`
-          : `<Error><Code>${state.status === 404 ? 'NoSuchKey' : 'AccessDenied'}</Code><Message>fixture</Message></Error>`
-        return { response: { statusCode: state.status, headers: { 'x-amz-request-id': 'fixture-request' }, body: Readable.from([Buffer.from(text)]) } }
-      } } })
-    }
+
+vi.mock('../scripts/desktop-cos.ts', async (original) => {
+  const actual = await original<typeof import('../scripts/desktop-cos.ts')>()
+  return { ...actual, createDesktopCos: (credentials: Parameters<typeof actual.createDesktopCos>[0]) => {
+    const cos = actual.createDesktopCos(credentials)
+    state.redirect?.(cos)
+    return cos
   } }
 })
-afterEach(() => { state.requests.length = 0; state.status = 200; state.versioning = ''; state.settings = 'test'; vi.unstubAllGlobals() })
+
+const loopbacks: CosLoopback[] = []
+const roots: string[] = []
+afterEach(async () => {
+  const closers = loopbacks.splice(0)
+  const paths = roots.splice(0)
+  vi.useRealTimers()
+  state.settings = 'test'
+  state.redirect = undefined
+  try {
+    try { await Promise.all(closers.map(loopback => loopback.close())) }
+    finally { vi.unstubAllGlobals() }
+  }
+  finally { await Promise.all(paths.map(path => rm(path, { recursive: true, force: true }))) }
+})
+
+const BUCKET = 'bj-toc-download-test-1320056602'
 const key = `dsh-desk/bin/qualification/${'a'.repeat(24)}/win-x64/package.exe`
 
-describe('qualification COS transport with real SDK serialization and substituted HTTP', () => {
-  it('sends explicit non-overwrite and MD5 headers without retrying or passing signing credentials', async () => {
-    const store = createInstalledUpdateCos()
-    try {
-      expect(state.requests).toHaveLength(0)
-      expect(await store.versioningDisabled()).toBe(true)
-      expect(await store.put(key, { source: { contents: 'bytes' }, size: 5,
-        sha512: createHash('sha512').update('bytes').digest('base64'), forbidOverwrite: true })).toEqual({ requestId: 'fixture-request' })
-      const request = state.requests.at(-1)!
-      expect(request.headers['x-cos-forbid-overwrite']).toBe('true')
-      expect(request.headers.authorization).toContain('x-cos-forbid-overwrite')
-      expect(request.headers['content-md5']).toBe(createHash('md5').update('bytes').digest('base64'))
-      expect(request.headers['cache-control']).toBe('no-store')
-      expect(JSON.stringify(request.headers)).not.toContain('fixture-pin')
-      expect(state.maxAttempts).toBe(1)
-    } finally { store.dispose() }
+/** Store whose SDK client sends every request to a fresh loopback origin. */
+async function store(responder: CosLoopbackResponder) {
+  const loopback = await startCosLoopback(responder)
+  loopbacks.push(loopback)
+  state.redirect = (cos) => { loopback.redirect(cos) }
+  return { store: createInstalledUpdateCos(), loopback }
+}
+
+function expectedHost(): string {
+  return `${BUCKET}.cos.ap-beijing.myqcloud.com`
+}
+
+describe('qualification COS transport with real SDK serialization over a loopback origin', () => {
+  it('sends one streamed PUT with explicit length, MD5, no-store policy and a signed non-overwrite header', async () => {
+    const { store: cos, loopback } = await store((_request, response) => {
+      answer(response, 200, '', { 'x-cos-request-id': 'fixture-request' })
+    })
+    expect(loopback.requests).toHaveLength(0)
+    expect(await cos.put(key, { source: { contents: 'bytes' }, size: 5,
+      sha512: createHash('sha512').update('bytes').digest('base64'), forbidOverwrite: true })).toEqual({ requestId: 'fixture-request' })
+    expect(loopback.requests).toHaveLength(1)
+    const request = loopback.requests[0]!
+    expect(request.method).toBe('PUT')
+    expect(request.path).toBe(`/${key}`)
+    expect(request.body.toString('utf8')).toBe('bytes')
+    expect(request.headers['content-length']).toBe('5')
+    expect(request.headers['content-md5']).toBe(createHash('md5').update('bytes').digest('base64'))
+    expect(request.headers['cache-control']).toBe('no-store')
+    expect(request.headers['x-cos-forbid-overwrite']).toBe('true')
+    expect(request.headers['transfer-encoding']).toBeUndefined()
+    expect(request.headers['content-encoding']).toBeUndefined()
+    const signed = new URL(request.signedUrl!)
+    expect(signed.protocol).toBe('https:')
+    expect(signed.host).toBe(expectedHost())
+    const authorization = String(request.headers.authorization)
+    expect(authorization).toContain('q-sign-algorithm=sha1')
+    expect(authorization).toContain('x-cos-forbid-overwrite')
+    expect(JSON.stringify(request.headers)).not.toContain('fixture-pin')
+    expect(JSON.stringify(request.headers)).not.toContain('fixture-key')
+  })
+
+  it('streams a file-backed object with its declared length', async () => {
+    const { store: cos, loopback } = await store((_request, response) => { answer(response, 200) })
+    const root = await mkdtemp(join(tmpdir(), 'dsh-cos-file-'))
+    roots.push(root)
+    const path = join(root, 'package.exe')
+    await writeFile(path, 'file bytes')
+    expect(await cos.put(key, { source: { path }, size: 10,
+      sha512: createHash('sha512').update('file bytes').digest('base64'), forbidOverwrite: true })).toEqual({})
+    expect(loopback.requests).toHaveLength(1)
+    expect(loopback.requests[0]!.body.toString('utf8')).toBe('file bytes')
+    expect(loopback.requests[0]!.headers['content-length']).toBe('10')
+    expect(loopback.requests[0]!.headers['content-type']).toBe('application/octet-stream')
+    expect(loopback.requests[0]!.headers['x-cos-forbid-overwrite']).toBe('true')
   })
 
-  it.each(['<Status>Enabled</Status>', '<Status>Suspended</Status>'])('does not authorize overwrites for %s', async (versioning) => {
-    state.versioning = versioning
-    const store = createInstalledUpdateCos()
-    try { expect(await store.versioningDisabled()).toBe(false) } finally { store.dispose() }
+  it.each([
+    ['', true],
+    ['<Status>Enabled</Status>', false],
+    ['<Status>Suspended</Status>', false],
+  ] as const)('reads bucket versioning %j as disabled=%s', async (status, disabled) => {
+    const { store: cos, loopback } = await store((_request, response) => {
+      answer(response, 200, `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">${status}</VersioningConfiguration>`,
+        { 'content-type': 'application/xml' })
+    })
+    expect(await cos.versioningDisabled()).toBe(disabled)
+    expect(loopback.requests).toHaveLength(1)
+    expect(loopback.requests[0]!.path).toContain('versioning')
   })
 
   it('does not retry an uncertain PUT failure', async () => {
-    state.status = 500
-    const store = createInstalledUpdateCos()
-    try {
-      await expect(store.put(key, { source: { contents: 'x' }, size: 1,
-        sha512: createHash('sha512').update('x').digest('base64'), forbidOverwrite: true })).rejects.toThrow()
-      expect(state.requests).toHaveLength(1)
-    } finally { store.dispose() }
+    const { store: cos, loopback } = await store((_request, response) => {
+      answer(response, 500, cosError('InternalError'), { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
+    })
+    await expect(cos.put(key, { source: { contents: 'x' }, size: 1,
+      sha512: createHash('sha512').update('x').digest('base64'), forbidOverwrite: true })).rejects.toThrow()
+    expect(loopback.requests).toHaveLength(1)
+  })
+
+  it.each(['versioning', 'read', 'put'] as const)('enforces the qualification %s total deadline', async (operation) => {
+    const received = Promise.withResolvers<undefined>()
+    const closed = Promise.withResolvers<undefined>()
+    vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
+    const { store: cos, loopback } = await store((_request, response) => {
+      response.once('close', () => { closed.resolve(undefined) })
+      received.resolve(undefined)
+    })
+    const pending = operation === 'versioning' ? cos.versioningDisabled()
+      : operation === 'read' ? cos.read(key)
+        : cos.put(key, { source: { contents: 'x' }, size: 1,
+          sha512: createHash('sha512').update('x').digest('base64'), forbidOverwrite: true })
+    const rejected = expect(pending).rejects.toMatchObject({ name: 'TimeoutError' })
+    await received.promise
+    vi.advanceTimersByTime(operation === 'versioning' ? 30_000 : 900_000)
+    await rejected
+    await closed.promise
+    expect(loopback.requests).toHaveLength(1)
+  })
+
+  it('hashes exact received object bytes without buffering the whole object', async () => {
+    const { store: cos, loopback } = await store((_request, response) => { answer(response, 200, 'object bytes') })
+    expect(await cos.read(key)).toEqual({ size: 12, sha512: createHash('sha512').update('object bytes').digest('base64') })
+    expect(loopback.requests).toHaveLength(1)
+    expect(loopback.requests[0]!.method).toBe('GET')
+    expect(loopback.requests[0]!.path).toBe(`/${key}`)
   })
 
   it('distinguishes confirmed object absence from permission errors', async () => {
-    const store = createInstalledUpdateCos()
-    try {
-      expect(await store.read(key)).toEqual({ size: 12, sha512: createHash('sha512').update('object bytes').digest('base64') })
-      state.status = 404
-      expect(await store.read(key)).toBeNull()
-      state.status = 403
-      await expect(store.read(key)).rejects.toThrow()
-    } finally { store.dispose() }
+    const { store: cos, loopback } = await store((_request, response) => {
+      const status = loopback.requests.length === 1 ? 404 : 403
+      answer(response, status, cosError(status === 404 ? 'NoSuchKey' : 'AccessDenied'),
+        { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
+    })
+    expect(await cos.read(key)).toBeNull()
+    await expect(cos.read(key)).rejects.toThrow()
+    expect(loopback.requests).toHaveLength(2)
+  })
+
+  it('fails a read whose transfer ends before the declared length', async () => {
+    const { store: cos, loopback } = await store((_request, response) => {
+      response.writeHead(200, { 'content-length': '12', connection: 'close' })
+      response.write('part')
+      response.socket?.destroy()
+    })
+    await expect(cos.read(key)).rejects.toThrow()
+    expect(loopback.requests).toHaveLength(1)
   })
 
   it('hashes public response bytes at the exact URL and rejects redirects or unrelated destinations', async () => {
     const fetch = vi.fn(async () => new Response('public bytes'))
     vi.stubGlobal('fetch', fetch)
-    const store = createInstalledUpdateCos()
-    try {
-      const url = `https://download-test.deepseek.com/${key}`
-      expect(await store.publicRead(url)).toEqual({ size: 12, sha512: createHash('sha512').update('public bytes').digest('base64') })
-      expect(fetch).toHaveBeenCalledWith(url, expect.objectContaining({ redirect: 'error', cache: 'no-store' }))
-      await expect(store.publicRead(`${url}?fresh=1`)).rejects.toThrow('exact test public URL')
-      await expect(store.publicRead(url.replace('download-test', 'download'))).rejects.toThrow('exact test public URL')
-      await expect(store.read('dsh-desk/feeds/nightly.yml')).rejects.toThrow('qualification namespace')
-    } finally { store.dispose() }
+    const { store: cos } = await store((_request, response) => { answer(response) })
+    const url = `https://download-test.deepseek.com/${key}`
+    expect(await cos.publicRead(url)).toEqual({ size: 12, sha512: createHash('sha512').update('public bytes').digest('base64') })
+    expect(fetch).toHaveBeenCalledWith(url, expect.objectContaining({ redirect: 'error', cache: 'no-store' }))
+    await expect(cos.publicRead(`${url}?fresh=1`)).rejects.toThrow('exact test public URL')
+    await expect(cos.publicRead(url.replace('download-test', 'download'))).rejects.toThrow('exact test public URL')
+    await expect(cos.read('dsh-desk/feeds/nightly.yml')).rejects.toThrow('qualification namespace')
   })
 
-  it('refuses production settings without creating a request', () => {
+  it('refuses production settings without creating a request', async () => {
     state.settings = 'production'
+    const loopback = await startCosLoopback((_request, response) => { answer(response) })
+    loopbacks.push(loopback)
+    state.redirect = (cos) => { loopback.redirect(cos) }
     expect(() => createInstalledUpdateCos()).toThrow('test upload settings')
-    expect(state.requests).toHaveLength(0)
+    expect(loopback.requests).toHaveLength(0)
   })
 
   it('rejects mismatched upload bytes and unsafe keys before sending a request', async () => {
-    const store = createInstalledUpdateCos()
-    try {
-      await expect(store.put(key, { source: { contents: 'changed' }, size: 1, sha512: 'wrong', forbidOverwrite: true }))
-        .rejects.toThrow('input bytes changed')
-      await expect(store.read(key.replace('package.exe', '..'))).rejects.toThrow('qualification namespace')
-      expect(state.requests).toHaveLength(0)
-    } finally { store.dispose() }
+    const { store: cos, loopback } = await store((_request, response) => { answer(response) })
+    await expect(cos.put(key, { source: { contents: 'changed' }, size: 1, sha512: 'wrong', forbidOverwrite: true }))
+      .rejects.toThrow('input bytes changed')
+    await expect(cos.read(key.replace('package.exe', '..'))).rejects.toThrow('qualification namespace')
+    expect(loopback.requests).toHaveLength(0)
   })
 })

+ 2 - 1
apps/desktop/tests/installed-update-publication.spec.ts

@@ -26,7 +26,7 @@ class Store implements InstalledUpdatePublicationStore {
   }
   async put(key: string, object: Parameters<InstalledUpdatePublicationStore['put']>[1]) {
     this.writes.push({ key, immutable: object.forbidOverwrite })
-    if (this.failure === 'write') throw new Error('fixture-secret-must-not-be-recorded')
+    if (this.failure === 'write') throw Object.assign(new Error('fixture-secret-must-not-be-recorded'), { statusCode: 503 })
     if (object.forbidOverwrite && this.objects.has(key)) throw new Error('object exists')
     const bytes = 'path' in object.source ? await readFile(object.source.path) : Buffer.from(object.source.contents)
     expect(digest(bytes)).toEqual({ sha512: object.sha512, size: object.size })
@@ -154,6 +154,7 @@ describe('qualification publication sequencing', () => {
         const results = await Promise.all(paths.map(path => readFile(join(records, path, 'result.json'), 'utf8')))
         const record = results.find(value => (JSON.parse(value) as { success: unknown }).success === false)!
         expect(JSON.parse(record)).toMatchObject({ success: false })
+        if (failure === 'write') expect(JSON.parse(record)).toMatchObject({ httpStatus: 503 })
         expect(record).not.toContain('fixture-secret')
         expect(await readdir(join(manifest, '..'))).not.toContain('publication.lock')
       })

+ 2 - 2
apps/desktop/tests/installed-update/publication/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/tests/installed-update/publication/README.md
-README.md: 168a54f84b213a0818e9e82b94d02dedcaa0dfb9
-README.zh.md: 6ae2ee749a7163e116ddaeb3526fae1f4f7444f5
+README.md: ae91020f4d14cb6d9190562c873afc5ee2c7d072
+README.zh.md: fa119a23718dedc341bc0d6150ef22b902696251

+ 2 - 0
apps/desktop/tests/installed-update/publication/README.md

@@ -61,6 +61,8 @@ Diagnose `403 AccessDenied` by the exact API: `GetBucketVersioning`, `GetObject`
 
 A manually requested later operation verifies matching objects without rewriting them. `alreadyPublished: true` means reconciliation of an existing desired feed, not a new publication; use the original operation's evidence for publication timing. Unexpected feed bytes stop the operation, including attempted replacement of version 2 with version 1. A crash can leave `publication.lock`; confirm no publisher remains and preserve failed records before an operator removes that exact empty directory. Never delete a lock to bypass an active publisher.
 
+Qualification COS version queries have a 30-second total deadline, and object reads and PUTs have a 15-minute total deadline. Expiration aborts the underlying HTTP requests and waits for closure before releasing the operation lock; ongoing transfer activity does not extend the budget.
+
 <a id="dev-note"></a>
 
 ## Dev Note

+ 2 - 0
apps/desktop/tests/installed-update/publication/README.zh.md

@@ -61,6 +61,8 @@ node --import tsx apps/desktop/scripts/publish-installed-update.ts publish-feed
 
 人工请求的后续操作验证已匹配对象,不重写它们。`alreadyPublished: true` 表示核对已存在的目标 feed,不是一次新发布;发布时间使用原操作证据。非预期 feed 字节会使操作停止,包括尝试用版本 1 替换版本 2。崩溃可能留下 `publication.lock`;确认没有发布者运行并保留失败记录后,才由操作者移除这个准确的空目录。绝不为了绕过活跃发布者而删除锁。
 
+qualification COS 版本查询的总截止时间为 30 秒,对象读取与 PUT 的总截止时间为 15 分钟。到期会中止底层 HTTP 请求,并在释放操作锁前等待请求关闭;持续传输不会延长时限。
+
 <a id="dev-note"></a>
 
 ## 开发备注

+ 255 - 149
pnpm-lock.yaml

@@ -536,9 +536,6 @@ importers:
         specifier: ^7.8.5
         version: 7.8.5
     devDependencies:
-      '@aws-sdk/client-s3':
-        specifier: 3.1067.0
-        version: 3.1067.0
       '@deepseek-ai/dsh-app-boot':
         specifier: workspace:^
         version: link:../../packages/boot/app-boot
@@ -563,6 +560,9 @@ importers:
       app-builder-lib:
         specifier: 26.15.3
         version: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)
+      cos-nodejs-sdk-v5:
+        specifier: 3.0.0
+        version: 3.0.0
       electron:
         specifier: ^44.0.0
         version: 44.0.0
@@ -12826,9 +12826,6 @@ packages:
     resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
     engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/sha1-browser@5.2.0':
-    resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
-
   '@aws-crypto/sha256-browser@5.2.0':
     resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
 
@@ -12842,26 +12839,14 @@ packages:
   '@aws-crypto/util@5.2.0':
     resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
 
-  '@aws-sdk/checksums@3.1000.29':
-    resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/client-bedrock-runtime@3.1048.0':
     resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/client-s3@3.1067.0':
-    resolution: {integrity: sha512-3f64o9YWzwJ9WzMIC4JlUQiMOm7R/EtkIDyFdj8yaQXuh8SR9ezz2R32UMpvTlVMtpoPan3Uj8oveAHr2UeExw==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/core@3.974.20':
     resolution: {integrity: sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/core@3.977.9':
-    resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/credential-provider-env@3.972.46':
     resolution: {integrity: sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==}
     engines: {node: '>=20.0.0'}
@@ -12902,14 +12887,6 @@ packages:
     resolution: {integrity: sha512-tdbnXbw73ww62ABWP0G0Z/euvFowEEvAoi/zG4NaZo7HJFpfGho/Z65HyVzkJLT1cMsUregr4pTyxljlarT0wA==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/middleware-flexible-checksums@3.974.54':
-    resolution: {integrity: sha512-cDplgLpXZy7MfREXbAeOm8PFT8ibjD5B5rbqxocFR/5rdSbXUPURIAvRToVqjcOR5gHYbEkndKAs+Zw7FfZj1A==}
-    engines: {node: '>=20.0.0'}
-
-  '@aws-sdk/middleware-sdk-s3@3.972.75':
-    resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/middleware-websocket@3.972.28':
     resolution: {integrity: sha512-SCW06Zjugn86pq7+dxGnFcyWJuEWHT753HTU/Vj/OzVxP+NoShwdAr4ynxAcvWL883OgRVbSqW3ohnjIxwXjjw==}
     engines: {node: '>= 14.0.0'}
@@ -12922,10 +12899,6 @@ packages:
     resolution: {integrity: sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/signature-v4-multi-region@3.996.46':
-    resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/token-providers@3.1048.0':
     resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==}
     engines: {node: '>=20.0.0'}
@@ -12938,10 +12911,6 @@ packages:
     resolution: {integrity: sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/types@3.974.5':
-    resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==}
-    engines: {node: '>=20.0.0'}
-
   '@aws-sdk/util-locate-window@3.965.7':
     resolution: {integrity: sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==}
     engines: {node: '>=20.0.0'}
@@ -12950,18 +12919,10 @@ packages:
     resolution: {integrity: sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==}
     engines: {node: '>=20.0.0'}
 
-  '@aws-sdk/xml-builder@3.972.40':
-    resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==}
-    engines: {node: '>=20.0.0'}
-
   '@aws/lambda-invoke-store@0.2.4':
     resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==}
     engines: {node: '>=18.0.0'}
 
-  '@aws/lambda-invoke-store@0.3.0':
-    resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
-    engines: {node: '>=18.0.0'}
-
   '@babel/code-frame@7.29.7':
     resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
     engines: {node: '>=6.9.0'}
@@ -15146,10 +15107,6 @@ packages:
     resolution: {integrity: sha512-KoUi4M1f3BG6kzN1FnCwL7oyFptTbyBJKjR6yhSib+JHRdUmM1o+VwsFtJ66NZCkCzVfJMWRHJNo0R0jznp0Pg==}
     engines: {node: '>=18.0.0'}
 
-  '@smithy/core@3.33.3':
-    resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==}
-    engines: {node: '>=18.0.0'}
-
   '@smithy/credential-provider-imds@4.3.9':
     resolution: {integrity: sha512-ZlfJ/4Fa3jYb+3eaohPfG9utX9HmdhFNcFtpoGAhUhdynAOmGXtmigbi7eEiONKM+ykHw8RwKuDEb85Lx7t7fA==}
     engines: {node: '>=18.0.0'}
@@ -15174,18 +15131,10 @@ packages:
     resolution: {integrity: sha512-LwQZazFayImv+IOm0S0enoLeUJwmAlhGC5O6YCcLWezyu08dF46GOxPOq35OpBIHkgd7OvNvBStIFwVNyrvoBw==}
     engines: {node: '>=18.0.0'}
 
-  '@smithy/signature-v4@5.7.3':
-    resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
-    engines: {node: '>=18.0.0'}
-
   '@smithy/types@4.14.4':
     resolution: {integrity: sha512-B2S9+UGm1+/pHkcx3ZoLVX1a+pmSk8rqxRR+ZsNqZaJ5q9FWX9AFGQVM4qG5+OBeQUZVy99HY8HqW8gK/wgXzQ==}
     engines: {node: '>=18.0.0'}
 
-  '@smithy/types@4.17.2':
-    resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==}
-    engines: {node: '>=18.0.0'}
-
   '@smithy/util-buffer-from@2.2.0':
     resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
     engines: {node: '>=14.0.0'}
@@ -15950,10 +15899,17 @@ packages:
   aria-query@5.3.0:
     resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
 
+  asn1@0.2.6:
+    resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
+
   asn1js@3.0.10:
     resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==}
     engines: {node: '>=12.0.0'}
 
+  assert-plus@1.0.0:
+    resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
+    engines: {node: '>=0.8'}
+
   assertion-error@2.0.1:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
     engines: {node: '>=12'}
@@ -15979,6 +15935,9 @@ packages:
     resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
     engines: {node: '>= 4.0.0'}
 
+  aws-sign2@0.7.0:
+    resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
+
   aws4@1.13.2:
     resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
 
@@ -16046,6 +16005,9 @@ packages:
     resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
     engines: {node: '>= 0.8'}
 
+  bcrypt-pbkdf@1.0.2:
+    resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
+
   bidi-js@1.0.3:
     resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
 
@@ -16153,6 +16115,9 @@ packages:
   caniuse-lite@1.0.30001806:
     resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
 
+  caseless@0.12.0:
+    resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
+
   ccount@2.0.1:
     resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
 
@@ -16306,6 +16271,9 @@ packages:
     resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
     engines: {node: '>=18'}
 
+  core-util-is@1.0.2:
+    resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+
   core-util-is@1.0.3:
     resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
 
@@ -16317,6 +16285,19 @@ packages:
     resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==}
     engines: {node: '>= 0.4.0'}
 
+  cos-fast-xml-parser@1.0.0:
+    resolution: {integrity: sha512-kOPJb1cuj+gc5E8jN5ekZn4rgQHaxYTLcQT+jmbHtlTNcNjv7wnOBEYH2uRA0pG3h8giot4z9h4WFLDuXATZwg==}
+    engines: {node: '>= 9'}
+    hasBin: true
+
+  cos-nodejs-sdk-v5@3.0.0:
+    resolution: {integrity: sha512-xUqiDdUxfEjfaWoyBA3qsSnlQVHPz13DRFErL+NliadBVUeeZhAPHGVa2inGpUvPTOs52ALIKkMbzL+inSqvXg==}
+    engines: {node: '>= 9'}
+
+  cos-request@1.3.3:
+    resolution: {integrity: sha512-zD7fKMAIMfJNHssx8VZE5mUQ67hs3QDi7S2BX7JuD8MzT1XYqEOAN42PJ0BtRhgutnn+8HiUF+Fup+j5zORasQ==}
+    engines: {node: '>= 8'}
+
   cose-base@1.0.3:
     resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
 
@@ -16493,6 +16474,10 @@ packages:
   dagre-d3-es@7.0.14:
     resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
 
+  dashdash@1.14.1:
+    resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
+    engines: {node: '>=0.10'}
+
   data-uri-to-buffer@4.0.1:
     resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
     engines: {node: '>= 12'}
@@ -16637,6 +16622,9 @@ packages:
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
+  ecc-jsbn@0.1.2:
+    resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
+
   ecdsa-sig-formatter@1.0.11:
     resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
 
@@ -16891,6 +16879,10 @@ packages:
     engines: {node: '>= 10.17.0'}
     hasBin: true
 
+  extsprintf@1.3.0:
+    resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
+    engines: {'0': node >=0.6.0}
+
   fast-check@4.8.0:
     resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==}
     engines: {node: '>=12.17.0'}
@@ -16978,9 +16970,16 @@ packages:
     resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
     engines: {node: '>=14'}
 
+  forever-agent@0.6.1:
+    resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
+
   form-data-encoder@1.7.2:
     resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
 
+  form-data@2.5.6:
+    resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==}
+    engines: {node: '>= 0.12'}
+
   form-data@4.0.6:
     resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
     engines: {node: '>= 6'}
@@ -17087,6 +17086,9 @@ packages:
     resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==}
     engines: {node: '>=20.20.0'}
 
+  getpass@0.1.7:
+    resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
+
   github-from-package@0.0.0:
     resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
 
@@ -17221,6 +17223,10 @@ packages:
     engines: {node: '>=12'}
     hasBin: true
 
+  http-signature@1.2.0:
+    resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
+    engines: {node: '>=0.8', npm: '>=1.3.7'}
+
   http2-wrapper@1.0.3:
     resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
     engines: {node: '>=10.19.0'}
@@ -17345,6 +17351,9 @@ packages:
     resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
     engines: {node: '>=18'}
 
+  is-typedarray@1.0.0:
+    resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
+
   is-unicode-supported@2.1.0:
     resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
     engines: {node: '>=18'}
@@ -17379,6 +17388,9 @@ packages:
     resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
     engines: {node: '>=20'}
 
+  isstream@0.1.2:
+    resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
+
   istanbul-lib-coverage@3.2.2:
     resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
     engines: {node: '>=8'}
@@ -17420,6 +17432,9 @@ packages:
     resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
     hasBin: true
 
+  jsbn@0.1.1:
+    resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
+
   jscpd-darwin-arm64@5.0.12:
     resolution: {integrity: sha512-CQCDYhQY9yOGGeauzkRGYW/QtXurPCjfDkEhUoM/M5UdmpzxfG3i9mnNsaheJ0RdFRf73mL7JaLVRP8U2l5/kA==}
     cpu: [arm64]
@@ -17491,6 +17506,9 @@ packages:
   json-schema-typed@8.0.2:
     resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
 
+  json-schema@0.4.0:
+    resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
   json-stable-stringify-without-jsonify@1.0.1:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
 
@@ -17508,6 +17526,10 @@ packages:
   jsonfile@6.2.1:
     resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
 
+  jsprim@1.4.2:
+    resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
+    engines: {node: '>=0.6.0'}
+
   jsx-ast-utils-x@0.1.0:
     resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -18131,6 +18153,9 @@ packages:
     resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
     engines: {node: '>=18'}
 
+  oauth-sign@0.9.0:
+    resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
+
   object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
@@ -18299,6 +18324,9 @@ packages:
   perfect-debounce@1.0.0:
     resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
 
+  performance-now@2.1.0:
+    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -18425,6 +18453,9 @@ packages:
     resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
     engines: {node: '>= 0.10'}
 
+  psl@1.15.0:
+    resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
+
   publint@0.3.21:
     resolution: {integrity: sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==}
     engines: {node: '>=18'}
@@ -18454,6 +18485,9 @@ packages:
   quansync@1.0.0:
     resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
 
+  querystringify@2.2.0:
+    resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+
   quick-lru@5.1.1:
     resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
     engines: {node: '>=10'}
@@ -18805,6 +18839,11 @@ packages:
   sprintf-js@1.1.3:
     resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
 
+  sshpk@1.18.0:
+    resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
+    engines: {node: '>=0.10.0'}
+    hasBin: true
+
   stackback@0.0.2:
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 
@@ -18869,6 +18908,9 @@ packages:
     resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
     engines: {node: '>=0.10.0'}
 
+  strnum@1.1.2:
+    resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
+
   strnum@2.4.0:
     resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==}
 
@@ -18970,6 +19012,10 @@ packages:
     resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
     engines: {node: '>=0.6'}
 
+  tough-cookie@4.1.4:
+    resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
+    engines: {node: '>=6'}
+
   tough-cookie@6.0.1:
     resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
     engines: {node: '>=16'}
@@ -19064,6 +19110,9 @@ packages:
     resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==}
     engines: {node: '>=18', npm: '>=9'}
 
+  tweetnacl@0.14.5:
+    resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
+
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
@@ -19149,6 +19198,10 @@ packages:
     resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
     engines: {node: '>= 4.0.0'}
 
+  universalify@0.2.0:
+    resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+    engines: {node: '>= 4.0.0'}
+
   universalify@2.0.1:
     resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
     engines: {node: '>= 10.0.0'}
@@ -19172,6 +19225,9 @@ packages:
   url-join@4.0.1:
     resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
 
+  url-parse@1.5.10:
+    resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+
   use-sync-external-store@1.2.0:
     resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
     peerDependencies:
@@ -19191,6 +19247,10 @@ packages:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
 
+  verror@1.10.0:
+    resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
+    engines: {'0': node >=0.6.0}
+
   vfile-message@4.0.3:
     resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
 
@@ -19796,15 +19856,6 @@ snapshots:
       '@aws-sdk/types': 3.973.12
       tslib: 2.8.1
 
-  '@aws-crypto/sha1-browser@5.2.0':
-    dependencies:
-      '@aws-crypto/supports-web-crypto': 5.2.0
-      '@aws-crypto/util': 5.2.0
-      '@aws-sdk/types': 3.973.12
-      '@aws-sdk/util-locate-window': 3.965.7
-      '@smithy/util-utf8': 2.3.0
-      tslib: 2.8.1
-
   '@aws-crypto/sha256-browser@5.2.0':
     dependencies:
       '@aws-crypto/sha256-js': 5.2.0
@@ -19831,14 +19882,6 @@ snapshots:
       '@smithy/util-utf8': 2.3.0
       tslib: 2.8.1
 
-  '@aws-sdk/checksums@3.1000.29':
-    dependencies:
-      '@aws-sdk/core': 3.977.9
-      '@aws-sdk/types': 3.974.5
-      '@smithy/core': 3.33.3
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@aws-sdk/client-bedrock-runtime@3.1048.0':
     dependencies:
       '@aws-crypto/sha256-browser': 5.2.0
@@ -19856,23 +19899,6 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@aws-sdk/client-s3@3.1067.0':
-    dependencies:
-      '@aws-crypto/sha1-browser': 5.2.0
-      '@aws-crypto/sha256-browser': 5.2.0
-      '@aws-crypto/sha256-js': 5.2.0
-      '@aws-sdk/core': 3.974.20
-      '@aws-sdk/credential-provider-node': 3.972.55
-      '@aws-sdk/middleware-flexible-checksums': 3.974.54
-      '@aws-sdk/middleware-sdk-s3': 3.972.75
-      '@aws-sdk/signature-v4-multi-region': 3.996.34
-      '@aws-sdk/types': 3.973.12
-      '@smithy/core': 3.24.7
-      '@smithy/fetch-http-handler': 5.4.7
-      '@smithy/node-http-handler': 4.7.8
-      '@smithy/types': 4.14.4
-      tslib: 2.8.1
-
   '@aws-sdk/core@3.974.20':
     dependencies:
       '@aws-sdk/types': 3.973.12
@@ -19884,17 +19910,6 @@ snapshots:
       bowser: 2.14.1
       tslib: 2.8.1
 
-  '@aws-sdk/core@3.977.9':
-    dependencies:
-      '@aws-sdk/types': 3.974.5
-      '@aws-sdk/xml-builder': 3.972.40
-      '@aws/lambda-invoke-store': 0.3.0
-      '@smithy/core': 3.33.3
-      '@smithy/signature-v4': 5.7.3
-      '@smithy/types': 4.17.2
-      bowser: 2.14.1
-      tslib: 2.8.1
-
   '@aws-sdk/credential-provider-env@3.972.46':
     dependencies:
       '@aws-sdk/core': 3.974.20
@@ -19993,20 +20008,6 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@aws-sdk/middleware-flexible-checksums@3.974.54':
-    dependencies:
-      '@aws-sdk/checksums': 3.1000.29
-      tslib: 2.8.1
-
-  '@aws-sdk/middleware-sdk-s3@3.972.75':
-    dependencies:
-      '@aws-sdk/core': 3.977.9
-      '@aws-sdk/signature-v4-multi-region': 3.996.46
-      '@aws-sdk/types': 3.974.5
-      '@smithy/core': 3.33.3
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@aws-sdk/middleware-websocket@3.972.28':
     dependencies:
       '@aws-sdk/core': 3.974.20
@@ -20037,13 +20038,6 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@aws-sdk/signature-v4-multi-region@3.996.46':
-    dependencies:
-      '@aws-sdk/types': 3.974.5
-      '@smithy/signature-v4': 5.7.3
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@aws-sdk/token-providers@3.1048.0':
     dependencies:
       '@aws-sdk/core': 3.974.20
@@ -20067,11 +20061,6 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@aws-sdk/types@3.974.5':
-    dependencies:
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@aws-sdk/util-locate-window@3.965.7':
     dependencies:
       tslib: 2.8.1
@@ -20082,15 +20071,8 @@ snapshots:
       fast-xml-parser: 5.7.3
       tslib: 2.8.1
 
-  '@aws-sdk/xml-builder@3.972.40':
-    dependencies:
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@aws/lambda-invoke-store@0.2.4': {}
 
-  '@aws/lambda-invoke-store@0.3.0': {}
-
   '@babel/code-frame@7.29.7':
     dependencies:
       '@babel/helper-validator-identifier': 7.29.7
@@ -21827,11 +21809,6 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@smithy/core@3.33.3':
-    dependencies:
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@smithy/credential-provider-imds@4.3.9':
     dependencies:
       '@smithy/core': 3.24.7
@@ -21866,20 +21843,10 @@ snapshots:
       '@smithy/types': 4.14.4
       tslib: 2.8.1
 
-  '@smithy/signature-v4@5.7.3':
-    dependencies:
-      '@smithy/core': 3.33.3
-      '@smithy/types': 4.17.2
-      tslib: 2.8.1
-
   '@smithy/types@4.14.4':
     dependencies:
       tslib: 2.8.1
 
-  '@smithy/types@4.17.2':
-    dependencies:
-      tslib: 2.8.1
-
   '@smithy/util-buffer-from@2.2.0':
     dependencies:
       '@smithy/is-array-buffer': 2.2.0
@@ -22776,12 +22743,18 @@ snapshots:
     dependencies:
       dequal: 2.0.3
 
+  asn1@0.2.6:
+    dependencies:
+      safer-buffer: 2.1.2
+
   asn1js@3.0.10:
     dependencies:
       pvtsutils: 1.3.6
       pvutils: 1.2.0
       tslib: 2.8.1
 
+  assert-plus@1.0.0: {}
+
   assertion-error@2.0.1: {}
 
   ast-kit@3.0.0-beta.1:
@@ -22804,6 +22777,8 @@ snapshots:
 
   at-least-node@1.0.0: {}
 
+  aws-sign2@0.7.0: {}
+
   aws4@1.13.2: {}
 
   b4a@1.8.1: {}
@@ -22849,6 +22824,10 @@ snapshots:
     dependencies:
       safe-buffer: 5.1.2
 
+  bcrypt-pbkdf@1.0.2:
+    dependencies:
+      tweetnacl: 0.14.5
+
   bidi-js@1.0.3:
     dependencies:
       require-from-string: 2.0.2
@@ -22987,6 +22966,8 @@ snapshots:
 
   caniuse-lite@1.0.30001806: {}
 
+  caseless@0.12.0: {}
+
   ccount@2.0.1: {}
 
   chai@6.2.2: {}
@@ -23104,6 +23085,8 @@ snapshots:
     dependencies:
       is-what: 5.5.0
 
+  core-util-is@1.0.2: {}
+
   core-util-is@1.0.3: {}
 
   cors@2.8.6:
@@ -23113,6 +23096,37 @@ snapshots:
 
   corser@2.0.1: {}
 
+  cos-fast-xml-parser@1.0.0:
+    dependencies:
+      strnum: 1.1.2
+
+  cos-nodejs-sdk-v5@3.0.0:
+    dependencies:
+      cos-fast-xml-parser: 1.0.0
+      cos-request: 1.3.3
+      mime-types: 2.1.35
+
+  cos-request@1.3.3:
+    dependencies:
+      aws-sign2: 0.7.0
+      aws4: 1.13.2
+      caseless: 0.12.0
+      combined-stream: 1.0.8
+      extend: 3.0.2
+      forever-agent: 0.6.1
+      form-data: 2.5.6
+      http-signature: 1.2.0
+      is-typedarray: 1.0.0
+      isstream: 0.1.2
+      json-stringify-safe: 5.0.1
+      mime-types: 2.1.35
+      oauth-sign: 0.9.0
+      performance-now: 2.1.0
+      qs: 6.15.3
+      safe-buffer: 5.2.1
+      tough-cookie: 4.1.4
+      tunnel-agent: 0.6.0
+
   cose-base@1.0.3:
     dependencies:
       layout-base: 1.0.2
@@ -23321,6 +23335,10 @@ snapshots:
       d3: 7.9.0
       lodash-es: 4.18.1
 
+  dashdash@1.14.1:
+    dependencies:
+      assert-plus: 1.0.0
+
   data-uri-to-buffer@4.0.1: {}
 
   data-urls@7.0.0(@noble/hashes@2.3.0):
@@ -23451,6 +23469,11 @@ snapshots:
 
   eastasianwidth@0.2.0: {}
 
+  ecc-jsbn@0.1.2:
+    dependencies:
+      jsbn: 0.1.1
+      safer-buffer: 2.1.2
+
   ecdsa-sig-formatter@1.0.11:
     dependencies:
       safe-buffer: 5.2.1
@@ -23864,6 +23887,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  extsprintf@1.3.0: {}
+
   fast-check@4.8.0:
     dependencies:
       pure-rand: 8.4.0
@@ -23949,8 +23974,19 @@ snapshots:
       cross-spawn: 7.0.6
       signal-exit: 4.1.0
 
+  forever-agent@0.6.1: {}
+
   form-data-encoder@1.7.2: {}
 
+  form-data@2.5.6:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.4
+      mime-types: 2.1.35
+      safe-buffer: 5.2.1
+
   form-data@4.0.6:
     dependencies:
       asynckit: 0.4.0
@@ -24076,6 +24112,10 @@ snapshots:
     dependencies:
       resolve-pkg-maps: 1.0.0
 
+  getpass@0.1.7:
+    dependencies:
+      assert-plus: 1.0.0
+
   github-from-package@0.0.0: {}
 
   glob-parent@6.0.2:
@@ -24271,6 +24311,12 @@ snapshots:
       - debug
       - supports-color
 
+  http-signature@1.2.0:
+    dependencies:
+      assert-plus: 1.0.0
+      jsprim: 1.4.2
+      sshpk: 1.18.0
+
   http2-wrapper@1.0.3:
     dependencies:
       quick-lru: 5.1.1
@@ -24360,6 +24406,8 @@ snapshots:
 
   is-stream@4.0.1: {}
 
+  is-typedarray@1.0.0: {}
+
   is-unicode-supported@2.1.0: {}
 
   is-what@5.5.0: {}
@@ -24380,6 +24428,8 @@ snapshots:
 
   isexe@4.0.0: {}
 
+  isstream@0.1.2: {}
+
   istanbul-lib-coverage@3.2.2: {}
 
   istanbul-lib-report@3.0.1:
@@ -24421,6 +24471,8 @@ snapshots:
     dependencies:
       argparse: 2.0.1
 
+  jsbn@0.1.1: {}
+
   jscpd-darwin-arm64@5.0.12:
     optional: true
 
@@ -24493,10 +24545,11 @@ snapshots:
 
   json-schema-typed@8.0.2: {}
 
+  json-schema@0.4.0: {}
+
   json-stable-stringify-without-jsonify@1.0.1: {}
 
-  json-stringify-safe@5.0.1:
-    optional: true
+  json-stringify-safe@5.0.1: {}
 
   json5@2.2.3: {}
 
@@ -24510,6 +24563,13 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
+  jsprim@1.4.2:
+    dependencies:
+      assert-plus: 1.0.0
+      extsprintf: 1.3.0
+      json-schema: 0.4.0
+      verror: 1.10.0
+
   jsx-ast-utils-x@0.1.0: {}
 
   jszip@3.10.1:
@@ -25280,6 +25340,8 @@ snapshots:
       path-key: 4.0.0
       unicorn-magic: 0.3.0
 
+  oauth-sign@0.9.0: {}
+
   object-assign@4.1.1: {}
 
   object-inspect@1.13.4: {}
@@ -25457,6 +25519,8 @@ snapshots:
 
   perfect-debounce@1.0.0: {}
 
+  performance-now@2.1.0: {}
+
   picocolors@1.1.1: {}
 
   picomatch@4.0.4: {}
@@ -25589,6 +25653,10 @@ snapshots:
       forwarded: 0.2.0
       ipaddr.js: 1.9.1
 
+  psl@1.15.0:
+    dependencies:
+      punycode: 2.3.1
+
   publint@0.3.21:
     dependencies:
       '@publint/pack': 0.1.4
@@ -25618,6 +25686,8 @@ snapshots:
 
   quansync@1.0.0: {}
 
+  querystringify@2.2.0: {}
+
   quick-lru@5.1.1: {}
 
   range-parser@1.3.0: {}
@@ -26077,6 +26147,18 @@ snapshots:
   sprintf-js@1.1.3:
     optional: true
 
+  sshpk@1.18.0:
+    dependencies:
+      asn1: 0.2.6
+      assert-plus: 1.0.0
+      bcrypt-pbkdf: 1.0.2
+      dashdash: 1.14.1
+      ecc-jsbn: 0.1.2
+      getpass: 0.1.7
+      jsbn: 0.1.1
+      safer-buffer: 2.1.2
+      tweetnacl: 0.14.5
+
   stackback@0.0.2: {}
 
   standardwebhooks@1.1.1:
@@ -26151,6 +26233,8 @@ snapshots:
 
   strip-json-comments@2.0.1: {}
 
+  strnum@1.1.2: {}
+
   strnum@2.4.0:
     dependencies:
       anynum: 1.0.0
@@ -26279,6 +26363,13 @@ snapshots:
 
   toidentifier@1.0.1: {}
 
+  tough-cookie@4.1.4:
+    dependencies:
+      psl: 1.15.0
+      punycode: 2.3.1
+      universalify: 0.2.0
+      url-parse: 1.5.10
+
   tough-cookie@6.0.1:
     dependencies:
       tldts: 7.4.5
@@ -26352,6 +26443,8 @@ snapshots:
     dependencies:
       '@mixmark-io/domino': 2.2.0
 
+  tweetnacl@0.14.5: {}
+
   type-check@0.4.0:
     dependencies:
       prelude-ls: 1.2.1
@@ -26431,6 +26524,8 @@ snapshots:
 
   universalify@0.1.2: {}
 
+  universalify@0.2.0: {}
+
   universalify@2.0.1: {}
 
   unpipe@1.0.0: {}
@@ -26455,6 +26550,11 @@ snapshots:
 
   url-join@4.0.1: {}
 
+  url-parse@1.5.10:
+    dependencies:
+      querystringify: 2.2.0
+      requires-port: 1.0.0
+
   use-sync-external-store@1.2.0(react@18.3.1):
     dependencies:
       react: 18.3.1
@@ -26467,6 +26567,12 @@ snapshots:
 
   vary@1.1.2: {}
 
+  verror@1.10.0:
+    dependencies:
+      assert-plus: 1.0.0
+      core-util-is: 1.0.2
+      extsprintf: 1.3.0
+
   vfile-message@4.0.3:
     dependencies:
       '@types/unist': 3.0.3

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov