sbuilder-mcp 0.82.2 → 0.82.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
@@ -6,6 +6,12 @@ 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.3] - 2026-10-10
10
+
11
+ ### Fixed
12
+ - `sb_media_upload` with `url` pins the address check to the connection itself, so a DNS answer that changes between the check and the connect (DNS rebinding) can no longer reach a private or loopback host.
13
+ - `sb_media_upload` with `url` sends a user-agent header when it fetches a file, so CDNs that answer 403 without one now serve the download.
14
+
9
15
  ## [0.82.2] - 2026-10-10
10
16
 
11
17
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,12 @@ 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.3] - 2026-10-10
10
+
11
+ ### Fixed
12
+ - `sb_media_upload` với `url` ghim bước kiểm tra địa chỉ vào chính kết nối, nên câu trả lời DNS thay đổi giữa lúc kiểm tra và lúc kết nối (DNS rebinding) không còn dẫn tới host riêng hoặc loopback.
13
+ - `sb_media_upload` với `url` gửi header user-agent khi tải file, nên các CDN trả 403 khi thiếu header này giờ cho tải bình thường.
14
+
9
15
  ## [0.82.2] - 2026-10-10
10
16
 
11
17
  ### Fixed
@@ -4,6 +4,8 @@ import { guardLocal } from './localfile.js';
4
4
  import { basename, extname } from 'node:path';
5
5
  import { lookup } from 'node:dns/promises';
6
6
  import { BlockList, isIP } from 'node:net';
7
+ import { request as httpRequest } from 'node:http';
8
+ import { request as httpsRequest } from 'node:https';
7
9
  import { ApiError } from './http.js';
8
10
  import { siteToken } from '../tools/credentialpick.js';
9
11
  /**
@@ -140,9 +142,10 @@ async function fromUrl(ctx, siteId, url, source) {
140
142
  * checked — a hostname regex is beaten by `[::ffff:127.0.0.1]` or a DNS name that
141
143
  * points inward — and a redirect is followed by hand, each hop checked again.
142
144
  *
143
- * ponytail: resolve-then-fetch leaves a DNS-rebinding window between the two
144
- * lookups; pinning the checked IP into the connection (an undici dispatcher) is the
145
- * upgrade.
145
+ * The check is PINNED to the connection: the real fetch goes through node:http(s)
146
+ * with a `lookup` that checks the very address it hands to the socket, so a DNS
147
+ * answer that changes between "check" and "connect" (rebinding) still meets the
148
+ * check. Only an injected `ctx.fetchImpl` (tests) skips that path.
146
149
  */
147
150
  // No ::ffff:0:0/96 rule: Node's BlockList already applies the IPv4 rules to an
148
151
  // IPv4-mapped address, and that rule would match EVERY IPv4 address (measured).
@@ -169,15 +172,56 @@ async function assertPublic(ctx, raw) {
169
172
  throw new Error(`sbuilder: could not resolve ${host}.`);
170
173
  });
171
174
  for (const a of addrs) {
172
- if (PRIVATE.check(a, isIP(a) === 6 ? 'ipv6' : 'ipv4'))
175
+ if (isPrivate(a))
173
176
  throw refuse();
174
177
  }
175
178
  }
176
- async function fetchPublic(ctx, doFetch, url) {
179
+ const isPrivate = (a) => PRIVATE.check(a, isIP(a) === 6 ? 'ipv6' : 'ipv4');
180
+ /** A `lookup` for node:net that refuses to hand a private address to the socket. */
181
+ export function pinnedLookup(resolve = (h) => lookup(h, { all: true })) {
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.'));
186
+ }
187
+ else if (opts.all)
188
+ cb(null, addrs);
189
+ else
190
+ cb(null, addrs[0].address, addrs[0].family);
191
+ }, (e) => cb(e));
192
+ };
193
+ }
194
+ const NULL_BODY = new Set([101, 204, 205, 304]);
195
+ function pinnedFetch(url) {
196
+ const u = new URL(url);
197
+ const send = u.protocol === 'https:' ? httpsRequest : httpRequest;
198
+ return new Promise((resolve, reject) => {
199
+ // The headers global fetch sends: many CDNs answer 403 to a request with no agent.
200
+ 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);
205
+ 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);
210
+ }
211
+ const status = res.statusCode ?? 502;
212
+ resolve(new Response(NULL_BODY.has(status) ? null : Buffer.concat(chunks), { status, headers }));
213
+ });
214
+ });
215
+ req.setTimeout(30_000, () => req.destroy(new Error('sbuilder: the media url timed out.')));
216
+ req.on('error', reject);
217
+ req.end();
218
+ });
219
+ }
220
+ export async function fetchPublic(ctx, doFetch, url) {
177
221
  let at = url;
178
222
  for (let hop = 0; hop < 4; hop++) {
179
223
  await assertPublic(ctx, at);
180
- const res = await doFetch(at, { redirect: 'manual' });
224
+ const res = ctx.fetchImpl ? await doFetch(at, { redirect: 'manual' }) : await pinnedFetch(at);
181
225
  const next = res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
182
226
  if (!next)
183
227
  return res;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.82.2",
3
+ "version": "0.82.3",
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",