fix(desktop): upload releases with Tencent COS SDK
This commit is contained in:
1 parent
9fefcd7c06
commit
e6c507d65f
27 files changed
+1061
-400
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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,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
|
||||
@@ -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.
|
||||
@@ -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)负责;本记录改变的是实现它的传输。
|
||||
@@ -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,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
|
||||
@@ -128,7 +128,7 @@ The Windows installer extracts the new version beside the installation directory
|
||||
|
||||
### Upload updates
|
||||
|
||||
Test and production uploads through `upload:*` retain a fresh `.desktop-build/upload-records/<environment>-<target>-*` directory after release preflight. `plan.json` records destination, version, every file's size/SHA-512, and published YAML bytes; flushed `events.jsonl` records PUT intent and available response status/request ID; `result.json` records completion or the last failed stage. A missing final result means interruption or unavailable storage, not success. No credential values, authorization headers, or raw SDK errors are recorded. Audit-write failures stop later PUTs, and SDK writes have no automatic retries. Retain partial records and inspect remote state before another operation: a timeout or failed receipt write does not prove that the object was not stored. These records are local, not tamper-proof or automatically backed up; archive each release's records with its build evidence in controlled storage. Public CDN readback remains separate release qualification, explicitly marked `not-performed` in the upload result.
|
||||
Test and production uploads through `upload:*` retain a fresh `.desktop-build/upload-records/<environment>-<target>-*` directory after release preflight. `plan.json` records destination, version, every file's size/SHA-512, and published YAML bytes; flushed `events.jsonl` records PUT intent and available response status/request ID; `result.json` records completion or the last failed stage. A missing final result means interruption or unavailable storage, not success. No credential values, authorization headers, or raw SDK errors are recorded. Audit-write failures stop later PUTs. Every object is one streamed Tencent COS PUT with an explicit length and Content-MD5; the COS SDK repeats a request only when its body is not a stream, and this uploader does not retry either. Retain partial records and inspect remote state before another operation: a timeout or failed receipt write does not prove that the object was not stored. These records are local, not tamper-proof or automatically backed up; archive each release's records with its build evidence in controlled storage. Public CDN readback remains separate release qualification, explicitly marked `not-performed` in the upload result.
|
||||
|
||||
Windows operators can keep a CLIXML object with DPAPI-encrypted `SecretId` and `SecretKey` SecureString fields outside the repository. The [credential launcher](scripts/upload-with-credentials.ps1) requires an explicit `-CredentialFile` and `-Environment production` or `test`; without `-Upload`, it only verifies decryption and injection into a local Node child, with no network request. It requires Node on `PATH` and the Windows user and machine that encrypted the file. Plaintext, empty, and whitespace-only fields fail. The parent environment is unchanged; the child receives only the selected COS pair after unrelated secrets and Node preload options are removed. Raw child stderr is suppressed and credential values in stdout are redacted. This check does not prove COS authorization. An explicit upload additionally requires `-Upload -Target <target> -Bucket <bucket>` and the normal completed-release prerequisites below; actual cloud upload remains release-operator qualification. This launcher supports permanent keys, not STS credentials, and never changes the packaged update destination.
|
||||
|
||||
@@ -150,7 +150,7 @@ pnpm run upload:mac:arm64
|
||||
|
||||
The target dotenv file for internal-test packaging explicitly sets `DSH_DESKTOP_AUTO_UPDATE_ENV=test` and `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`; uploads use `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` and separate test credentials. Both test and production use the fixed Nightly channel; deployment selection does not enable channel switching.
|
||||
|
||||
Early internal-test packages use the `test` deployment. Select `production` explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Every release publishes `nightly.yml` or `nightly-mac.yml`; stable releases also publish `latest.yml` or `latest-mac.yml` pointing at the same artifacts. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification.
|
||||
Early internal-test packages use the `test` deployment. Select `production` explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Every release publishes `nightly.yml` or `nightly-mac.yml`; stable releases also publish `latest.yml` or `latest-mac.yml` pointing at the same artifacts. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset, including the empty header the COS SDK would otherwise add: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification.
|
||||
|
||||
The macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>`.
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Windows 安装器先将新版本解压到安装目录旁边,再退出旧应用
|
||||
|
||||
### 上传更新
|
||||
|
||||
test 与 production 的 `upload:*` 上传在发布前置检查通过后,分别保留新的 `.desktop-build/upload-records/<environment>-<target>-*` 目录。`plan.json` 记录目标、版本、每个文件的大小/SHA-512 和发布的 YAML 字节;刷盘的 `events.jsonl` 记录 PUT 意图及可用的响应状态/请求 ID;`result.json` 记录完成结果或最后失败阶段。缺少最终结果表示中断或存储不可用,不表示成功。不记录凭据值、认证头或原始 SDK 错误。审计写入失败即停止后续 PUT,SDK 写入没有自动重试。保留部分记录,检查远端状态后再执行下一次操作:超时或回执写入失败不能证明对象未存储。这些记录仅在本地,不防篡改,也不会自动备份;每次发布应将它们与构建证据一同归档到受控存储。公网 CDN 回读仍是单独的发布验收,上传结果明确标记为 `not-performed`。
|
||||
test 与 production 的 `upload:*` 上传在发布前置检查通过后,分别保留新的 `.desktop-build/upload-records/<environment>-<target>-*` 目录。`plan.json` 记录目标、版本、每个文件的大小/SHA-512 和发布的 YAML 字节;刷盘的 `events.jsonl` 记录 PUT 意图及可用的响应状态/请求 ID;`result.json` 记录完成结果或最后失败阶段。缺少最终结果表示中断或存储不可用,不表示成功。不记录凭据值、认证头或原始 SDK 错误。审计写入失败即停止后续 PUT。每个对象都以一次流式腾讯 COS PUT 上传,并携带显式长度与 Content-MD5;COS SDK 仅在请求体不是流时才会重发请求,上传器自身也不重试。保留部分记录,检查远端状态后再执行下一次操作:超时或回执写入失败不能证明对象未存储。这些记录仅在本地,不防篡改,也不会自动备份;每次发布应将它们与构建证据一同归档到受控存储。公网 CDN 回读仍是单独的发布验收,上传结果明确标记为 `not-performed`。
|
||||
|
||||
Windows 操作人员可以在仓库外保存 CLIXML 对象,其中 `SecretId` 和 `SecretKey` 是经 DPAPI 加密的 SecureString 字段。[凭据启动器](scripts/upload-with-credentials.ps1)要求显式提供 `-CredentialFile` 和 `-Environment production` 或 `test`;不指定 `-Upload` 时,只验证解密以及向本地 Node 子进程注入凭据,不发起网络请求。它要求 `PATH` 中有 Node,并使用加密该文件时的 Windows 用户和机器。明文、空字段及纯空白字段都会失败。父进程环境保持不变;子进程先清除无关密钥与 Node 预加载选项,再仅接收所选 COS 凭据对。原始子进程 stderr 不会显示,stdout 中的凭据值会被遮盖。此检查不能证明 COS 授权有效。显式上传还要求 `-Upload -Target <target> -Bucket <bucket>` 及下述常规发布完成前提;真实云端上传仍需发布操作人员验收。此启动器支持长期密钥,不支持 STS 凭据,也不会改变安装包中的更新目标。
|
||||
|
||||
@@ -150,7 +150,7 @@ pnpm run upload:mac:arm64
|
||||
|
||||
内测打包在目标 `.env` 中显式设置 `DSH_DESKTOP_AUTO_UPDATE_ENV=test` 和 `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`;上传使用 `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` 及独立测试凭据。test 和 production 都使用固定 Nightly 通道,部署选择不提供通道切换。
|
||||
|
||||
前期内测包使用 `test` 部署。只有正式发布才显式选择 `production`;更换上传凭据不会改变已有安装包的更新目标。打包不需要 COS 凭据,会禁用 electron-builder 发布、移除子进程的 COS 凭据,并且仅在签名与公证成功后记录完成状态。上传在读取凭据前验证该记录、部署、目标、共同版本号、文件名、大小与 SHA-512。安装包和 blockmap 先于 YAML 上传;历史对象继续保留。每个版本发布 `nightly.yml` 或 `nightly-mac.yml`;稳定版本还发布指向相同产物的 `latest.yml` 或 `latest-mac.yml`。发布的 YAML 使用安装包绝对 URL。上传器不设置 Cache-Control:缓存策略由部署基础设施负责,清单不缓存,安装包缓存单独配置。同一目标应串行发布,并在发布验收前验证公网产物与清单内容。
|
||||
前期内测包使用 `test` 部署。只有正式发布才显式选择 `production`;更换上传凭据不会改变已有安装包的更新目标。打包不需要 COS 凭据,会禁用 electron-builder 发布、移除子进程的 COS 凭据,并且仅在签名与公证成功后记录完成状态。上传在读取凭据前验证该记录、部署、目标、共同版本号、文件名、大小与 SHA-512。安装包和 blockmap 先于 YAML 上传;历史对象继续保留。每个版本发布 `nightly.yml` 或 `nightly-mac.yml`;稳定版本还发布指向相同产物的 `latest.yml` 或 `latest-mac.yml`。发布的 YAML 使用安装包绝对 URL。上传器不设置 Cache-Control,包括 COS SDK 否则会添加的空头部:缓存策略由部署基础设施负责,清单不缓存,安装包缓存单独配置。同一目标应串行发布,并在发布验收前验证公网产物与清单内容。
|
||||
|
||||
macOS 配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。仅生成目录的命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
}
|
||||
}
|
||||
@@ -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'}`)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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('refuses networking when the audit parent cannot be created or retries are enabled', async () => {
|
||||
it('cannot repeat the write when a client leaves every SDK retry option at its default', 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 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', async () => {
|
||||
const f = await fixture()
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -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.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('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([
|
||||
['', 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)
|
||||
})
|
||||
})
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
## 开发备注
|
||||
|
||||
Generated
+255
-149
@@ -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([email protected])([email protected])
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
|
||||
|
||||
@@ -12842,26 +12839,14 @@ packages:
|
||||
'@aws-crypto/[email protected]':
|
||||
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-3f64o9YWzwJ9WzMIC4JlUQiMOm7R/EtkIDyFdj8yaQXuh8SR9ezz2R32UMpvTlVMtpoPan3Uj8oveAHr2UeExw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-cDplgLpXZy7MfREXbAeOm8PFT8ibjD5B5rbqxocFR/5rdSbXUPURIAvRToVqjcOR5gHYbEkndKAs+Zw7FfZj1A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@aws/[email protected]':
|
||||
resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@aws/[email protected]':
|
||||
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
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/[email protected]':
|
||||
resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-B2S9+UGm1+/pHkcx3ZoLVX1a+pmSk8rqxRR+ZsNqZaJ5q9FWX9AFGQVM4qG5+OBeQUZVy99HY8HqW8gK/wgXzQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@smithy/[email protected]':
|
||||
resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -15950,10 +15899,17 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -15979,6 +15935,9 @@ packages:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
@@ -16153,6 +16115,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -16306,6 +16271,9 @@ packages:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@@ -16317,6 +16285,19 @@ packages:
|
||||
resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kOPJb1cuj+gc5E8jN5ekZn4rgQHaxYTLcQT+jmbHtlTNcNjv7wnOBEYH2uRA0pG3h8giot4z9h4WFLDuXATZwg==}
|
||||
engines: {node: '>= 9'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xUqiDdUxfEjfaWoyBA3qsSnlQVHPz13DRFErL+NliadBVUeeZhAPHGVa2inGpUvPTOs52ALIKkMbzL+inSqvXg==}
|
||||
engines: {node: '>= 9'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zD7fKMAIMfJNHssx8VZE5mUQ67hs3QDi7S2BX7JuD8MzT1XYqEOAN42PJ0BtRhgutnn+8HiUF+Fup+j5zORasQ==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||
|
||||
@@ -16493,6 +16474,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
@@ -16637,6 +16622,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
@@ -16891,6 +16879,10 @@ packages:
|
||||
engines: {node: '>= 10.17.0'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
@@ -16978,9 +16970,16 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==}
|
||||
engines: {node: '>= 0.12'}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
@@ -17221,6 +17223,10 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
|
||||
engines: {node: '>=0.8', npm: '>=1.3.7'}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -17379,6 +17388,9 @@ packages:
|
||||
resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-CQCDYhQY9yOGGeauzkRGYW/QtXurPCjfDkEhUoM/M5UdmpzxfG3i9mnNsaheJ0RdFRf73mL7JaLVRP8U2l5/kA==}
|
||||
cpu: [arm64]
|
||||
@@ -17491,6 +17506,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -17508,6 +17526,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -18299,6 +18324,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -18454,6 +18485,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -18805,6 +18839,11 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
@@ -18869,6 +18908,9 @@ packages:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==}
|
||||
|
||||
@@ -18970,6 +19012,10 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -19172,6 +19225,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
@@ -19796,15 +19856,6 @@ snapshots:
|
||||
'@aws-sdk/types': 3.973.12
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-crypto/[email protected]':
|
||||
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/[email protected]':
|
||||
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/[email protected]':
|
||||
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/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 5.2.0
|
||||
@@ -19856,23 +19899,6 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.973.12
|
||||
@@ -19884,17 +19910,6 @@ snapshots:
|
||||
bowser: 2.14.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.974.20
|
||||
@@ -19993,20 +20008,6 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/checksums': 3.1000.29
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
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/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.974.20
|
||||
@@ -20037,13 +20038,6 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/types': 3.974.5
|
||||
'@smithy/signature-v4': 5.7.3
|
||||
'@smithy/types': 4.17.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.974.20
|
||||
@@ -20067,11 +20061,6 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.17.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -20082,15 +20071,8 @@ snapshots:
|
||||
fast-xml-parser: 5.7.3
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.17.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws/[email protected]': {}
|
||||
|
||||
'@aws/[email protected]': {}
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
@@ -21827,11 +21809,6 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/types': 4.17.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/core': 3.24.7
|
||||
@@ -21866,20 +21843,10 @@ snapshots:
|
||||
'@smithy/types': 4.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/core': 3.33.3
|
||||
'@smithy/types': 4.17.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@smithy/[email protected]':
|
||||
dependencies:
|
||||
'@smithy/is-array-buffer': 2.2.0
|
||||
@@ -22776,12 +22743,18 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.6
|
||||
pvutils: 1.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -22804,6 +22777,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -22849,6 +22824,10 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tweetnacl: 0.14.5
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
@@ -22987,6 +22966,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -23104,6 +23085,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -23113,6 +23096,37 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
cos-fast-xml-parser: 1.0.0
|
||||
cos-request: 1.3.3
|
||||
mime-types: 2.1.35
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
layout-base: 1.0.2
|
||||
@@ -23321,6 +23335,10 @@ snapshots:
|
||||
d3: 7.9.0
|
||||
lodash-es: 4.18.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@noble/[email protected]):
|
||||
@@ -23451,6 +23469,11 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
jsbn: 0.1.1
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -23864,6 +23887,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pure-rand: 8.4.0
|
||||
@@ -23949,8 +23974,19 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
@@ -24076,6 +24112,10 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -24271,6 +24311,12 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
jsprim: 1.4.2
|
||||
sshpk: 1.18.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
quick-lru: 5.1.1
|
||||
@@ -24360,6 +24406,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -24380,6 +24428,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -24421,6 +24471,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
@@ -24493,10 +24545,11 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -24510,6 +24563,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
extsprintf: 1.3.0
|
||||
json-schema: 0.4.0
|
||||
verror: 1.10.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -25280,6 +25340,8 @@ snapshots:
|
||||
path-key: 4.0.0
|
||||
unicorn-magic: 0.3.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -25457,6 +25519,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -25589,6 +25653,10 @@ snapshots:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@publint/pack': 0.1.4
|
||||
@@ -25618,6 +25686,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -26077,6 +26147,18 @@ snapshots:
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -26151,6 +26233,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
anynum: 1.0.0
|
||||
@@ -26279,6 +26363,13 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
psl: 1.15.0
|
||||
punycode: 2.3.1
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tldts: 7.4.5
|
||||
@@ -26352,6 +26443,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@mixmark-io/domino': 2.2.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -26431,6 +26524,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -26455,6 +26550,11 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
@@ -26467,6 +26567,12 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
core-util-is: 1.0.2
|
||||
extsprintf: 1.3.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
Reference in new issue
Block a user