@farmslot/adapter-web 0.6.1 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ All notable changes to `@farmslot/adapter-web` are tracked here.
6
6
 
7
7
  - Active-development baseline; add user-facing changes here before release or package publication.
8
8
 
9
+ ## 0.6.2 - 2026-10-08
10
+
11
+ - Fix: `launch-browser` no longer builds its `node -e` CDP request from the port, path or target id. They reach the child as argv, `cdpHttp` takes only an integer port from 1 to 65535 and the paths it uses (`/json/list`, `/json/version`, `/json/close/<id>`), and closing duplicate home tabs skips, with a warning, a target id from `/json/list` that is not a CDP id (letters, digits, `-`). Before, a hostile or substituted local CDP endpoint could run code in that child through a crafted target id. `cdpHttp` and `pruneExtraHomeTabs` are exported for tests. ([#859](https://github.com/deeeed/farmslot/pull/859))
12
+ - Publish with adapter-sdk 0.7.1 and recipe-runner 0.27.0 so consumers share one adapter-sdk and one recipe-runner copy.
13
+
9
14
  ## 0.6.1 - 2026-10-08
10
15
 
11
16
  - `web-dapp` refuses a headful launch while the macOS login session is locked (`SESSION_LOCKED`, with the unlock step), before starting, reusing or stopping a browser, as mm-harness does since its #363 (0.6.0 shipped `web-dapp` without it). `macosSessionLocked()`, `parseSessionLocked()`, `SESSION_LOCKED_MESSAGE` and `SESSION_LOCKED_USER_ACTION` are exported.
@@ -116,3 +116,22 @@ export function homeTabsToClose(targets: unknown[], { extensionId, homePage, def
116
116
  id: string;
117
117
  url: string;
118
118
  }[];
119
+ /**
120
+ * GET a CDP HTTP path on 127.0.0.1. Returns the body, or null when the port or
121
+ * path is not one the launcher uses, or the request fails.
122
+ * @param {number} port
123
+ * @param {string} pathname `/json/list`, `/json/version` or `/json/close/<target id>`
124
+ * @returns {string | null}
125
+ */
126
+ export function cdpHttp(port: number, pathname: string): string | null;
127
+ /**
128
+ * Close the extra `homePage` tabs and stray blank tabs the browser on `port` has.
129
+ * @param {number} port CDP port on 127.0.0.1.
130
+ * @param {string} extensionDir The unpacked extension, for its id.
131
+ * @param {{ homePage?: string, defaultTitle?: string }} options
132
+ * @returns {string[]} What could not be done, one line each; empty when all went well.
133
+ */
134
+ export function pruneExtraHomeTabs(port: number, extensionDir: string, { homePage, defaultTitle }: {
135
+ homePage?: string;
136
+ defaultTitle?: string;
137
+ }): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farmslot/adapter-web",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -643,25 +643,39 @@ function extensionPageUrl(extensionDir, page) {
643
643
  return id ? `chrome-extension://${id}/${page}` : '';
644
644
  }
645
645
 
646
+ // The CDP HTTP paths the launcher reads: the target list, the version, or one
647
+ // target to close. CDP target ids are hex or UUIDs.
648
+ const CDP_HTTP_PATH = /^\/json\/(?:list|version|close\/[A-Za-z0-9-]+)$/u;
649
+ const CDP_TARGET_ID = /^[A-Za-z0-9-]+$/u;
650
+ // A child node keeps cdpHttp synchronous. The port and path reach it as argv,
651
+ // never as script source, so a path cannot change what it runs.
652
+ const CDP_HTTP_SCRIPT = `
653
+ const http = require('http');
654
+ const [port, pathname] = process.argv.slice(1);
655
+ http.get({ host: '127.0.0.1', port: Number(port), path: pathname }, (res) => {
656
+ let body = '';
657
+ res.on('data', (chunk) => { body += chunk; });
658
+ res.on('end', () => {
659
+ process.stdout.write(body);
660
+ process.exitCode = res.statusCode === 200 ? 0 : 1;
661
+ });
662
+ }).on('error', () => process.exit(1));
663
+ `;
664
+
665
+ /**
666
+ * GET a CDP HTTP path on 127.0.0.1. Returns the body, or null when the port or
667
+ * path is not one the launcher uses, or the request fails.
668
+ * @param {number} port
669
+ * @param {string} pathname `/json/list`, `/json/version` or `/json/close/<target id>`
670
+ * @returns {string | null}
671
+ */
646
672
  function cdpHttp(port, pathname) {
647
- const result = spawnSync(
648
- process.execPath,
649
- [
650
- '-e',
651
- `
652
- const http = require('http');
653
- http.get('http://127.0.0.1:${Number(port)}${pathname}', (res) => {
654
- let body = '';
655
- res.on('data', (chunk) => { body += chunk; });
656
- res.on('end', () => {
657
- process.stdout.write(body);
658
- process.exitCode = res.statusCode === 200 ? 0 : 1;
659
- });
660
- }).on('error', () => process.exit(1));
661
- `,
662
- ],
663
- { encoding: 'utf8', timeout: 4000 },
664
- );
673
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
674
+ if (typeof pathname !== 'string' || !CDP_HTTP_PATH.test(pathname)) return null;
675
+ const result = spawnSync(process.execPath, ['-e', CDP_HTTP_SCRIPT, String(port), pathname], {
676
+ encoding: 'utf8',
677
+ timeout: 4000,
678
+ });
665
679
  if (result.status !== 0) return null;
666
680
  return result.stdout;
667
681
  }
@@ -710,6 +724,13 @@ function homeTabsToClose(targets, { extensionId, homePage, defaultTitle }) {
710
724
  // readiness converges home tabs again, so a pruning failure must not fail the
711
725
  // launch. Every failure is returned so the launch log says why a duplicate tab
712
726
  // survived instead of hiding it.
727
+ /**
728
+ * Close the extra `homePage` tabs and stray blank tabs the browser on `port` has.
729
+ * @param {number} port CDP port on 127.0.0.1.
730
+ * @param {string} extensionDir The unpacked extension, for its id.
731
+ * @param {{ homePage?: string, defaultTitle?: string }} options
732
+ * @returns {string[]} What could not be done, one line each; empty when all went well.
733
+ */
713
734
  function pruneExtraHomeTabs(port, extensionDir, { homePage, defaultTitle }) {
714
735
  if (!homePage) return [];
715
736
  const extensionId = extensionIdFromExtensionDir(extensionDir);
@@ -725,10 +746,17 @@ function pruneExtraHomeTabs(port, extensionDir, { homePage, defaultTitle }) {
725
746
  if (!Array.isArray(targets)) return ['/json/list did not return a target list'];
726
747
  const problems = [];
727
748
  for (const target of homeTabsToClose(targets, { extensionId, homePage, defaultTitle })) {
728
- if (cdpHttp(port, `/json/close/${target.id}`) === null)
749
+ // The id comes from the endpoint's own response: close only a CDP-shaped one.
750
+ if (!CDP_TARGET_ID.test(target.id)) {
751
+ problems.push(
752
+ `skipped target with an invalid id: ${JSON.stringify(target.id)} (${target.url})`,
753
+ );
754
+ continue;
755
+ }
756
+ if (cdpHttp(port, `/json/close/${encodeURIComponent(target.id)}`) === null)
729
757
  problems.push(`could not close ${target.url} (${target.id})`);
730
758
  }
731
759
  return problems;
732
760
  }
733
761
 
734
- module.exports = { launchBrowser, homeTabsToClose };
762
+ module.exports = { launchBrowser, homeTabsToClose, cdpHttp, pruneExtraHomeTabs };