magic-builder 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -126,6 +126,31 @@ magic-builder link create --title "Weekly Report"
126
126
  magic-builder link create --fid <faas-id>
127
127
  ```
128
128
 
129
+ Submit Markdown to a People performance draft (`perf` is an alias):
130
+
131
+ ```bash
132
+ magic-builder performance --cookie ./cookie.txt --markdown ./performance.md
133
+ magic-builder perf --cookie "$PEOPLE_COOKIE" --markdown "# Performance summary"
134
+ magic-builder perf --cookie ./cookie.txt --markdown https://example.com/performance.md --dry-run
135
+ ```
136
+
137
+ Cookie input accepts a raw `Cookie` header, a local path, or a Netscape cookie file. Markdown input accepts literal content, a local path, an HTTP(S) URL, or stdin. The command extracts `x-f-csrf` from the Cookie and does not print the Cookie in its output.
138
+
139
+ When `--cookie` and `--cookie-file` are omitted, `performance` automatically reads `./cookie.txt`, matching the default output of `extract-cookie`:
140
+
141
+ ```bash
142
+ magic-builder extract-cookie --curl-file ./request.curl
143
+ magic-builder performance --markdown ./performance.md
144
+ ```
145
+
146
+ Extract Cookie from a copied curl command. This is a top-level general-purpose command. The default output is `./cookie.txt`; the file is created with mode `0600`:
147
+
148
+ ```bash
149
+ magic-builder extract-cookie --curl-file ./request.curl
150
+ magic-builder extract-cookie --curl "curl 'https://example.com/...' -b 'foo=bar; token=value'"
151
+ cat ./request.curl | magic-builder extract-cookie --out-dir ./secrets
152
+ ```
153
+
129
154
  Create or append a Feishu document HTML Box:
130
155
 
131
156
  ```bash
@@ -12,6 +12,9 @@ const COMMANDS = {
12
12
  link: require('../src/commands/link'),
13
13
  doc: require('../src/commands/doc'),
14
14
  feedback: require('../src/commands/feedback'),
15
+ performance: require('../src/commands/performance'),
16
+ perf: require('../src/commands/performance'),
17
+ 'extract-cookie': require('../src/commands/extract-cookie'),
15
18
  skill: require('../src/commands/skill'),
16
19
  version: require('../src/commands/version'),
17
20
  update: require('../src/commands/update'),
package/package.json CHANGED
@@ -1,19 +1,30 @@
1
1
  {
2
2
  "name": "magic-builder",
3
- "version": "1.0.0",
4
- "description": "CLI for Magic Builder — publish pages, functions, and files",
3
+ "version": "1.1.0",
4
+ "description": "CLI for Magic Builder — publish pages, functions, files, and performance drafts",
5
5
  "bin": {
6
- "magic-builder": "./bin/magic-builder.js",
7
- "magic-cli": "./bin/magic-builder.js",
8
- "miaobi": "./bin/magic-builder.js"
6
+ "magic-builder": "bin/magic-builder.js",
7
+ "magic-cli": "bin/magic-builder.js",
8
+ "miaobi": "bin/magic-builder.js"
9
9
  },
10
10
  "main": "./src/index.js",
11
11
  "engines": {
12
12
  "node": ">=18.0.0"
13
13
  },
14
14
  "type": "commonjs",
15
- "files": ["bin/", "src/", "README.md", "LICENSE"],
16
- "keywords": ["magic-builder", "miaobi", "cli", "faas", "publish"],
15
+ "files": [
16
+ "bin/",
17
+ "src/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "magic-builder",
23
+ "miaobi",
24
+ "cli",
25
+ "faas",
26
+ "publish"
27
+ ],
17
28
  "license": "MIT",
18
29
  "publishConfig": {
19
30
  "registry": "https://registry.npmjs.org/"
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
4
+ const { basename, dirname, resolve } = require('path');
5
+ const { success, fail } = require('../lib/output');
6
+
7
+ function run(args, opts) {
8
+ if (args.length) fail('Usage: magic-builder extract-cookie --curl <content|file> [--out <file>|--out-dir <dir>]', 'E_INVALID_ARGS');
9
+ const curl = readCurl(opts.curl, opts.curlFile);
10
+ if (!curl) fail('Missing curl content. Use --curl <content|file>, --curl-file <file>, or stdin.', 'E_INVALID_ARGS');
11
+ const cookie = extractCookieFromCurl(curl);
12
+ if (!cookie) fail('No Cookie found in curl. Expected -b/--cookie or a Cookie request header.', 'E_INVALID_ARGS');
13
+
14
+ const output = resolveOutput(opts.out, opts.outDir);
15
+ mkdirSync(dirname(output), { recursive: true });
16
+ writeFileSync(output, `${cookie}\n`, { encoding: 'utf8', mode: 0o600 });
17
+ success({ path: output, filename: basename(output), cookieCount: cookie.split(';').filter(Boolean).length }, opts);
18
+ }
19
+
20
+ function readCurl(value, file) {
21
+ const input = String(file || value || '').trim();
22
+ if (!input && !process.stdin.isTTY) return readFileSync(0, 'utf8');
23
+ if (!input) return '';
24
+ const path = resolve(input);
25
+ return file || existsSync(path) ? readFileSync(path, 'utf8') : input;
26
+ }
27
+
28
+ function extractCookieFromCurl(curl) {
29
+ const tokens = tokenizeShell(String(curl || '').replace(/\\\r?\n/g, ' '));
30
+ for (let i = 0; i < tokens.length; i++) {
31
+ const token = tokens[i];
32
+ if (token === '-b' || token === '--cookie') return normalizeCookie(tokens[i + 1] || '');
33
+ if (token.startsWith('--cookie=')) return normalizeCookie(token.slice('--cookie='.length));
34
+ if (token === '-H' || token === '--header') {
35
+ const header = tokens[i + 1] || '';
36
+ if (/^cookie\s*:/i.test(header)) return normalizeCookie(header);
37
+ }
38
+ if (token.startsWith('--header=')) {
39
+ const header = token.slice('--header='.length);
40
+ if (/^cookie\s*:/i.test(header)) return normalizeCookie(header);
41
+ }
42
+ }
43
+ return '';
44
+ }
45
+
46
+ function normalizeCookie(raw) {
47
+ return String(raw || '').replace(/^cookie\s*:\s*/i, '').trim();
48
+ }
49
+
50
+ function tokenizeShell(input) {
51
+ const tokens = [];
52
+ let token = '';
53
+ let quote = '';
54
+ let escaped = false;
55
+ for (const char of input) {
56
+ if (escaped) {
57
+ token += char;
58
+ escaped = false;
59
+ } else if (char === '\\' && quote !== "'") {
60
+ escaped = true;
61
+ } else if (quote) {
62
+ if (char === quote) quote = '';
63
+ else token += char;
64
+ } else if (char === "'" || char === '"') {
65
+ quote = char;
66
+ } else if (/\s/.test(char)) {
67
+ if (token) {
68
+ tokens.push(token);
69
+ token = '';
70
+ }
71
+ } else {
72
+ token += char;
73
+ }
74
+ }
75
+ if (escaped) token += '\\';
76
+ if (token) tokens.push(token);
77
+ return tokens;
78
+ }
79
+
80
+ function resolveOutput(out, outDir) {
81
+ if (out) return resolve(String(out));
82
+ return resolve(String(outDir || process.cwd()), 'cookie.txt');
83
+ }
84
+
85
+ module.exports = { run, extractCookieFromCurl, resolveOutput };
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+
3
+ const { existsSync, readFileSync } = require('fs');
4
+ const { resolve } = require('path');
5
+ const { success, fail } = require('../lib/output');
6
+
7
+ const DEFAULTS = {
8
+ endpoint: 'https://people.bytedance.net/perf/api/foundation/v2/draft',
9
+ reviewId: '7654473253011852917',
10
+ formId: '7654466592117358592',
11
+ templateId: '7654466592117358609',
12
+ unitId: '7654466592117358775',
13
+ fieldId: '7654466592117358799',
14
+ textFieldId: '7654466592117358824',
15
+ operatorId: '6687792427639817740',
16
+ sourceId: '7784085065561419062',
17
+ fieldSourceId: '7662587009851985528',
18
+ rootReviewId: '7657808756893306935',
19
+ version: 5,
20
+ };
21
+
22
+ async function run(args, opts) {
23
+ if (args.length) fail('Usage: magic-builder performance --cookie <content|file> --markdown <content|file|url>', 'E_INVALID_ARGS');
24
+
25
+ const cookie = readCookie(opts.cookie, opts.cookieFile);
26
+ if (!cookie) fail('Missing Cookie. Use --cookie <content|file> or --cookie-file <file>.', 'E_INVALID_ARGS');
27
+
28
+ const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
29
+ if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
30
+
31
+ const csrf = getCookie(cookie, 'x-f-csrf');
32
+ if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
33
+
34
+ const config = resolveConfig(opts);
35
+ const payload = buildPayload(markdown, config);
36
+ if (opts.dryRun) {
37
+ success({ dryRun: true, endpoint: config.endpoint, payload }, opts);
38
+ return;
39
+ }
40
+
41
+ const response = await fetch(config.endpoint, {
42
+ method: 'POST',
43
+ headers: {
44
+ accept: 'application/json, text/plain, */*',
45
+ 'accept-language': 'zh-CN,zh;q=0.9',
46
+ 'content-type': 'application/json;charset=UTF-8',
47
+ cookie,
48
+ origin: 'https://people.bytedance.net',
49
+ referer: config.referer,
50
+ 'rpc-persist-lane-c-perfx-tenant-id': config.tenantId,
51
+ 'x-f-csrf': csrf,
52
+ 'x-f-lang': 'zh-CN',
53
+ 'x-f-timezone': 'Asia/Shanghai',
54
+ },
55
+ body: JSON.stringify(payload),
56
+ });
57
+
58
+ const text = await response.text();
59
+ const data = parseJson(text);
60
+ if (!response.ok) {
61
+ const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
62
+ error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
63
+ throw error;
64
+ }
65
+ success(data === null ? text : data, opts);
66
+ }
67
+
68
+ function readCookie(value, file) {
69
+ const defaultFile = resolve(process.cwd(), 'cookie.txt');
70
+ const input = String(file || value || (existsSync(defaultFile) ? defaultFile : '')).trim();
71
+ if (!input) return '';
72
+ const path = resolve(input);
73
+ const raw = file || existsSync(path) ? readFileSync(path, 'utf8') : input;
74
+ return normalizeCookie(raw);
75
+ }
76
+
77
+ async function readMarkdown(value, file) {
78
+ const input = String(file || value || '').trim();
79
+ if (!input && !process.stdin.isTTY) return readFileSync(0, 'utf8');
80
+ if (!input) return '';
81
+ if (!file && /^https?:\/\//i.test(input)) {
82
+ const response = await fetch(input);
83
+ if (!response.ok) throw new Error(`Unable to read Markdown URL: HTTP ${response.status}`);
84
+ return response.text();
85
+ }
86
+ const path = resolve(input);
87
+ return file || existsSync(path) ? readFileSync(path, 'utf8') : input;
88
+ }
89
+
90
+ function normalizeCookie(raw) {
91
+ const text = String(raw || '').trim();
92
+ const cookieHeader = text.match(/^cookie:\s*(.+)$/im);
93
+ if (cookieHeader) return cookieHeader[1].trim();
94
+ if (!text.includes('\n')) return text;
95
+
96
+ const netscape = text.split(/\r?\n/)
97
+ .filter(line => line && !line.startsWith('#'))
98
+ .map(line => line.split('\t'))
99
+ .filter(parts => parts.length >= 7)
100
+ .map(parts => `${parts[5]}=${parts[6]}`);
101
+ if (netscape.length) return netscape.join('; ');
102
+
103
+ return text.split(/\r?\n/).map(line => line.trim()).filter(Boolean).join(' ');
104
+ }
105
+
106
+ function getCookie(cookie, name) {
107
+ for (const part of cookie.split(';')) {
108
+ const index = part.indexOf('=');
109
+ if (index < 0) continue;
110
+ if (part.slice(0, index).trim().toLowerCase() === name.toLowerCase()) return part.slice(index + 1).trim();
111
+ }
112
+ return '';
113
+ }
114
+
115
+ function resolveConfig(opts) {
116
+ const reviewId = String(opts.reviewId || DEFAULTS.reviewId);
117
+ const formId = String(opts.formId || DEFAULTS.formId);
118
+ const templateId = String(opts.templateId || DEFAULTS.templateId);
119
+ const tenantId = String(opts.tenantId || getCookie(String(opts.cookie || ''), 'tenant_id') || '6685321562717324807');
120
+ return {
121
+ endpoint: String(opts.endpoint || DEFAULTS.endpoint),
122
+ reviewId,
123
+ formId,
124
+ templateId,
125
+ unitId: String(opts.unitId || DEFAULTS.unitId),
126
+ fieldId: String(opts.fieldId || DEFAULTS.fieldId),
127
+ textFieldId: String(opts.textFieldId || DEFAULTS.textFieldId),
128
+ operatorId: String(opts.operatorId || DEFAULTS.operatorId),
129
+ sourceId: String(opts.sourceId || DEFAULTS.sourceId),
130
+ fieldSourceId: String(opts.fieldSourceId || DEFAULTS.fieldSourceId),
131
+ rootReviewId: String(opts.rootReviewId || DEFAULTS.rootReviewId),
132
+ version: Number(opts.version || DEFAULTS.version),
133
+ tenantId,
134
+ referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${reviewId}/${formId}?mode=editable`),
135
+ uid: String(opts.uid || `magic-builder-${Date.now()}`),
136
+ };
137
+ }
138
+
139
+ function buildPayload(markdown, config) {
140
+ const now = Date.now();
141
+ const sections = splitMarkdownSections(markdown);
142
+ const subUnits = sections.map((section, index) => {
143
+ const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
144
+ if (index > 0) {
145
+ return [{
146
+ key: `${index - 1}-${now + index}-${Math.random()}`,
147
+ id: config.textFieldId,
148
+ entityType: 'text',
149
+ value,
150
+ }];
151
+ }
152
+ return [{
153
+ id: config.textFieldId,
154
+ source_id: config.sourceId,
155
+ value,
156
+ json_value: {},
157
+ created_time: now,
158
+ updated_time: now,
159
+ entityType: 'text',
160
+ }];
161
+ });
162
+ return {
163
+ key: `${config.reviewId}__confirm_invitation__${config.operatorId}__${config.formId}`,
164
+ data: {
165
+ keyWorks: {
166
+ valueSetting: [{ operatorId: config.operatorId, source_id: config.sourceId, isReviewee: true }],
167
+ currentUserId: config.operatorId,
168
+ units: [{
169
+ id: config.unitId,
170
+ fields: [{
171
+ id: config.fieldId,
172
+ entityType: 'multiple_texts',
173
+ source_id: config.fieldSourceId,
174
+ value: '',
175
+ created_time: now,
176
+ updated_time: now,
177
+ extra: null,
178
+ json_value: {},
179
+ sub_units: subUnits,
180
+ }],
181
+ type: 'object',
182
+ unknown: false,
183
+ }],
184
+ templateType: 'write',
185
+ template_id: config.templateId,
186
+ },
187
+ rootReviewId: config.rootReviewId,
188
+ uid: config.uid,
189
+ },
190
+ version: config.version,
191
+ };
192
+ }
193
+
194
+ function splitMarkdownSections(markdown) {
195
+ const text = String(markdown || '').replace(/\r\n?/g, '\n').trim();
196
+ if (!text) return [''];
197
+ const lines = text.split('\n');
198
+ const sections = [];
199
+ let current = [];
200
+ for (const line of lines) {
201
+ if (/^##\s+/.test(line)) {
202
+ if (current.some(item => item.trim()) && !current.every(item => /^#\s+/.test(item) || !item.trim())) {
203
+ sections.push(current.join('\n').trim());
204
+ }
205
+ current = [`# ${line.replace(/^##\s+/, '')}`];
206
+ } else if (current.length || !/^#\s+/.test(line)) {
207
+ current.push(line);
208
+ }
209
+ }
210
+ if (current.some(item => item.trim())) sections.push(current.join('\n').trim());
211
+ return sections.length ? sections : [text];
212
+ }
213
+
214
+ function markdownToDelta(markdown) {
215
+ const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
216
+ const ops = [];
217
+ let inFence = false;
218
+ for (const line of lines) {
219
+ if (/^\s*```/.test(line)) {
220
+ inFence = !inFence;
221
+ continue;
222
+ }
223
+ if (inFence) {
224
+ appendInline(ops, line);
225
+ ops.push({ insert: '\n', attributes: { 'code-block': true } });
226
+ continue;
227
+ }
228
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
229
+ const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
230
+ const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
231
+ const quote = line.match(/^\s*>\s?(.*)$/);
232
+ const content = heading?.[2] ?? unordered?.[1] ?? ordered?.[1] ?? quote?.[1] ?? line;
233
+ appendInline(ops, content);
234
+ const attributes = heading ? { header: heading[1].length }
235
+ : unordered ? { list: 'bullet' }
236
+ : ordered ? { list: 'ordered' }
237
+ : quote ? { blockquote: true }
238
+ : undefined;
239
+ ops.push(attributes ? { insert: '\n', attributes } : { insert: '\n' });
240
+ }
241
+ if (ops.length > 1 && lines.at(-1) === '') ops.pop();
242
+ return ops.length ? ops : [{ insert: '\n' }];
243
+ }
244
+
245
+ function appendInline(ops, text) {
246
+ const pattern = /(\*\*|__)(.+?)\1|(?<!\*)\*([^*\n]+)\*|(?<!_)_([^_\n]+)_|`([^`\n]+)`|\[([^\]]+)]\((https?:\/\/[^\s)]+)\)/g;
247
+ let cursor = 0;
248
+ let match;
249
+ while ((match = pattern.exec(text))) {
250
+ if (match.index > cursor) ops.push({ insert: text.slice(cursor, match.index) });
251
+ if (match[2]) ops.push({ insert: match[2], attributes: { bold: true } });
252
+ else if (match[3] || match[4]) ops.push({ insert: match[3] || match[4], attributes: { italic: true } });
253
+ else if (match[5]) ops.push({ insert: match[5], attributes: { code: true } });
254
+ else ops.push({ insert: match[6], attributes: { link: match[7] } });
255
+ cursor = pattern.lastIndex;
256
+ }
257
+ if (cursor < text.length) ops.push({ insert: text.slice(cursor) });
258
+ }
259
+
260
+ function parseJson(text) {
261
+ try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
262
+ }
263
+
264
+ module.exports = { run, readCookie, normalizeCookie, getCookie, splitMarkdownSections, markdownToDelta, buildPayload };
package/src/index.js CHANGED
@@ -13,6 +13,9 @@ module.exports = {
13
13
  link: require('./commands/link'),
14
14
  doc: require('./commands/doc'),
15
15
  feedback: require('./commands/feedback'),
16
+ performance: require('./commands/performance'),
17
+ perf: require('./commands/performance'),
18
+ extractCookie: require('./commands/extract-cookie'),
16
19
  skill: require('./commands/skill'),
17
20
  version: require('./commands/version'),
18
21
  update: require('./commands/update'),
package/src/lib/help.js CHANGED
@@ -21,6 +21,8 @@ COMMANDS:
21
21
  link Generate Magic share links
22
22
  doc Create or append Feishu Doc HTML Box apps
23
23
  feedback Create Magic feedback records
24
+ performance Submit Markdown content to a People performance draft (alias: perf)
25
+ extract-cookie Extract Cookie data from a curl command
24
26
  skill Check or update the Magic Builder skill package
25
27
  version Show CLI version
26
28
  update Update this CLI package
@@ -45,6 +47,8 @@ EXAMPLES:
45
47
  magic-builder link create --title "Weekly Report"
46
48
  magic-builder doc create --html app.html --title "Demo"
47
49
  magic-builder feedback create --feedback "打不开页面"
50
+ magic-builder performance --cookie ./cookie.txt --markdown ./performance.md
51
+ magic-builder extract-cookie --curl-file ./request.curl
48
52
  magic-builder skill install
49
53
  magic-builder skill check-update
50
54
  magic-builder version
@@ -103,6 +107,11 @@ COMMANDS:
103
107
  feedback create --feedback <text> [--title <title>] [--summary <text>]
104
108
  feedback create --feedback-file <file> [--dry-run]
105
109
 
110
+ performance --cookie <content|file> --markdown <content|file|url> [--dry-run]
111
+ perf --cookie-file <file> --markdown-file <file> [--dry-run]
112
+
113
+ extract-cookie --curl <content|file> [--out <file>|--out-dir <dir>]
114
+
106
115
  skill install [--environment auto|local|cloud] [--skills-root <dir>]
107
116
  skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
108
117
  skill update [--environment auto|local|cloud] [--skills-root <dir>]
@@ -184,6 +193,44 @@ SYNTAX:
184
193
  SYNTAX:
185
194
  magic-builder feedback create --feedback <text>
186
195
  magic-builder feedback create --feedback-file <file>
196
+ `,
197
+ performance: `@HELP magic-builder/performance
198
+
199
+ SYNTAX:
200
+ magic-builder performance --cookie <content|file> --markdown <content|file|url>
201
+ magic-builder perf --cookie-file <file> --markdown-file <file>
202
+
203
+ COOKIE:
204
+ When --cookie and --cookie-file are omitted, ./cookie.txt is used.
205
+
206
+ OPTIONS:
207
+ --dry-run Print the generated payload without sending it
208
+ --review-id <id> Override the performance review id
209
+ --form-id <id> Override the invitation/form id used by the draft key
210
+ --template-id <id> Override the review template id
211
+ --root-review-id <id> Override the root review id
212
+ --operator-id <id> Override the current user/operator id
213
+ --source-id <id> Override the form source id
214
+ --field-source-id <id> Override the writable field source id
215
+ --tenant-id <id> Override the People tenant id
216
+ --endpoint <url> Override the draft API endpoint
217
+ `,
218
+ perf: `@HELP magic-builder/performance
219
+
220
+ Alias of "magic-builder performance".
221
+ `,
222
+ 'extract-cookie': `@HELP magic-builder/extract-cookie
223
+
224
+ SYNTAX:
225
+ magic-builder extract-cookie --curl <content|file> [--out <file>]
226
+ magic-builder extract-cookie --curl-file <file> [--out-dir <dir>]
227
+ cat request.curl | magic-builder extract-cookie [--out <file>]
228
+
229
+ OPTIONS:
230
+ --curl <content|file> curl command text or a local curl file
231
+ --curl-file <file> Read curl command from a local file
232
+ --out <file> Output file (default: ./cookie.txt)
233
+ --out-dir <dir> Output directory; created when missing
187
234
  `,
188
235
  skill: `@HELP magic-builder/skill
189
236
 
@@ -26,13 +26,18 @@ async function uploadSingle(filePath, opts = {}) {
26
26
 
27
27
  if (signRes.code !== 0) throw new Error(signRes.msg || 'Sign failed');
28
28
  const { signed_url, url } = signRes.data;
29
+ const uploadHeaders = signRes.data.upload_headers || {};
29
30
 
30
31
  const buf = fs.readFileSync(filePath);
31
32
  if (!quiet) process.stderr.write('Uploading... ');
32
33
 
33
34
  await request(signed_url, {
34
35
  method: 'PUT',
35
- headers: { 'Content-Type': mime },
36
+ headers: {
37
+ ...uploadHeaders,
38
+ 'Content-Type': uploadHeaders['Content-Type'] || mime,
39
+ 'Content-Disposition': uploadHeaders['Content-Disposition'] || 'inline',
40
+ },
36
41
  body: buf,
37
42
  timeout: 120000,
38
43
  });