ftown 0.19.52 → 0.19.54

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.
Files changed (139) hide show
  1. package/claude-mod/hooks/register.js +6 -5
  2. package/dist/bridge-auth.d.ts +4 -0
  3. package/dist/bridge-auth.js +2 -2
  4. package/dist/bridge-auth.js.map +1 -1
  5. package/dist/centrifugo-client.d.ts +11 -1
  6. package/dist/centrifugo-client.js +71 -69
  7. package/dist/centrifugo-client.js.map +1 -1
  8. package/dist/cli-client.d.ts +6 -0
  9. package/dist/cli-client.js +43 -18
  10. package/dist/cli-client.js.map +1 -1
  11. package/dist/command-rpc.d.ts +5 -2
  12. package/dist/command-rpc.js +62 -16
  13. package/dist/command-rpc.js.map +1 -1
  14. package/dist/direct-transport/loopback-server.d.ts +5 -1
  15. package/dist/direct-transport/loopback-server.js +19 -6
  16. package/dist/direct-transport/loopback-server.js.map +1 -1
  17. package/dist/direct-transport/peer-manager.d.ts +4 -0
  18. package/dist/direct-transport/peer-manager.js +16 -2
  19. package/dist/direct-transport/peer-manager.js.map +1 -1
  20. package/dist/ftown-cli.js +4 -1
  21. package/dist/ftown-cli.js.map +1 -1
  22. package/dist/index.js +100 -40
  23. package/dist/index.js.map +1 -1
  24. package/dist/local-api-server.d.ts +12 -0
  25. package/dist/local-api-server.js +182 -41
  26. package/dist/local-api-server.js.map +1 -1
  27. package/dist/local-pair-cli.d.ts +2 -3
  28. package/dist/local-pair-cli.js +12 -53
  29. package/dist/local-pair-cli.js.map +1 -1
  30. package/dist/mcp/fleet.d.ts +11 -9
  31. package/dist/mcp/fleet.js +61 -44
  32. package/dist/mcp/fleet.js.map +1 -1
  33. package/dist/mcp/http.d.ts +10 -9
  34. package/dist/mcp/http.js +133 -8
  35. package/dist/mcp/http.js.map +1 -1
  36. package/dist/mcp/legacy-relay.d.ts +2 -2
  37. package/dist/mcp/legacy-relay.js +4 -43
  38. package/dist/mcp/legacy-relay.js.map +1 -1
  39. package/dist/mcp/mcp-key-store.d.ts +52 -0
  40. package/dist/mcp/mcp-key-store.js +206 -0
  41. package/dist/mcp/mcp-key-store.js.map +1 -0
  42. package/dist/mcp/oauth.d.ts +5 -1
  43. package/dist/mcp/oauth.js +113 -39
  44. package/dist/mcp/oauth.js.map +1 -1
  45. package/dist/mcp/relay-handler.d.ts +2 -1
  46. package/dist/mcp/relay-handler.js +5 -40
  47. package/dist/mcp/relay-handler.js.map +1 -1
  48. package/dist/mcp/relay.d.ts +4 -4
  49. package/dist/mcp/relay.js +35 -85
  50. package/dist/mcp/relay.js.map +1 -1
  51. package/dist/mcp/secure-client.d.ts +23 -0
  52. package/dist/mcp/secure-client.js +124 -0
  53. package/dist/mcp/secure-client.js.map +1 -0
  54. package/dist/mcp/security-events.d.ts +65 -0
  55. package/dist/mcp/security-events.js +210 -0
  56. package/dist/mcp/security-events.js.map +1 -0
  57. package/dist/mcp/server.d.ts +1 -0
  58. package/dist/mcp/server.js +2 -2
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/mcp/test-support.d.ts +32 -0
  61. package/dist/mcp/test-support.js +120 -0
  62. package/dist/mcp/test-support.js.map +1 -0
  63. package/dist/mcp-cli.js +56 -36
  64. package/dist/mcp-cli.js.map +1 -1
  65. package/dist/pairing-client.d.ts +10 -12
  66. package/dist/pairing-client.js +36 -9
  67. package/dist/pairing-client.js.map +1 -1
  68. package/dist/push-notifier.js +3 -1
  69. package/dist/push-notifier.js.map +1 -1
  70. package/dist/security/account-crypto.d.ts +33 -0
  71. package/dist/security/account-crypto.js +649 -0
  72. package/dist/security/account-crypto.js.map +1 -0
  73. package/dist/security/account-protocol.d.ts +228 -0
  74. package/dist/security/account-protocol.js +2 -0
  75. package/dist/security/account-protocol.js.map +1 -0
  76. package/dist/security/bridge/account-client.d.ts +7 -0
  77. package/dist/security/bridge/account-client.js +87 -0
  78. package/dist/security/bridge/account-client.js.map +1 -0
  79. package/dist/security/bridge/account-state.d.ts +471 -0
  80. package/dist/security/bridge/account-state.js +78 -0
  81. package/dist/security/bridge/account-state.js.map +1 -0
  82. package/dist/security/bridge/audit.d.ts +37 -0
  83. package/dist/security/bridge/audit.js +185 -0
  84. package/dist/security/bridge/audit.js.map +1 -0
  85. package/dist/security/bridge/direct.d.ts +22 -0
  86. package/dist/security/bridge/direct.js +72 -0
  87. package/dist/security/bridge/direct.js.map +1 -0
  88. package/dist/security/bridge/errors.d.ts +7 -0
  89. package/dist/security/bridge/errors.js +12 -0
  90. package/dist/security/bridge/errors.js.map +1 -0
  91. package/dist/security/bridge/execute.d.ts +3 -0
  92. package/dist/security/bridge/execute.js +38 -0
  93. package/dist/security/bridge/execute.js.map +1 -0
  94. package/dist/security/bridge/identity.d.ts +441 -0
  95. package/dist/security/bridge/identity.js +64 -0
  96. package/dist/security/bridge/identity.js.map +1 -0
  97. package/dist/security/bridge/local-client.d.ts +4 -0
  98. package/dist/security/bridge/local-client.js +43 -0
  99. package/dist/security/bridge/local-client.js.map +1 -0
  100. package/dist/security/bridge/metadata.d.ts +99 -0
  101. package/dist/security/bridge/metadata.js +23 -0
  102. package/dist/security/bridge/metadata.js.map +1 -0
  103. package/dist/security/bridge/native-wake.d.ts +16 -0
  104. package/dist/security/bridge/native-wake.js +15 -0
  105. package/dist/security/bridge/native-wake.js.map +1 -0
  106. package/dist/security/bridge/pending-enrollment.d.ts +12 -0
  107. package/dist/security/bridge/pending-enrollment.js +113 -0
  108. package/dist/security/bridge/pending-enrollment.js.map +1 -0
  109. package/dist/security/bridge/permissions.d.ts +11 -0
  110. package/dist/security/bridge/permissions.js +97 -0
  111. package/dist/security/bridge/permissions.js.map +1 -0
  112. package/dist/security/bridge/policy.d.ts +55 -0
  113. package/dist/security/bridge/policy.js +415 -0
  114. package/dist/security/bridge/policy.js.map +1 -0
  115. package/dist/security/bridge/security.d.ts +61 -0
  116. package/dist/security/bridge/security.js +388 -0
  117. package/dist/security/bridge/security.js.map +1 -0
  118. package/dist/security/bridge/snapshot.d.ts +14 -0
  119. package/dist/security/bridge/snapshot.js +77 -0
  120. package/dist/security/bridge/snapshot.js.map +1 -0
  121. package/dist/security/crypto.d.ts +17 -0
  122. package/dist/security/crypto.js +280 -0
  123. package/dist/security/crypto.js.map +1 -0
  124. package/dist/security/protocol.d.ts +400 -0
  125. package/dist/security/protocol.js +9 -0
  126. package/dist/security/protocol.js.map +1 -0
  127. package/dist/security-cli.d.ts +3 -0
  128. package/dist/security-cli.js +69 -0
  129. package/dist/security-cli.js.map +1 -0
  130. package/dist/solo/solo-server.js +24 -2
  131. package/dist/solo/solo-server.js.map +1 -1
  132. package/dist/types.d.ts +1 -1
  133. package/dist/wire-types.d.ts +3 -0
  134. package/dist/wire-types.js.map +1 -1
  135. package/hooks/notify.sh +1 -1
  136. package/muse-plugin/lib/ftown-hook.sh +6 -22
  137. package/opencode-plugin/ftown.js +33 -5
  138. package/package.json +4 -2
  139. package/pi-extension/ftown.js +33 -5
@@ -40,7 +40,7 @@ add_candidate() {
40
40
  ENV_PORT="${FTOWN_HOOK_PORT:-}"
41
41
  ENV_TOKEN="${FTOWN_HOOK_TOKEN:-}"
42
42
  add_candidate "$ENV_PORT" "$ENV_TOKEN"
43
- BRIDGE_JSON="${HOME:-}/.ftown/bridge.json"
43
+ BRIDGE_JSON="${FTOWN_BRIDGE_POINTER:-${HOME:-}/.ftown/bridge.json}"
44
44
  if [ -n "${HOME:-}" ] && [ -f "$BRIDGE_JSON" ]; then
45
45
  BPORT=$(jq -r '.port // empty' "$BRIDGE_JSON" 2>/dev/null)
46
46
  BTOKEN=$(jq -r '.token // empty' "$BRIDGE_JSON" 2>/dev/null)
@@ -89,27 +89,11 @@ post_hook "$PAYLOAD"
89
89
  if [ "$HOOK_EVENT" = "Stop" ]; then
90
90
  SID_ENC=$(jq -n -r --arg s "$FTOWN_SESSION_ID" '$s | @uri' 2>/dev/null)
91
91
  [ -n "$SID_ENC" ] || exit 0
92
- _tmp=$(mktemp /tmp/ftown-muse-inbox.XXXXXX 2>/dev/null) || exit 0
93
- INBOX=""
94
- while IFS=' ' read -r _port _token _rest; do
95
- [ -n "${_port:-}" ] || continue
96
- if [ -n "${_token:-}" ]; then
97
- _code=$(curl -s -o "$_tmp" -w '%{http_code}' \
98
- "http://127.0.0.1:${_port}/api/sessions/${SID_ENC}/inbox?wait=0" \
99
- -H "Authorization: Bearer ${_token}" \
100
- --connect-timeout 1 --max-time 2 2>/dev/null)
101
- else
102
- _code=$(curl -s -o "$_tmp" -w '%{http_code}' \
103
- "http://127.0.0.1:${_port}/api/sessions/${SID_ENC}/inbox?wait=0" \
104
- --connect-timeout 1 --max-time 2 2>/dev/null)
105
- fi
106
- case "${_code:-}" in
107
- 2*) INBOX=$(cat "$_tmp" 2>/dev/null); break ;;
108
- esac
109
- done <<CANDIDATES_EOF
110
- $CANDIDATES
111
- CANDIDATES_EOF
112
- rm -f "$_tmp"
92
+ FTOWN_SECURE_CLI=$(command -v ftown 2>/dev/null || true)
93
+ [ -n "$FTOWN_SECURE_CLI" ] || FTOWN_SECURE_CLI="${HOME:-}/.ftown/bin/ftown"
94
+ [ -x "$FTOWN_SECURE_CLI" ] || exit 0
95
+ REQUEST=$(jq -n -c --arg path "/api/sessions/${SID_ENC}/inbox?wait=0" '{method:"GET",path:$path,timeoutMs:2000}') || exit 0
96
+ INBOX=$(printf '%s' "$REQUEST" | "$FTOWN_SECURE_CLI" security proxy 2>/dev/null | jq -c 'select(.status >= 200 and .status < 300) | .body' 2>/dev/null)
113
97
  [ -n "$INBOX" ] || exit 0
114
98
  COUNT=$(printf '%s' "$INBOX" | jq -r '(.messages // []) | length' 2>/dev/null)
115
99
  if [ -n "$COUNT" ] && [ "$COUNT" -gt 0 ] 2>/dev/null; then
@@ -1,21 +1,45 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
 
5
6
  async function defaultReadBridgePointer() {
6
7
  try {
7
- const raw = await readFile(join(homedir(), '.ftown', 'bridge.json'), 'utf8');
8
+ const raw = await readFile(process.env.FTOWN_BRIDGE_POINTER || (process.env.FTOWN_DATA_DIR ? join(process.env.FTOWN_DATA_DIR, 'bridge.json') : join(homedir(), '.ftown', 'bridge.json')), 'utf8');
8
9
  const parsed = JSON.parse(raw);
9
10
  if (!parsed || typeof parsed.port !== 'number') return null;
10
- return {
11
- port: parsed.port,
12
- token: typeof parsed.token === 'string' ? parsed.token : undefined,
13
- };
11
+ return parsed;
14
12
  } catch {
15
13
  return null;
16
14
  }
17
15
  }
18
16
 
17
+
18
+ async function secureProxy(env, pointer, path, init = {}) {
19
+ const cli = pointer?.harnessCli || join(homedir(), '.ftown', 'cli', 'dist', 'ftown-cli.js');
20
+ const args = [cli, 'security'];
21
+ const directory = env.FTOWN_DATA_DIR || pointer?.dataDir;
22
+ if (directory) args.push('--data-dir', directory);
23
+ args.push('proxy');
24
+ const request = { method: init.method || 'GET', path, timeoutMs: 60000 };
25
+ if (init.body !== undefined) request.body = typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(process.execPath, args, { stdio: ['pipe', 'pipe', 'ignore'], signal: init.signal, env });
28
+ const chunks = []; let bytes = 0;
29
+ const timeout = setTimeout(() => child.kill(), 65000); timeout.unref?.();
30
+ child.on('error', () => { clearTimeout(timeout); reject(new Error('Encrypted bridge request failed')); });
31
+ child.stdout.on('data', data => { bytes += data.length; if (bytes > 1048576) child.kill(); else chunks.push(data); });
32
+ child.on('close', code => {
33
+ clearTimeout(timeout);
34
+ if (code !== 0 || bytes > 1048576) return reject(new Error('Encrypted bridge request failed'));
35
+ try { const response = JSON.parse(Buffer.concat(chunks).toString('utf8'));
36
+ resolve(new Response(JSON.stringify(response.body), { status: response.status })); }
37
+ catch { reject(new Error('Encrypted bridge response failed')); }
38
+ });
39
+ child.stdin.on('error', () => {}); child.stdin.end(JSON.stringify(request));
40
+ });
41
+ }
42
+
19
43
  function endpoints(env, pointer) {
20
44
  const candidates = [];
21
45
  const envPort = Number.parseInt(env.FTOWN_HOOK_PORT ?? '', 10);
@@ -82,6 +106,10 @@ export function registerFtownOpencodePlugin(client, options = {}) {
82
106
 
83
107
  async function request(path, init = {}) {
84
108
  const pointer = await readBridgePointer();
109
+ if (path.startsWith('/api/')) {
110
+ try { return await (options.proxy ?? ((p, i) => secureProxy(env, pointer, p, i)))(path, init); }
111
+ catch { return null; } // Delivery may be unknown: never retry a mutation through another endpoint.
112
+ }
85
113
  if (closed) return null;
86
114
  let lastResponse = null;
87
115
  for (const endpoint of endpoints(env, pointer)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftown",
3
- "version": "0.19.52",
3
+ "version": "0.19.54",
4
4
  "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -49,6 +49,8 @@
49
49
  "directory": "bridge"
50
50
  },
51
51
  "dependencies": {
52
+ "@hpke/common": "1.10.1",
53
+ "@hpke/core": "1.9.0",
52
54
  "@modelcontextprotocol/sdk": "^1.32.1",
53
55
  "@xterm/addon-serialize": "^0.14.0",
54
56
  "@xterm/headless": "^6.0.0",
@@ -74,5 +76,5 @@
74
76
  "tsx": "^4.19.0",
75
77
  "typescript": "^5.7.0"
76
78
  },
77
- "gitHead": "0e3e8c7e8150ec46770a297c414e3d2d6c11927b"
79
+ "gitHead": "d2701bcfae7ff9ccc9ee7c910d6dec2d33d262c1"
78
80
  }
@@ -1,21 +1,45 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
 
5
6
  async function defaultReadBridgePointer() {
6
7
  try {
7
- const raw = await readFile(join(homedir(), '.ftown', 'bridge.json'), 'utf8');
8
+ const raw = await readFile(process.env.FTOWN_BRIDGE_POINTER || (process.env.FTOWN_DATA_DIR ? join(process.env.FTOWN_DATA_DIR, 'bridge.json') : join(homedir(), '.ftown', 'bridge.json')), 'utf8');
8
9
  const parsed = JSON.parse(raw);
9
10
  if (!parsed || typeof parsed.port !== 'number') return null;
10
- return {
11
- port: parsed.port,
12
- token: typeof parsed.token === 'string' ? parsed.token : undefined,
13
- };
11
+ return parsed;
14
12
  } catch {
15
13
  return null;
16
14
  }
17
15
  }
18
16
 
17
+
18
+ async function secureProxy(env, pointer, path, init = {}) {
19
+ const cli = pointer?.harnessCli || join(homedir(), '.ftown', 'cli', 'dist', 'ftown-cli.js');
20
+ const args = [cli, 'security'];
21
+ const directory = env.FTOWN_DATA_DIR || pointer?.dataDir;
22
+ if (directory) args.push('--data-dir', directory);
23
+ args.push('proxy');
24
+ const request = { method: init.method || 'GET', path, timeoutMs: 60000 };
25
+ if (init.body !== undefined) request.body = typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(process.execPath, args, { stdio: ['pipe', 'pipe', 'ignore'], signal: init.signal, env });
28
+ const chunks = []; let bytes = 0;
29
+ const timeout = setTimeout(() => child.kill(), 65000); timeout.unref?.();
30
+ child.on('error', () => { clearTimeout(timeout); reject(new Error('Encrypted bridge request failed')); });
31
+ child.stdout.on('data', data => { bytes += data.length; if (bytes > 1048576) child.kill(); else chunks.push(data); });
32
+ child.on('close', code => {
33
+ clearTimeout(timeout);
34
+ if (code !== 0 || bytes > 1048576) return reject(new Error('Encrypted bridge request failed'));
35
+ try { const response = JSON.parse(Buffer.concat(chunks).toString('utf8'));
36
+ resolve(new Response(JSON.stringify(response.body), { status: response.status })); }
37
+ catch { reject(new Error('Encrypted bridge response failed')); }
38
+ });
39
+ child.stdin.on('error', () => {}); child.stdin.end(JSON.stringify(request));
40
+ });
41
+ }
42
+
19
43
  function endpoints(env, pointer) {
20
44
  const candidates = [];
21
45
  const envPort = Number.parseInt(env.FTOWN_HOOK_PORT ?? '', 10);
@@ -156,6 +180,10 @@ export function registerFtownPiExtension(pi, options = {}) {
156
180
 
157
181
  async function request(path, init = {}) {
158
182
  const pointer = await readBridgePointer();
183
+ if (path.startsWith('/api/')) {
184
+ try { return await (options.proxy ?? ((p, i) => secureProxy(env, pointer, p, i)))(path, init); }
185
+ catch { return null; } // Delivery may be unknown: never retry a mutation through another endpoint.
186
+ }
159
187
  let lastResponse = null;
160
188
  for (const endpoint of endpoints(env, pointer)) {
161
189
  try {