openfused 0.3.9 → 0.3.11

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
@@ -241,10 +241,20 @@ The daemon has two modes:
241
241
  # Full mode — serves everything to trusted LAN peers
242
242
  openfused serve --store ./my-context --port 9781
243
243
 
244
- # Public mode — only PROFILE.md + inbox (for WAN/tunnels)
244
+ # Public mode — PROFILE.md + inbox + outbox pickup (for WAN/tunnels)
245
245
  openfused serve --store ./my-context --port 9781 --public
246
246
  ```
247
247
 
248
+ Public mode endpoints:
249
+
250
+ | Endpoint | Method | Purpose |
251
+ |----------|--------|---------|
252
+ | `/` | GET | Service info |
253
+ | `/profile` | GET | Your PROFILE.md (public address card) |
254
+ | `/config` | GET | Your public keys (JSON) |
255
+ | `/inbox` | POST | Accept signed messages (rejects invalid signatures) |
256
+ | `/outbox/{name}` | GET | Pickup replies addressed to `{name}` (encrypted) |
257
+
248
258
  ## File Watching
249
259
 
250
260
  `openfuse watch` combines three things:
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import * as registry from "./registry.js";
8
8
  import { fingerprint } from "./crypto.js";
9
9
  import { resolve, join } from "node:path";
10
10
  import { readFile } from "node:fs/promises";
11
- const VERSION = "0.3.9";
11
+ const VERSION = "0.3.11";
12
12
  const program = new Command();
13
13
  program
14
14
  .name("openfuse")
@@ -544,8 +544,13 @@ program
544
544
  const reg = registry.resolveRegistry(opts.registry);
545
545
  try {
546
546
  const manifest = await registry.discover(name, reg);
547
- const config = await store.readConfig();
548
- // Auto-import key (untrusted)
547
+ // Auto-import key (untrusted) + add as peer so future `openfuse sync` can
548
+ // deliver replies and pull context. Key is deliberately NOT trusted — the user
549
+ // must explicitly `openfuse key trust <name>` after out-of-band verification.
550
+ // NOTE: manifest data comes from the registry and is attacker-controlled.
551
+ // The endpoint URL is stored as-is; a malicious entry could point at an internal
552
+ // service. Sync will pull from it — consider validating URL scheme/host.
553
+ let config = await store.readConfig();
549
554
  if (!config.keyring.some((e) => e.signingKey === manifest.publicKey)) {
550
555
  config.keyring.push({
551
556
  name: manifest.name,
@@ -556,10 +561,16 @@ program
556
561
  trusted: false,
557
562
  added: new Date().toISOString(),
558
563
  });
559
- await store.writeConfig(config);
560
- console.log(`Imported key for ${manifest.name} from registry [untrusted]`);
561
- console.log(` Run \`openfuse key trust ${manifest.name}\` to trust`);
562
564
  }
565
+ if (manifest.endpoint && !config.peers.some((p) => p.name === manifest.name)) {
566
+ config.peers.push({
567
+ id: (await import("nanoid")).nanoid(12),
568
+ name: manifest.name,
569
+ url: manifest.endpoint,
570
+ access: "read",
571
+ });
572
+ }
573
+ await store.writeConfig(config);
563
574
  const filename = await store.sendInbox(name, message);
564
575
  // Try direct HTTP delivery if endpoint is http(s)
565
576
  if (manifest.endpoint.startsWith("http")) {
package/dist/mcp.js CHANGED
@@ -23,7 +23,7 @@ const storeDir = process.env.OPENFUSE_DIR || process.argv[3] || ".";
23
23
  const store = new ContextStore(resolve(storeDir));
24
24
  const server = new McpServer({
25
25
  name: "openfuse",
26
- version: "0.3.9",
26
+ version: "0.3.11",
27
27
  });
28
28
  // --- Context ---
29
29
  server.tool("context_read", "Read the agent's CONTEXT.md (working memory)", async () => {
package/dist/sync.js CHANGED
@@ -153,6 +153,35 @@ async function syncHttp(store, peer, baseUrl, peerDir) {
153
153
  errors.push(`${dir}/: ${e.message}`);
154
154
  }
155
155
  }
156
+ // Pull peer's outbox for messages addressed to us (HTTP version).
157
+ // The peer's daemon filters to _to-{myName}.json, but the JSON body is untrusted —
158
+ // a malicious peer controls every field. We must sanitize before using any value
159
+ // in a filename to prevent path traversal (e.g. from="../../.keys/evil").
160
+ const config = await store.readConfig();
161
+ const myName = config.name;
162
+ const inboxDir = join(store.root, "inbox");
163
+ await mkdir(inboxDir, { recursive: true });
164
+ try {
165
+ const resp = await fetch(`${baseUrl}/outbox/${myName}`);
166
+ if (resp.ok) {
167
+ const messages = (await resp.json());
168
+ for (const msg of messages) {
169
+ const ts = (msg.timestamp || new Date().toISOString()).replace(/[:.]/g, "-");
170
+ const from = msg.from || "unknown";
171
+ // SECURITY: sanitize remote-controlled values before constructing local filenames.
172
+ // Without this, a malicious "from" like "../../.keys/x" could write outside inbox/.
173
+ const safeFrom = from.replace(/[^a-zA-Z0-9\-_]/g, "");
174
+ const safeTs = ts.replace(/[^a-zA-Z0-9\-_]/g, "");
175
+ const fname = `${safeTs}_from-${safeFrom}_to-${myName}.json`;
176
+ const dest = join(inboxDir, fname);
177
+ if (!existsSync(dest)) {
178
+ await writeFile(dest, JSON.stringify(msg, null, 2));
179
+ pulled.push(`outbox→${fname}`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ catch { }
156
185
  // Push outbox → peer inbox
157
186
  const outboxDir = join(store.root, "outbox");
158
187
  if (existsSync(outboxDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "The file protocol for AI agent context. Encrypted, signed, peer-to-peer.",
5
5
  "license": "MIT",
6
6
  "type": "module",