@cnbcool/cnb-api-generate 2.9.0 → 2.9.2

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.
Files changed (2) hide show
  1. package/client/core.ts +43 -35
  2. package/package.json +1 -1
package/client/core.ts CHANGED
@@ -6,6 +6,25 @@ import { generateUniqueId } from './utils/generate-unique-id';
6
6
  import { resolveToken } from './utils/resolve-token';
7
7
  import { resolveApiDomain } from './utils/resolve-api-domain';
8
8
 
9
+ /**
10
+ * 将 buffer 写入临时文件并返回文件路径
11
+ */
12
+ async function writeBufferToTempFile(buffer: Buffer, extension: string, customFileName?: string): Promise<string> {
13
+ const tempDir = os.tmpdir();
14
+ const uploadDir = path.join(tempDir, 'cnb-api');
15
+ // recursive: true 在目录已存在时不会报错,无需先 existsSync 检查(消除 TOCTOU)
16
+ await fs.promises.mkdir(uploadDir, { recursive: true });
17
+ const fileName = customFileName || `${generateUniqueId()}${extension}`;
18
+ const filePath = path.join(uploadDir, fileName);
19
+ // 安全检查:使用 path.relative 确保最终路径不跳出 uploadDir
20
+ const relative = path.relative(path.resolve(uploadDir), path.resolve(filePath));
21
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
22
+ throw new Error(`Rejected path traversal: ${fileName}`);
23
+ }
24
+ await fs.promises.writeFile(filePath, buffer);
25
+ return filePath;
26
+ }
27
+
9
28
  /**
10
29
  * 格式化 API 响应
11
30
  * 构建包含以下字段的响应对象:
@@ -13,7 +32,7 @@ import { resolveApiDomain } from './utils/resolve-api-domain';
13
32
  * - trace: traceparent(用于调试追踪)
14
33
  * - header: 原始分页 header(x-cnb-page 等)
15
34
  * - page/pageSize/total/totalPages: 从 header 计算的分页信息(仅列表 API)
16
- * - data: 响应体(JSON/文本/图片路径/base64)
35
+ * - data: 响应体(JSON/文本/二进制文件路径)
17
36
  *
18
37
  * 注意:所有字段始终保留,由 index.ts 的输出层决定展示哪些
19
38
  */
@@ -61,52 +80,41 @@ async function formatResponse(data: any, response: any) {
61
80
  'application/json',
62
81
  'text/json',
63
82
  ].some((t) => contentType.includes(t));
64
- const isText = contentType.startsWith('text/') || contentType.includes('xml');
65
- const isImage = contentType.startsWith('image/');
83
+ const isText = contentType.startsWith('text/') || contentType.includes('/xml') || contentType.includes('+xml');
66
84
  try {
67
85
  if (isJson) {
68
86
  responseData.data = await response.json();
69
87
  } else if (isText) {
70
88
  responseData.data = await response.text();
71
- } else if (isImage) {
89
+ } else {
90
+ // 非文本/JSON 响应视为二进制数据,保存到文件
72
91
  const arrayBuffer = await response.arrayBuffer();
73
92
  const buffer = Buffer.from(arrayBuffer);
74
93
 
75
- // 确定文件扩展名
76
- let extension = '.bin';
77
- if (contentType.includes('png')) extension = '.png';
78
- else if (contentType.includes('jpeg') || contentType.includes('jpg')) extension = '.jpg';
79
- else if (contentType.includes('gif')) extension = '.gif';
80
- else if (contentType.includes('webp')) extension = '.webp';
81
- else if (contentType.includes('svg')) extension = '.svg';
82
- // 可以根据需要添加更多类型
83
-
84
- // 构建本地保存路径
85
- // 注意:请确保 './downloads' 目录存在,或者使用 fs.mkdirSync 创建它
86
- const tempDir = os.tmpdir();
87
- const uploadDir = path.join(tempDir, 'cnb-api');
88
- if (!fs.existsSync(uploadDir)) {
89
- fs.mkdirSync(uploadDir, { recursive: true });
94
+ // 从下载 URL 中解析文件名(含扩展名)
95
+ let fileName: string | undefined;
96
+ try {
97
+ const urlObj = new URL(data.url, 'https://dummy.example');
98
+ const lastSegment = path.basename(urlObj.pathname);
99
+ if (lastSegment && lastSegment.includes('.')) {
100
+ let decoded = decodeURIComponent(lastSegment);
101
+ // 拒绝空字节(不同 OS/Node.js 版本行为不一致)
102
+ if (decoded.includes('\0')) throw new Error('Invalid filename: contains null byte');
103
+ // 只保留安全字符:字母、数字、下划线、连字符、点号
104
+ fileName = decoded.replace(/[^a-zA-Z0-9._-]/g, '_');
105
+ }
106
+ } catch (err) {
107
+ console.warn('[cnb-api] Failed to parse filename from URL:', err instanceof Error ? err.message : err);
90
108
  }
91
109
 
92
- const fileName = `${generateUniqueId()}${extension}`;
93
- const filePath = path.join(uploadDir, fileName);
94
-
95
- fs.writeFileSync(filePath, buffer);
96
- responseData.data = filePath;
97
- } else {
98
- // 其他二进制数据保持原有的 Base64 逻辑,或者也可以按需保存
99
- const arrayBuffer = await response.arrayBuffer();
100
- const buffer = Buffer.from(arrayBuffer);
101
- const base64String = buffer.toString('base64');
102
-
103
- responseData.data = {
104
- type: 'base64',
105
- data: base64String,
106
- mimeType: contentType || 'application/octet-stream',
107
- };
110
+ if (fileName) {
111
+ responseData.data = await writeBufferToTempFile(buffer, '', fileName);
112
+ } else {
113
+ responseData.data = await writeBufferToTempFile(buffer, '.bin');
114
+ }
108
115
  }
109
116
  } catch (err: any) {
117
+ console.error('[cnb-api] formatResponse error:', err);
110
118
  responseData.data = err?.message || 'Unknown Error';
111
119
  }
112
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",