hostpad 0.2.2 → 0.3.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/lib/bridge.js CHANGED
@@ -108,7 +108,7 @@ function createBridge({
108
108
  const pairId = crypto.randomUUID();
109
109
  const code = codeStore.issue(pairId, pairCode);
110
110
  conn.pair = { pairId };
111
- sendTo(conn, { type: 'pair-challenge', pairId, code });
111
+ sendTo(conn, { type: 'pair-challenge', pairId, code, codeTtlMs });
112
112
  conn.pairTimer = setTimeout(() => {
113
113
  conn.pairTimer = null;
114
114
  if (conn.pair?.pairId === pairId) startPairing(conn);
package/lib/tools.js CHANGED
@@ -47,14 +47,18 @@ function createToolStore(toolsDir) {
47
47
  const toolDir = (id) => path.join(toolsDir, id);
48
48
 
49
49
  function readRaw(id) {
50
+ // id 来自 MCP 输入(AI 可控),读取/删除入口必须先过白名单,防 ../ 穿越
51
+ if (typeof id !== 'string' || !ID_RE.test(id)) return null;
50
52
  const dir = toolDir(id);
51
53
  if (!fs.existsSync(path.join(dir, 'manifest.json'))) return null;
52
54
  const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
53
55
  const files = [];
56
+ const root = path.resolve(dir);
54
57
  const walk = (rel) => {
55
58
  for (const name of fs.readdirSync(path.join(dir, rel))) {
56
59
  const relPath = rel ? `${rel}/${name}` : name;
57
- const abs = path.join(dir, relPath);
60
+ const abs = path.resolve(dir, relPath);
61
+ if (abs !== root && !abs.startsWith(root + path.sep)) continue; // 深度防御:symlink 等逃逸
58
62
  if (fs.statSync(abs).isDirectory()) { walk(relPath); continue; }
59
63
  if (relPath === 'manifest.json') continue;
60
64
  files.push({ path: relPath, content: fs.readFileSync(abs, 'utf8') });
@@ -120,6 +124,7 @@ function createToolStore(toolsDir) {
120
124
  },
121
125
 
122
126
  remove(id) {
127
+ if (typeof id !== 'string' || !ID_RE.test(id)) throw new Error(`非法工具 id: ${id}`);
123
128
  fs.rmSync(toolDir(id), { recursive: true, force: true });
124
129
  },
125
130
  };
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "hostpad",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "HostPad 的 MCP 服务器 + iPhone 同步桥:AI agent 经 MCP 管理与调试 iOS 工具(写→推→读日志闭环)",
5
5
  "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Huang-Yangkai/hostpad.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/Huang-Yangkai/hostpad/issues"
12
+ },
6
13
  "bin": {
7
14
  "hostpad": "./bin/hostpad.js",
8
15
  "hostpad-dev": "./server.js"
package/server.js CHANGED
@@ -9,7 +9,6 @@
9
9
  //
10
10
  // 正式的 agent 入口是 bin/hostpad.js(MCP 服务器,`npx hostpad`)。
11
11
  'use strict';
12
- const fs = require('node:fs');
13
12
  const path = require('node:path');
14
13
  const readline = require('node:readline');
15
14
  const { createBridge } = require('./lib/bridge.js');
@@ -75,17 +74,12 @@ bridge.start().then(() => {
75
74
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
76
75
 
77
76
  async function runSoak() {
78
- const dir = bridge.store.list().map((id) => path.join(TOOLS_DIR, id))
79
- .find((d) => {
80
- try {
81
- const m = JSON.parse(fs.readFileSync(path.join(d, 'manifest.json'), 'utf8'));
82
- return fs.readFileSync(path.join(d, m.entry), 'utf8').includes('SOAK_MARKER');
83
- } catch { return false; }
84
- });
85
- if (!dir) { console.log('[soak] 没有含 SOAK_MARKER 的工具,无法压测'); process.exit(2); }
86
- const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
87
- const entryPath = path.join(dir, manifest.entry);
88
- const template = fs.readFileSync(entryPath, 'utf8');
77
+ // 目标工具:entry SOAK_MARKER;每轮 reload 强推触发 App 热重载(走 bridge 原生推送,不直接碰文件系统)
78
+ const tool = bridge.store.list()
79
+ .map((id) => bridge.store.read(id))
80
+ .find((t) => t?.files.some((f) => f.path === t.manifest.entry && f.content.includes('SOAK_MARKER')));
81
+ if (!tool) { console.log('[soak] 没有含 SOAK_MARKER 的工具,无法压测'); process.exit(2); }
82
+ const { manifest } = tool;
89
83
  console.log(`[soak] tool=${manifest.id} cycles=${SOAK_N} interval=${SOAK_INTERVAL}ms timeout=${SOAK_TIMEOUT}ms`);
90
84
  console.log('[soak] 等待 App 连接…');
91
85
  while (bridge.listDevices().filter((d) => d.authed).length === 0) await sleep(200);
@@ -93,7 +87,7 @@ async function runSoak() {
93
87
 
94
88
  const results = [];
95
89
  for (let i = 1; i <= SOAK_N; i++) {
96
- fs.writeFileSync(entryPath, template.replace(/SOAK_MARKER/g, `#${i}`)); // 触发 watch push
90
+ bridge.reload(manifest.id); // force 重推(无变化也推)→ App 热重载
97
91
  const r = await bridge.waitForLoaded(manifest.id, SOAK_TIMEOUT);
98
92
  results.push(r);
99
93
  console.log(`[soak] ${i}/${SOAK_N} ${r.ok ? `${r.ms}ms` : `FAIL(>${SOAK_TIMEOUT}ms)`}`);