@cloudbase/manager-node 5.9.1 → 5.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [5.9.2] 2026-10-21
2
+
3
+ - [Fixed] function: COS 上传超时改为连续无进度判定,避免大包被总耗时误杀
4
+ - [Fixed] utils: 解压远程 zip 增加路径穿越防护并支持失败即中止
5
+ ## [5.9.1] 2026-10-15
6
+
7
+ - [Added] function: CBF 环境支持 PutUrl 直传函数代码包
8
+ - [Added] function: 新增流量策略 V2 与版本删除接口,切换 TCB 协议
9
+ - [Fixed] error: 补齐新增代码的错误 code,通过 D31 与 code 取值来源守卫
10
+ - [Fixed] function: 适配 CBF 环境接口限制并补齐版本/流量与 CBF 集成测试
1
11
  ## @cloudbase/manager-node v5.9.0 版本发布 🚀(2026-09-28)
2
12
 
3
13
  - 新增 错误对象契约统一与无 code 抛错清零 + 自造码文档锚点(D5)
@@ -1,16 +1,4 @@
1
- ## [5.9.0] 2026-09-28
1
+ ## [5.9.2] 2026-10-21
2
2
 
3
- - [Added] Add `ERROR_TYPES` standard error type table (6 entries) and support `type` option in `CloudBaseError` constructor (decides `code` / `source` with priority and cannot be overridden by caller); helpers now reference `ERROR_TYPES`
4
- - [Added] error-docs: locate self-made codes by layer registry page (returns `/error-code/<layer>#<lowercase-code>` anchor when `source` is `cli` / `toolbox` / `manager-node`); manager-node soft-fallback whitelist collapsed to layer registry page (`ready: true`)
5
- - [Added] Unify error object contract and clear no-`code` throws: add `CloudBaseError.toJSON()`, `ErrorSource` union type (with `'toolbox'`), `isCloudBaseError` / `isValidErrorCode`; converge 171 raw `throw new Error` to unified helpers, storage batch delete now carries `code` + `source` + `details`
6
- - [Added] Upgrade `@cloudbase/toolbox` to `~0.9.0`
7
- - [Added] Update cloud function and cloud run integration tests: replace fixed sleep with polling wait, unify jest timeout to 10 minutes, add `waitUntil` / `waitForSandboxToolSettled` helpers with fast-fail diagnostics on failed states
8
- - [Fixed] Fix `function` regression where throw-helper was misused in value position (`reject(throwNetError(...))`) causing Promise to never settle; now construct `new CloudBaseError(msg, { type: ERROR_TYPES.netError })` in value position
9
- - [Fixed] `error`: `toJSON()` normalizes numeric codes to strings (e.g. `876` ⇒ `"876"`) at output layer while `code` on object stays faithful; `NaN` / `±Infinity` fall back to `ERROR`
10
- - [Fixed] `error`: add ES5 prototype fallback (`Object.setPrototypeOf(this, CloudBaseError.prototype)`) to avoid lost prototype chain under third-party downlevel compilation
11
- - [Fixed] Address code review fixes: extract `MODEL_STATUS` constant in `aiModel`; fix `authoration` → `authorization` spelling in `environment` / `cloud-api-request`; use truthy checks in `user` validation; change `error: any` → `unknown` in `function` and use `WeakMap` for idempotency marker; use truthy checks for `customConfiguration` / `DNSConfig` in `sandbox` to avoid null pointer
12
- - [Fixed] `review`: fix empty `failures[0].id` null pointer when deploy report is inconsistent; narrow `description` with `typeof === 'string'` in `permission`; remove `as any` and tighten `isRetryableError` retry logic in `function`
13
- - [Fixed] `deploy`: downgrade image `latest` tag from block (`fail`) to hint (`warn`) since platform allows `latest` as suboptimal not mandatory failure
14
- - [Fixed] `error`: change enterprise image guidance idempotency from `message` text dependency to marker flag to avoid duplicate append after text mutation
15
- - [Fixed] `error`: governance D14/D15/D16/D22 — pass through TCR permission errors; only retry retryable errors in trigger creation; `waitFunctionActive` tolerates creation-phase `NOT_FOUND`; `toJSON()` emits `stack`
16
- - [Refactor] `error`: rename throw helpers to `throwXxx` form (e.g. `invalidParam` → `throwInvalidParam`) so call sites signal interruption, 494 sites / 35 files
3
+ - [Fixed] [TODO: translate] function: COS 上传超时改为连续无进度判定,避免大包被总耗时误杀
4
+ - [Fixed] [TODO: translate] utils: 解压远程 zip 增加路径穿越防护并支持失败即中止
@@ -1806,28 +1806,75 @@ class FunctionService {
1806
1806
  }
1807
1807
  });
1808
1808
  return new Promise((resolve, reject) => {
1809
- // 添加超时保护
1810
- const timeout = setTimeout(() => {
1811
- packer.clean();
1812
- // 同上:值位置不能调用必抛 helper
1813
- reject(new error_1.CloudBaseError(`[${func.name}] COS 上传超时(60秒)`, { type: error_types_1.ERROR_TYPES.netError }));
1814
- }, 60000);
1815
- cos.sliceUploadFile(uploadParams, async (err, data) => {
1816
- clearTimeout(timeout);
1817
- // 清理临时文件
1818
- await packer.clean();
1819
- if (err) {
1820
- // 保留完整的错误信息(避免原始错误丢失)
1821
- const errorMessage = err.message || err.error || String(err);
1822
- const errorCode = err.code || err.statusCode || '';
1823
- reject(new error_1.CloudBaseError(`COS 上传失败: ${errorMessage}${errorCode ? ` (${errorCode})` : ''}`, {
1824
- code: errorCode
1825
- }));
1826
- }
1827
- else {
1828
- resolve(data);
1829
- }
1830
- });
1809
+ // 上传停滞保护:连续 120 秒无进度才判超时(而非总耗时 120 秒)。
1810
+ // 大包在慢网 / 代理下总耗时可能远超 120 秒,只要仍在传输就不应被打断。
1811
+ const STALL_TIMEOUT = 120000;
1812
+ let stallTimer;
1813
+ let taskId;
1814
+ // 超时与上传回调可能先后到达,用 settled 保证只结算一次
1815
+ let settled = false;
1816
+ const clearStallTimer = () => {
1817
+ if (stallTimer)
1818
+ clearTimeout(stallTimer);
1819
+ stallTimer = undefined;
1820
+ };
1821
+ // 清理临时包;失败不影响主流程,也不能留下浮空 Promise
1822
+ const cleanup = () => packer.clean().catch(() => undefined);
1823
+ const settle = (fn) => {
1824
+ if (settled)
1825
+ return;
1826
+ settled = true;
1827
+ clearStallTimer();
1828
+ fn();
1829
+ };
1830
+ // 每收到一次上传进度就重置计时器
1831
+ const resetStallTimer = () => {
1832
+ clearStallTimer();
1833
+ if (settled)
1834
+ return;
1835
+ stallTimer = setTimeout(() => {
1836
+ // cancelTask 只是不再调度后续分片,不会中断已在飞的请求;
1837
+ // 且取消后 SDK 不再回调,临时包必须在这里自行清理
1838
+ if (taskId)
1839
+ cos.cancelTask(taskId);
1840
+ // 同上:值位置不能调用必抛 helper
1841
+ settle(() => reject(new error_1.CloudBaseError(`[${func.name}] COS 上传超时(连续 ${STALL_TIMEOUT / 1000} 秒无进度)`, { type: error_types_1.ERROR_TYPES.netError })));
1842
+ cleanup();
1843
+ }, STALL_TIMEOUT);
1844
+ };
1845
+ resetStallTimer();
1846
+ try {
1847
+ cos.sliceUploadFile(Object.assign(Object.assign({}, uploadParams), { onTaskReady: (id) => {
1848
+ taskId = id;
1849
+ }, onProgress: resetStallTimer }), async (err, data) => {
1850
+ // 已因停滞超时结算过,这里只补一次清理
1851
+ if (settled) {
1852
+ cleanup();
1853
+ return;
1854
+ }
1855
+ // 先置位再 await,避免清理期间定时器再次触发
1856
+ clearStallTimer();
1857
+ settled = true;
1858
+ // 清理临时文件
1859
+ await cleanup();
1860
+ if (err) {
1861
+ // 保留完整的错误信息(避免原始错误丢失)
1862
+ const errorMessage = err.message || err.error || String(err);
1863
+ const errorCode = err.code || err.statusCode || '';
1864
+ reject(new error_1.CloudBaseError(`COS 上传失败: ${errorMessage}${errorCode ? ` (${errorCode})` : ''}`, {
1865
+ code: errorCode
1866
+ }));
1867
+ }
1868
+ else {
1869
+ resolve(data);
1870
+ }
1871
+ });
1872
+ }
1873
+ catch (e) {
1874
+ // 同步抛错时 Promise 已 reject,但 stallTimer 不会自动清理,需在此收尾
1875
+ settle(() => reject(e));
1876
+ cleanup();
1877
+ }
1831
1878
  });
1832
1879
  }
1833
1880
  /**
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.getCompleteTimeRange = exports.formatTimeByMs = exports.formatTimeValue = exports.guid6 = void 0;
21
21
  exports.compressToZip = compressToZip;
22
22
  exports.shouldSkipEntry = shouldSkipEntry;
23
+ exports.safeResolveZipEntryPath = safeResolveZipEntryPath;
23
24
  exports.downloadAndExtractRemoteZip = downloadAndExtractRemoteZip;
24
25
  exports.upload = upload;
25
26
  exports.getRuntime = getRuntime;
@@ -163,12 +164,51 @@ function shouldSkipEntry(entryPath) {
163
164
  return false;
164
165
  });
165
166
  }
167
+ /**
168
+ * 校验 zip 条目路径,返回解析后的绝对路径
169
+ *
170
+ * 条目名来自压缩包本身(不可信),必须确保解析结果仍落在 baseDir 内,
171
+ * 否则 `../` 之类的条目会跳出目标目录,形成任意文件写入(Zip Slip / CWE-22)。
172
+ *
173
+ * @param {string} baseDir 目标根目录(受信任)
174
+ * @param {string} entryPath 压缩包内的条目路径(不可信)
175
+ * @returns {string} 校验通过的绝对路径
176
+ * @throws 条目路径为绝对路径 / 盘符 / UNC,或解析后越出 baseDir 时抛出
177
+ */
178
+ function safeResolveZipEntryPath(baseDir, entryPath) {
179
+ if (typeof entryPath !== 'string' || entryPath.length === 0) {
180
+ throw new Error('zip 条目路径无效');
181
+ }
182
+ // 绝对路径、Windows 盘符、UNC 路径一律拒绝
183
+ if (path_1.default.isAbsolute(entryPath) ||
184
+ /^[a-zA-Z]:[\\/]/.test(entryPath) ||
185
+ entryPath.startsWith('\\\\') ||
186
+ entryPath.startsWith('//')) {
187
+ throw new Error(`检测到不安全的 zip 条目路径(绝对路径): ${entryPath}`);
188
+ }
189
+ // 统一分隔符并去掉多余的前导分隔符
190
+ const normalizedEntryPath = entryPath.replace(/\\/g, '/').replace(/^\/+/, '');
191
+ const resolvedBase = path_1.default.resolve(baseDir);
192
+ const resolvedTarget = path_1.default.resolve(resolvedBase, normalizedEntryPath);
193
+ // 用 path.relative 判断是否仍在 baseDir 内:
194
+ // 越界时一定是 '..' 本身或以 '..' + 路径分隔符开头(不能仅用 '..' 前缀判断,
195
+ // 否则会误伤 '..gitignore'、'..env/prod.txt' 等以 '..' 开头的合法条目名);
196
+ // Windows 跨盘符时 path.relative 会直接返回绝对路径,故 path.isAbsolute 这一判不能省。
197
+ const relative = path_1.default.relative(resolvedBase, resolvedTarget);
198
+ if (relative === '') {
199
+ return resolvedTarget;
200
+ }
201
+ if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
202
+ throw new Error(`检测到不安全的 zip 条目路径(路径穿越): ${entryPath}`);
203
+ }
204
+ return resolvedTarget;
205
+ }
166
206
  /**
167
207
  * 下载并解压压缩包到指定目录
168
208
  * @param {string} downloadUrl 压缩包下载地址
169
209
  * @param {string} targetPath 目标路径
170
210
  * @returns {Promise<void>}
171
- * @throws 当下载失败或解压失败时抛出错误
211
+ * @throws 当下载失败或解压失败时抛出错误;当条目路径越界时抛出错误
172
212
  */
173
213
  async function downloadAndExtractRemoteZip(downloadUrl, targetPath) {
174
214
  const downloadResponse = await (0, http_request_1.fetchStream)(downloadUrl);
@@ -186,22 +226,71 @@ async function downloadAndExtractRemoteZip(downloadUrl, targetPath) {
186
226
  let totalEntries = 0; // 总条目数(文件+目录)
187
227
  let processedEntries = 0; // 已处理条目数
188
228
  let streamEnded = false; // 流是否结束
189
- downloadResponse
229
+ let aborted = false; // 是否已判定失败(置位后剩余条目只丢弃、不再落盘)
230
+ const pendingWrites = new Set(); // 在途写入
231
+ let parseStream; // 解析流,失败时用于中止
232
+ // 统一失败出口:只认第一次失败,并立即中止下载 / 解析 / 在途写入。
233
+ // 只置标志位不够——已在处理的条目仍会继续写盘,导致 Promise 已 reject
234
+ // 之后目标目录内容还在变化。
235
+ const fail = (err) => {
236
+ var _a;
237
+ if (aborted)
238
+ return;
239
+ aborted = true;
240
+ try {
241
+ // body 的类型声明里没有 destroy(实际是 node-fetch 的 Readable),这里按需断言
242
+ const rawBody = downloadResponse.body;
243
+ (_a = rawBody === null || rawBody === void 0 ? void 0 : rawBody.destroy) === null || _a === void 0 ? void 0 : _a.call(rawBody); // 下载流存在时中止
244
+ parseStream === null || parseStream === void 0 ? void 0 : parseStream.destroy(); // 解析流存在时中止
245
+ pendingWrites.forEach(ws => ws.destroy()); // 在途写入流存在时中止
246
+ pendingWrites.clear();
247
+ }
248
+ catch (_b) {
249
+ // 中止动作本身的异常不应覆盖原始错误
250
+ }
251
+ reject(err instanceof Error ? err : new Error(String(err)));
252
+ };
253
+ // HTTP 响应体 → unzipper.Parse() 这条管道的下游端
254
+ parseStream = downloadResponse
190
255
  .body.pipe(unzipper_1.default.Parse())
191
- .on('error', reject)
256
+ .on('error', fail)
192
257
  .on('entry', async (entry) => {
193
258
  totalEntries++;
194
- // 检查是否应该跳过这个条目
195
- if (shouldSkipEntry(entry.path)) {
259
+ // 检查是否应该跳过这个条目(已判定失败时同样丢弃,避免继续写盘)
260
+ if (shouldSkipEntry(entry.path) || aborted) {
196
261
  entry.autodrain(); // 丢弃数据流
197
262
  processedEntries++;
198
263
  checkCompletion();
199
264
  return;
200
265
  }
201
- const filePath = path_1.default.join(dirPath, entry.path);
266
+ // 条目名来自压缩包(不可信),校验解析结果必须仍在目标目录内
267
+ let filePath;
268
+ try {
269
+ filePath = safeResolveZipEntryPath(dirPath, entry.path);
270
+ }
271
+ catch (err) {
272
+ entry.autodrain();
273
+ processedEntries++;
274
+ fail(err);
275
+ return;
276
+ }
277
+ // 仅放行文件/目录两类条目,其余类型(符号链接等)一律丢弃
278
+ if (entry.type !== 'File' && entry.type !== 'Directory') {
279
+ entry.autodrain();
280
+ processedEntries++;
281
+ checkCompletion();
282
+ return;
283
+ }
202
284
  try {
203
285
  // 确保父目录存在(处理嵌套目录情况)
204
286
  await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
287
+ // await 期间可能已失败(aborted),此时不能再开写入流,否则 reject 之后还会写盘
288
+ if (aborted) {
289
+ entry.autodrain();
290
+ processedEntries++;
291
+ checkCompletion();
292
+ return;
293
+ }
205
294
  // 如果是目录则创建,否则写入文件
206
295
  if (entry.type === 'Directory') {
207
296
  await fs_extra_1.default.ensureDir(filePath);
@@ -209,17 +298,21 @@ async function downloadAndExtractRemoteZip(downloadUrl, targetPath) {
209
298
  checkCompletion();
210
299
  }
211
300
  else {
301
+ const writeStream = fs_extra_1.default.createWriteStream(filePath);
302
+ pendingWrites.add(writeStream);
212
303
  entry
213
- .pipe(fs_extra_1.default.createWriteStream(filePath))
214
- .on('error', reject)
304
+ .pipe(writeStream)
305
+ .on('error', fail)
306
+ .on('close', () => pendingWrites.delete(writeStream))
215
307
  .on('finish', () => {
308
+ pendingWrites.delete(writeStream);
216
309
  processedEntries++;
217
310
  checkCompletion();
218
311
  });
219
312
  }
220
313
  }
221
314
  catch (err) {
222
- reject(err);
315
+ fail(err);
223
316
  }
224
317
  })
225
318
  .on('close', () => {
@@ -227,6 +320,9 @@ async function downloadAndExtractRemoteZip(downloadUrl, targetPath) {
227
320
  checkCompletion();
228
321
  });
229
322
  function checkCompletion() {
323
+ // 已失败时 Promise 早已 settle,无需再尝试 resolve
324
+ if (aborted)
325
+ return;
230
326
  if (streamEnded && totalEntries === processedEntries) {
231
327
  resolve('');
232
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/manager-node",
3
- "version": "5.9.1",
3
+ "version": "5.9.3",
4
4
  "description": "The node manage service api for cloudbase.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -281,6 +281,7 @@ export interface HTTPServiceRoute {
281
281
  PathRewrite?: HTTPServicePathRewrite;
282
282
  UpstreamResourceType: 'SCF' | 'CBR' | 'STATIC_STORE' | 'WEB_SCF' | 'LH' | 'STORAGE';
283
283
  UpstreamResourceName: string;
284
+ UpstreamResourceVersion?: string;
284
285
  EnableSafeDomain: boolean;
285
286
  EnableAuth: boolean;
286
287
  EnablePathTransmission: boolean;
@@ -342,6 +343,7 @@ export interface HTTPServiceRouteParam {
342
343
  Path: string;
343
344
  UpstreamResourceType?: 'SCF' | 'CBR' | 'STATIC_STORE' | 'WEB_SCF' | 'LH' | 'STORAGE';
344
345
  UpstreamResourceName?: string;
346
+ UpstreamResourceVersion?: string;
345
347
  PathRewrite?: HTTPServicePathRewrite;
346
348
  EnableSafeDomain?: boolean;
347
349
  EnableAuth?: boolean;
@@ -32,12 +32,24 @@ export declare function compressToZip(option: IZipOption): Promise<unknown>;
32
32
  * @returns {boolean} 如果应该跳过则返回 true
33
33
  */
34
34
  export declare function shouldSkipEntry(entryPath: string): boolean;
35
+ /**
36
+ * 校验 zip 条目路径,返回解析后的绝对路径
37
+ *
38
+ * 条目名来自压缩包本身(不可信),必须确保解析结果仍落在 baseDir 内,
39
+ * 否则 `../` 之类的条目会跳出目标目录,形成任意文件写入(Zip Slip / CWE-22)。
40
+ *
41
+ * @param {string} baseDir 目标根目录(受信任)
42
+ * @param {string} entryPath 压缩包内的条目路径(不可信)
43
+ * @returns {string} 校验通过的绝对路径
44
+ * @throws 条目路径为绝对路径 / 盘符 / UNC,或解析后越出 baseDir 时抛出
45
+ */
46
+ export declare function safeResolveZipEntryPath(baseDir: string, entryPath: string): string;
35
47
  /**
36
48
  * 下载并解压压缩包到指定目录
37
49
  * @param {string} downloadUrl 压缩包下载地址
38
50
  * @param {string} targetPath 目标路径
39
51
  * @returns {Promise<void>}
40
- * @throws 当下载失败或解压失败时抛出错误
52
+ * @throws 当下载失败或解压失败时抛出错误;当条目路径越界时抛出错误
41
53
  */
42
54
  export declare function downloadAndExtractRemoteZip(downloadUrl: string, targetPath: string): Promise<void>;
43
55
  /**