@blocofy/cli 0.17.0 → 0.17.1

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
@@ -132,6 +132,9 @@ and editor views for this command are retired; to share a draft page, create a p
132
132
  it (v1 API `POST /themes/{id}/preview-links` or MCP `create_preview_link`; one page per link).
133
133
  The target site is verified once at start; a long session keeps that target (restart it after
134
134
  changing credentials, context or the project binding).
135
+ The dev server answers this machine only: it listens on `http://127.0.0.1:<port>`, refuses a request
136
+ whose `Host` (or `Origin`, when sent) is not that address or `localhost:<port>`, and answers only
137
+ `GET`/`HEAD` (anything else gets `405`). The reload channel is same-origin only.
135
138
  - `--port <n>` — local port (default 3030).
136
139
  - `--no-sync` — local preview only (skip draft sync).
137
140
  - `--name <name>` — name the draft when it is first created (ignored if it already exists).
@@ -623,6 +626,15 @@ Each retry prints a notice on stderr.
623
626
 
624
627
  ## Changelog
625
628
 
629
+ - **0.17.1** (2026-11-01) — Security fix: `theme dev` answers this machine only.
630
+ - The local server listens on IPv4 loopback (`127.0.0.1`) instead of every network interface, and
631
+ prints that address. There is no option to open it to the network.
632
+ - A request whose `Host` is not `127.0.0.1:<port>` / `localhost:<port>`, or whose `Origin` is another
633
+ site, is refused before anything is read or rendered (DNS-rebinding and cross-site pages).
634
+ - Only `GET` and `HEAD` are answered; other methods get `405` with `Allow: GET, HEAD` and never reach
635
+ the platform. `HEAD` returns headers only and never spends a render.
636
+ - The reload channel no longer sends `Access-Control-Allow-Origin: *`; the local page still reloads on
637
+ save. Views on other origins no longer subscribe to it.
626
638
  - **0.17.0** (2026-10-31) — `theme publish` tells you when the live site is not serving the files
627
639
  you published.
628
640
  - **`theme publish` reports an unrefreshed live site.** When the platform answers
package/bin/blocofy.mjs CHANGED
@@ -2596,7 +2596,8 @@ async function themeDev(rest) {
2596
2596
  }
2597
2597
  }
2598
2598
 
2599
- const localUrl = `http://localhost:${port}`;
2599
+ // The URL the dev server actually binds (IPv4 loopback): `localhost` may resolve to ::1 first.
2600
+ const localUrl = `http://127.0.0.1:${port}`;
2600
2601
  // A session without a URL (an older platform answer) must not become "undefined&hr=…".
2601
2602
  const previewUrl = session?.previewUrl ? `${session.previewUrl}&hr=${port}` : null;
2602
2603
  const editorUrl = session?.editorUrl ? `${session.editorUrl}&hr=${port}` : null;
@@ -2616,7 +2617,7 @@ async function themeDev(rest) {
2616
2617
  .filter(Boolean)
2617
2618
  .join(" ");
2618
2619
  console.log(`\n Press: ${keys}`);
2619
- console.log(` Edit a theme file and save — every open view reloads automatically.\n`);
2620
+ console.log(` Edit a theme file and save — the local view reloads automatically.\n`);
2620
2621
 
2621
2622
  // Senkron kapsamı (#119 CLI bulgu #2): hangi dizinler taşınıyor / taşınmıyor —
2622
2623
  // "config/pages senkronlanıyor sandım" karışıklığını açıkça önler.
@@ -11,6 +11,43 @@ import {
11
11
  } from "./local-theme.mjs";
12
12
  import { DRAFT_TARGET_AMBIGUOUS, draftSyncErrorLine, pushTheme } from "./theme-sync.mjs";
13
13
 
14
+ /**
15
+ * The dev server proxies page renders with the developer's bearer, so it answers only this machine:
16
+ * it binds IPv4 loopback, and every request must name it (Host + optional Origin) before any branch runs.
17
+ * There is no LAN option on purpose — exposing it needs its own auth/threat model.
18
+ */
19
+ const DEV_SERVER_HOST = "127.0.0.1";
20
+ const ALLOWED_METHODS = "GET, HEAD";
21
+
22
+ /** Why `req` is refused (status + short reason; never echoes request data or the credential), or null. */
23
+ function refusal(req, port) {
24
+ if (req.method !== "GET" && req.method !== "HEAD") return { status: 405, reason: "Method not allowed." };
25
+ // Origin-form only: an absolute request target (proxy style) could name another authority.
26
+ if (typeof req.url !== "string" || !req.url.startsWith("/")) return { status: 400, reason: "Bad request target." };
27
+ const ownHosts = new Set([`${DEV_SERVER_HOST}:${port}`, `localhost:${port}`]);
28
+ const ownOrigins = new Set([...ownHosts].map((host) => `http://${host}`));
29
+ // rawHeaders keeps duplicates that req.headers would silently collapse.
30
+ const headerValues = (name) => {
31
+ const values = [];
32
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
33
+ if (req.rawHeaders[i].toLowerCase() === name) values.push(req.rawHeaders[i + 1].trim().toLowerCase());
34
+ }
35
+ return values;
36
+ };
37
+ // Exactly one Host, and it is this server: a DNS-rebinding page arrives with its own host name.
38
+ // X-Forwarded-Host / Forwarded / Referer are never read.
39
+ const hosts = headerValues("host");
40
+ if (hosts.length !== 1 || !ownHosts.has(hosts[0])) {
41
+ return { status: 403, reason: `This dev server only answers http://${DEV_SERVER_HOST}:${port}.` };
42
+ }
43
+ // A browser that sends Origin must be on this server's own origin (cross-origin pages and `null` refused).
44
+ const origins = headerValues("origin");
45
+ if (origins.length > 1 || (origins.length === 1 && !ownOrigins.has(origins[0]))) {
46
+ return { status: 403, reason: `Cross-origin requests are not served; open http://${DEV_SERVER_HOST}:${port}.` };
47
+ }
48
+ return null;
49
+ }
50
+
14
51
  /**
15
52
  * Standalone dev server. For each page request it reads the local theme files and
16
53
  * posts them to the platform's `/api/dev/render` endpoint, then injects a livereload
@@ -105,18 +142,29 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, insta
105
142
  }
106
143
 
107
144
  const server = createServer(async (req, res) => {
145
+ // Before every branch: no SSE subscription, disk read or authenticated render for a refused request.
146
+ const refused = refusal(req, server.address()?.port);
147
+ if (refused) {
148
+ res.writeHead(refused.status, {
149
+ "content-type": "text/plain; charset=utf-8",
150
+ "cache-control": "no-store",
151
+ ...(refused.status === 405 ? { allow: ALLOWED_METHODS } : {}),
152
+ });
153
+ res.end(`${refused.reason}\n`);
154
+ return;
155
+ }
156
+ const head = req.method === "HEAD";
108
157
  try {
109
158
  const pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
110
159
 
111
- // Livereload SSE channel.
160
+ // Livereload SSE channel — same-origin only (no CORS header): cross-origin views no longer subscribe.
112
161
  if (pathname === LIVERELOAD_PATH) {
113
162
  res.writeHead(200, {
114
163
  "content-type": "text/event-stream",
115
164
  "cache-control": "no-cache, no-transform",
116
- connection: "keep-alive",
117
- // Cross-origin iframe'ler (canlı-domain önizleme + editör) subscribe edebilsin.
118
- "access-control-allow-origin": "*",
165
+ ...(head ? {} : { connection: "keep-alive" }),
119
166
  });
167
+ if (head) return void res.end();
120
168
  res.write("retry: 1000\n\n");
121
169
  clients.add(res);
122
170
  req.on("close", () => clients.delete(res));
@@ -127,7 +175,14 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, insta
127
175
  const assetFile = safeAssetFile(dir, pathname);
128
176
  if (assetFile) {
129
177
  res.writeHead(200, { "content-type": contentTypeFor(assetFile), "cache-control": "no-store" });
130
- res.end(readFileSync(assetFile));
178
+ res.end(head ? undefined : readFileSync(assetFile));
179
+ return;
180
+ }
181
+
182
+ // HEAD never spends an authenticated render: headers only.
183
+ if (head) {
184
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
185
+ res.end();
131
186
  return;
132
187
  }
133
188
 
@@ -166,7 +221,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, insta
166
221
  onReload?.({ file, synced: syncDraft && !error, clients: clients.size, error });
167
222
  });
168
223
 
169
- server.listen(port);
224
+ server.listen(port, DEV_SERVER_HOST);
170
225
 
171
226
  return {
172
227
  server,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Develop your Blocofy theme locally against live data with instant preview — login, theme dev, pull, push.",
5
5
  "type": "module",
6
6
  "license": "MIT",