sbuilder-mcp 0.82.3 → 0.82.4

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
@@ -6,6 +6,14 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.82.4] - 2026-10-10
10
+
11
+ ### Fixed
12
+ - `sb_media_upload` with `url` enforces a total time limit on the download, so a server that trickles bytes can no longer hang the call.
13
+ - `sb_media_upload` with `url` rejects a download that switches protocols, closes without answering or returns a status outside 200–599, instead of hanging or crashing the MCP process.
14
+ - `sb_media_upload` with `url` refuses a file larger than the platform's 100 MiB upload cap, early by `content-length` and while the body arrives.
15
+ - `sb_media_upload` with `url` honours the requested IP family and rejects unparseable resolved addresses during the address check.
16
+
9
17
  ## [0.82.3] - 2026-10-10
10
18
 
11
19
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,14 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.82.4] - 2026-10-10
10
+
11
+ ### Fixed
12
+ - `sb_media_upload` với `url` áp dụng hạn chót tổng cho cả lần tải, nên server nhỏ giọt từng byte không còn làm treo lệnh gọi.
13
+ - `sb_media_upload` với `url` từ chối bản tải chuyển giao thức, đóng kết nối mà không trả lời hoặc trả status ngoài 200–599, thay vì treo hay làm sập process MCP.
14
+ - `sb_media_upload` với `url` từ chối file lớn hơn giới hạn upload 100 MiB của platform, ngay từ `content-length` và cả khi nội dung đang về.
15
+ - `sb_media_upload` với `url` tôn trọng họ địa chỉ IP được yêu cầu và từ chối địa chỉ phân giải không hợp lệ trong bước kiểm tra địa chỉ.
16
+
9
17
  ## [0.82.3] - 2026-10-10
10
18
 
11
19
  ### Fixed
@@ -180,40 +180,120 @@ const isPrivate = (a) => PRIVATE.check(a, isIP(a) === 6 ? 'ipv6' : 'ipv4');
180
180
  /** A `lookup` for node:net that refuses to hand a private address to the socket. */
181
181
  export function pinnedLookup(resolve = (h) => lookup(h, { all: true })) {
182
182
  return (host, opts, cb) => {
183
- resolve(host).then((addrs) => {
184
- if (!addrs.length || addrs.some((a) => isPrivate(a.address))) {
185
- cb(new Error('sbuilder: a media url must be a public http(s) address.'));
183
+ resolve(host)
184
+ .then((all) => {
185
+ // The verdict is worked out INSIDE a try and `cb` is called once: a throw
186
+ // here would be an unhandled rejection, which ends the MCP process.
187
+ let err = null;
188
+ let addrs = all;
189
+ try {
190
+ if (!all.length || all.some((a) => isIP(a.address) === 0 || isPrivate(a.address))) {
191
+ err = new Error('sbuilder: a media url must be a public http(s) address.');
192
+ }
193
+ else if (opts.family === 4 || opts.family === 6) {
194
+ addrs = all.filter((a) => a.family === opts.family);
195
+ if (!addrs.length)
196
+ err = new Error(`sbuilder: ${host} has no IPv${opts.family} address.`);
197
+ }
198
+ }
199
+ catch (e) {
200
+ err = e;
186
201
  }
202
+ if (err)
203
+ cb(err);
187
204
  else if (opts.all)
188
205
  cb(null, addrs);
189
206
  else
190
207
  cb(null, addrs[0].address, addrs[0].family);
191
- }, (e) => cb(e));
208
+ }, (e) => cb(e))
209
+ .catch(() => { });
192
210
  };
193
211
  }
194
212
  const NULL_BODY = new Set([101, 204, 205, 304]);
195
- function pinnedFetch(url) {
213
+ /** The platform's own upload cap (`media.MaxUploadBytes`, 100 MiB): nothing larger is worth holding. */
214
+ const MAX_FETCH_BYTES = 100 << 20;
215
+ /**
216
+ * `lookupFn` is the pin, and `deadlineMs` the whole-request limit; both injectable
217
+ * only so a test can reach a local server quickly.
218
+ *
219
+ * EVERY WAY OUT SETTLES THE PROMISE ONCE, and nothing throws from a socket
220
+ * callback (an uncaught exception ends the MCP process): a total deadline (an idle
221
+ * timeout is reset by every trickled byte), a `101`/upgrade or a close with no
222
+ * answer is a rejection, and the body is refused early by content-length and
223
+ * capped as it arrives.
224
+ *
225
+ * ponytail: the body is held whole (about 3 copies at peak, up to the cap); stream
226
+ * it into the upload if concurrent large fetches ever matter.
227
+ */
228
+ export function pinnedFetch(url, lookupFn = pinnedLookup(), deadlineMs = 60_000) {
196
229
  const u = new URL(url);
197
230
  const send = u.protocol === 'https:' ? httpsRequest : httpRequest;
198
231
  return new Promise((resolve, reject) => {
232
+ let done = false;
233
+ const deadline = setTimeout(() => req.destroy(new Error('sbuilder: the media url took too long.')), deadlineMs);
234
+ const ok = (r) => {
235
+ if (done)
236
+ return;
237
+ done = true;
238
+ clearTimeout(deadline);
239
+ resolve(r);
240
+ };
241
+ const fail = (e) => {
242
+ if (done)
243
+ return;
244
+ done = true;
245
+ clearTimeout(deadline);
246
+ reject(e);
247
+ };
199
248
  // The headers global fetch sends: many CDNs answer 403 to a request with no agent.
200
249
  const headers = { 'user-agent': 'sbuilder-mcp', accept: '*/*' };
201
- const req = send(u, { method: 'GET', headers, lookup: pinnedLookup() }, (res) => {
202
- const chunks = [];
203
- res.on('data', (c) => chunks.push(c));
204
- res.on('error', reject);
250
+ const req = send(u, { method: 'GET', headers, lookup: lookupFn }, (res) => {
251
+ if (Number(res.headers['content-length'] ?? 0) > MAX_FETCH_BYTES) {
252
+ req.destroy(new Error('sbuilder: the media url is larger than an upload may be.'));
253
+ return;
254
+ }
255
+ let chunks = [];
256
+ let size = 0;
257
+ res.on('data', (c) => {
258
+ size += c.length;
259
+ if (size > MAX_FETCH_BYTES)
260
+ req.destroy(new Error('sbuilder: the media url is larger than an upload may be.'));
261
+ else
262
+ chunks.push(c);
263
+ });
264
+ res.on('error', fail);
205
265
  res.on('end', () => {
206
- const headers = new Headers();
207
- for (const [k, v] of Object.entries(res.headers)) {
208
- if (v !== undefined)
209
- headers.set(k, Array.isArray(v) ? v.join(', ') : v);
266
+ try {
267
+ const h = new Headers();
268
+ for (const [k, v] of Object.entries(res.headers)) {
269
+ if (v === undefined)
270
+ continue;
271
+ try {
272
+ h.set(k, Array.isArray(v) ? v.join(', ') : v);
273
+ }
274
+ catch {
275
+ // A header value fetch would refuse (non-ByteString) — not one this needs.
276
+ }
277
+ }
278
+ const status = res.statusCode ?? 502;
279
+ if (status < 200 || status > 599)
280
+ throw new Error(`sbuilder: the media url answered status ${status}.`);
281
+ const body = NULL_BODY.has(status) ? null : Buffer.concat(chunks);
282
+ chunks = [];
283
+ ok(new Response(body, { status, headers: h }));
284
+ }
285
+ catch (e) {
286
+ fail(e);
210
287
  }
211
- const status = res.statusCode ?? 502;
212
- resolve(new Response(NULL_BODY.has(status) ? null : Buffer.concat(chunks), { status, headers }));
213
288
  });
214
289
  });
215
290
  req.setTimeout(30_000, () => req.destroy(new Error('sbuilder: the media url timed out.')));
216
- req.on('error', reject);
291
+ req.on('upgrade', (_res, sock) => {
292
+ sock.destroy();
293
+ fail(new Error('sbuilder: the media url tried to switch protocols.'));
294
+ });
295
+ req.on('error', fail);
296
+ req.on('close', () => fail(new Error('sbuilder: the media url closed without an answer.')));
217
297
  req.end();
218
298
  });
219
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.82.3",
3
+ "version": "0.82.4",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",