@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 +10 -0
- package/changelog_content_en.md +3 -15
- package/lib/function/index.js +69 -22
- package/lib/utils/index.js +105 -9
- package/package.json +1 -1
- package/types/env/type.d.ts +2 -0
- package/types/utils/index.d.ts +13 -1
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)
|
package/changelog_content_en.md
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
## [5.9.
|
|
1
|
+
## [5.9.2] 2026-10-21
|
|
2
2
|
|
|
3
|
-
- [
|
|
4
|
-
- [
|
|
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 增加路径穿越防护并支持失败即中止
|
package/lib/function/index.js
CHANGED
|
@@ -1806,28 +1806,75 @@ class FunctionService {
|
|
|
1806
1806
|
}
|
|
1807
1807
|
});
|
|
1808
1808
|
return new Promise((resolve, reject) => {
|
|
1809
|
-
//
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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
|
/**
|
package/lib/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
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
|
-
|
|
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(
|
|
214
|
-
.on('error',
|
|
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
|
-
|
|
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
package/types/env/type.d.ts
CHANGED
|
@@ -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;
|
package/types/utils/index.d.ts
CHANGED
|
@@ -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
|
/**
|