openfused 0.1.2 → 0.2.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/dist/cli.js CHANGED
@@ -24,8 +24,10 @@ program
24
24
  const id = nanoid(12);
25
25
  await store.init(opts.name, id);
26
26
  console.log(`Initialized context store: ${store.root}`);
27
+ const config = await store.readConfig();
27
28
  console.log(` Agent ID: ${id}`);
28
29
  console.log(` Name: ${opts.name}`);
30
+ console.log(` Signing keys: generated (.keys/)`);
29
31
  console.log(`\nStructure:`);
30
32
  console.log(` CONTEXT.md — working memory (edit this)`);
31
33
  console.log(` SOUL.md — agent identity & rules`);
@@ -98,6 +100,7 @@ inbox
98
100
  .command("list")
99
101
  .description("List inbox messages")
100
102
  .option("-d, --dir <path>", "Context store directory", ".")
103
+ .option("--raw", "Show raw content instead of wrapped")
101
104
  .action(async (opts) => {
102
105
  const store = new ContextStore(resolve(opts.dir));
103
106
  const messages = await store.readInbox();
@@ -106,8 +109,9 @@ inbox
106
109
  return;
107
110
  }
108
111
  for (const msg of messages) {
109
- console.log(`\n--- From: ${msg.from} | ${msg.time} ---`);
110
- console.log(msg.content);
112
+ const badge = msg.verified ? "[VERIFIED]" : "[UNVERIFIED]";
113
+ console.log(`\n--- ${badge} From: ${msg.from} | ${msg.time} ---`);
114
+ console.log(opts.raw ? msg.content : msg.wrappedContent);
111
115
  }
112
116
  });
113
117
  inbox
@@ -203,4 +207,38 @@ peer
203
207
  await store.writeConfig(config);
204
208
  console.log(`Removed peer: ${id}`);
205
209
  });
210
+ peer
211
+ .command("trust <publicKeyFile>")
212
+ .description("Trust a peer's public key (messages from them will show as verified)")
213
+ .option("-d, --dir <path>", "Context store directory", ".")
214
+ .action(async (publicKeyFile, opts) => {
215
+ const store = new ContextStore(resolve(opts.dir));
216
+ const config = await store.readConfig();
217
+ const { readFile } = await import("node:fs/promises");
218
+ const pubKey = (await readFile(resolve(publicKeyFile), "utf-8")).trim();
219
+ if (!config.trustedKeys)
220
+ config.trustedKeys = [];
221
+ if (config.trustedKeys.includes(pubKey)) {
222
+ console.log("Key already trusted.");
223
+ return;
224
+ }
225
+ config.trustedKeys.push(pubKey);
226
+ await store.writeConfig(config);
227
+ console.log("Key trusted. Messages signed with this key will show as [VERIFIED].");
228
+ });
229
+ // --- key ---
230
+ program
231
+ .command("key")
232
+ .description("Show this agent's public key (share with peers so they can trust you)")
233
+ .option("-d, --dir <path>", "Context store directory", ".")
234
+ .action(async (opts) => {
235
+ const store = new ContextStore(resolve(opts.dir));
236
+ const config = await store.readConfig();
237
+ if (config.publicKey) {
238
+ console.log(config.publicKey);
239
+ }
240
+ else {
241
+ console.error("No keys found. Run `openfuse init` first.");
242
+ }
243
+ });
206
244
  program.parse();
@@ -0,0 +1,17 @@
1
+ export interface SignedMessage {
2
+ from: string;
3
+ timestamp: string;
4
+ message: string;
5
+ signature: string;
6
+ publicKey: string;
7
+ }
8
+ export declare function generateKeys(storeRoot: string): Promise<{
9
+ publicKey: string;
10
+ privateKey: string;
11
+ }>;
12
+ export declare function hasKeys(storeRoot: string): Promise<boolean>;
13
+ export declare function signMessage(storeRoot: string, from: string, message: string): Promise<SignedMessage>;
14
+ export declare function verifyMessage(signed: SignedMessage): boolean;
15
+ export declare function wrapExternalMessage(signed: SignedMessage, verified: boolean): string;
16
+ export declare function serializeSignedMessage(signed: SignedMessage): string;
17
+ export declare function deserializeSignedMessage(raw: string): SignedMessage | null;
package/dist/crypto.js ADDED
@@ -0,0 +1,65 @@
1
+ import { generateKeyPairSync, sign, verify, createPrivateKey, createPublicKey } from "node:crypto";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ const KEY_DIR = ".keys";
6
+ export async function generateKeys(storeRoot) {
7
+ const keyDir = join(storeRoot, KEY_DIR);
8
+ await mkdir(keyDir, { recursive: true });
9
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
10
+ publicKeyEncoding: { type: "spki", format: "pem" },
11
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
12
+ });
13
+ await writeFile(join(keyDir, "public.pem"), publicKey, { mode: 0o644 });
14
+ await writeFile(join(keyDir, "private.pem"), privateKey, { mode: 0o600 });
15
+ return { publicKey, privateKey };
16
+ }
17
+ export async function hasKeys(storeRoot) {
18
+ return existsSync(join(storeRoot, KEY_DIR, "private.pem"));
19
+ }
20
+ async function loadPrivateKey(storeRoot) {
21
+ const pem = await readFile(join(storeRoot, KEY_DIR, "private.pem"), "utf-8");
22
+ return createPrivateKey(pem);
23
+ }
24
+ async function loadPublicKey(storeRoot) {
25
+ return readFile(join(storeRoot, KEY_DIR, "public.pem"), "utf-8");
26
+ }
27
+ export async function signMessage(storeRoot, from, message) {
28
+ const privateKey = await loadPrivateKey(storeRoot);
29
+ const publicKey = await loadPublicKey(storeRoot);
30
+ const timestamp = new Date().toISOString();
31
+ const payload = Buffer.from(`${from}\n${timestamp}\n${message}`);
32
+ const signature = sign(null, payload, privateKey).toString("base64");
33
+ return { from, timestamp, message, signature, publicKey };
34
+ }
35
+ export function verifyMessage(signed) {
36
+ try {
37
+ const payload = Buffer.from(`${signed.from}\n${signed.timestamp}\n${signed.message}`);
38
+ const pubKey = createPublicKey(signed.publicKey);
39
+ return verify(null, payload, pubKey, Buffer.from(signed.signature, "base64"));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ // Wrap a message in security tags for the LLM
46
+ export function wrapExternalMessage(signed, verified) {
47
+ const status = verified ? "verified" : "UNVERIFIED";
48
+ return `<external_message from="${signed.from}" verified="${verified}" time="${signed.timestamp}" status="${status}">
49
+ ${signed.message}
50
+ </external_message>`;
51
+ }
52
+ // Format for writing to inbox files
53
+ export function serializeSignedMessage(signed) {
54
+ return JSON.stringify(signed, null, 2);
55
+ }
56
+ export function deserializeSignedMessage(raw) {
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed.from && parsed.message && parsed.signature && parsed.publicKey) {
60
+ return parsed;
61
+ }
62
+ }
63
+ catch { }
64
+ return null;
65
+ }
package/dist/store.d.ts CHANGED
@@ -2,7 +2,9 @@ export interface MeshConfig {
2
2
  id: string;
3
3
  name: string;
4
4
  created: string;
5
+ publicKey?: string;
5
6
  peers: PeerConfig[];
7
+ trustedKeys?: string[];
6
8
  }
7
9
  export interface PeerConfig {
8
10
  id: string;
@@ -27,8 +29,10 @@ export declare class ContextStore {
27
29
  readInbox(): Promise<Array<{
28
30
  file: string;
29
31
  content: string;
32
+ wrappedContent: string;
30
33
  from: string;
31
34
  time: string;
35
+ verified: boolean;
32
36
  }>>;
33
37
  listShared(): Promise<string[]>;
34
38
  share(filename: string, content: string): Promise<void>;
package/dist/store.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
- const STORE_DIRS = ["history", "knowledge", "inbox", "shared"];
4
+ import { generateKeys, signMessage, verifyMessage, deserializeSignedMessage, serializeSignedMessage, wrapExternalMessage } from "./crypto.js";
5
+ const STORE_DIRS = ["history", "knowledge", "inbox", "outbox", "shared"];
5
6
  export class ContextStore {
6
7
  root;
7
8
  constructor(root) {
@@ -29,12 +30,16 @@ export class ContextStore {
29
30
  await writeFile(destPath, content);
30
31
  }
31
32
  }
33
+ // Generate signing keypair
34
+ const keys = await generateKeys(this.root);
32
35
  // Write mesh config
33
36
  const config = {
34
37
  id,
35
38
  name,
36
39
  created: new Date().toISOString(),
40
+ publicKey: keys.publicKey,
37
41
  peers: [],
42
+ trustedKeys: [],
38
43
  };
39
44
  await this.writeConfig(config);
40
45
  }
@@ -57,26 +62,52 @@ export class ContextStore {
57
62
  async writeSoul(content) {
58
63
  await writeFile(join(this.root, "SOUL.md"), content);
59
64
  }
60
- // --- Inbox ---
65
+ // --- Inbox (signed messages) ---
61
66
  async sendInbox(peerId, message) {
62
- const inboxDir = join(this.root, "inbox");
67
+ const config = await this.readConfig();
68
+ const signed = await signMessage(this.root, config.id, message);
63
69
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
64
- const filename = `${timestamp}_${peerId}.md`;
65
- await writeFile(join(inboxDir, filename), message);
70
+ const filename = `${timestamp}_${peerId}.json`;
71
+ // Write to inbox (for the recipient) and outbox (our copy)
72
+ await writeFile(join(this.root, "inbox", filename), serializeSignedMessage(signed));
73
+ await writeFile(join(this.root, "outbox", filename), serializeSignedMessage(signed));
66
74
  }
67
75
  async readInbox() {
68
76
  const inboxDir = join(this.root, "inbox");
69
77
  if (!existsSync(inboxDir))
70
78
  return [];
79
+ const config = await this.readConfig();
71
80
  const files = await readdir(inboxDir);
72
81
  const messages = [];
73
- for (const file of files.filter((f) => f.endsWith(".md"))) {
74
- const content = await readFile(join(inboxDir, file), "utf-8");
75
- // Parse filename: 2026-03-20T01-30-00-000Z_peer-id.md
76
- const parts = file.replace(".md", "").split("_");
77
- const from = parts.slice(1).join("_");
78
- const time = parts[0].replace(/-/g, (m, i) => (i < 10 ? "-" : i < 13 ? "T" : i < 19 ? ":" : ".")).replace("Z", "");
79
- messages.push({ file, content, from, time });
82
+ for (const file of files.filter((f) => f.endsWith(".json") || f.endsWith(".md"))) {
83
+ const raw = await readFile(join(inboxDir, file), "utf-8");
84
+ // Try parsing as signed message
85
+ const signed = deserializeSignedMessage(raw);
86
+ if (signed) {
87
+ const verified = verifyMessage(signed);
88
+ const trusted = config.trustedKeys?.some(k => k.trim() === signed.publicKey.trim()) ?? false;
89
+ messages.push({
90
+ file,
91
+ content: signed.message,
92
+ wrappedContent: wrapExternalMessage(signed, verified && trusted),
93
+ from: signed.from,
94
+ time: signed.timestamp,
95
+ verified: verified && trusted,
96
+ });
97
+ }
98
+ else {
99
+ // Unsigned message — mark as unverified
100
+ const parts = file.replace(/\.(md|json)$/, "").split("_");
101
+ const from = parts.slice(1).join("_");
102
+ messages.push({
103
+ file,
104
+ content: raw,
105
+ wrappedContent: wrapExternalMessage({ from, timestamp: parts[0], message: raw, signature: "", publicKey: "" }, false),
106
+ from,
107
+ time: parts[0],
108
+ verified: false,
109
+ });
110
+ }
80
111
  }
81
112
  return messages.sort((a, b) => a.time.localeCompare(b.time));
82
113
  }
package/dist/watch.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export type InboxCallback = (from: string, message: string, file: string) => void;
1
+ export type InboxCallback = (from: string, message: string, file: string, verified: boolean) => void;
2
2
  export declare function watchInbox(storeRoot: string, callback: InboxCallback): () => void;
3
3
  export declare function watchContext(storeRoot: string, callback: (content: string) => void): () => void;
package/dist/watch.js CHANGED
@@ -1,36 +1,36 @@
1
1
  import { watch } from "chokidar";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join, basename } from "node:path";
4
+ import { deserializeSignedMessage, verifyMessage, wrapExternalMessage } from "./crypto.js";
4
5
  export function watchInbox(storeRoot, callback) {
5
6
  const inboxDir = join(storeRoot, "inbox");
6
- const watcher = watch(inboxDir, {
7
- ignoreInitial: true,
8
- awaitWriteFinish: { stabilityThreshold: 500 },
9
- });
10
- watcher.on("add", async (filePath) => {
11
- if (!filePath.endsWith(".md"))
12
- return;
13
- try {
14
- const content = await readFile(filePath, "utf-8");
15
- const filename = basename(filePath, ".md");
16
- const parts = filename.split("_");
17
- const from = parts.slice(1).join("_");
18
- callback(from, content, filePath);
19
- }
20
- catch { }
21
- });
22
- watcher.on("change", async (filePath) => {
23
- if (!filePath.endsWith(".md"))
7
+ const handleFile = async (filePath) => {
8
+ if (!filePath.endsWith(".json") && !filePath.endsWith(".md"))
24
9
  return;
25
10
  try {
26
- const content = await readFile(filePath, "utf-8");
27
- const filename = basename(filePath, ".md");
11
+ const raw = await readFile(filePath, "utf-8");
12
+ // Try signed message first
13
+ const signed = deserializeSignedMessage(raw);
14
+ if (signed) {
15
+ const verified = verifyMessage(signed);
16
+ callback(signed.from, wrapExternalMessage(signed, verified), filePath, verified);
17
+ return;
18
+ }
19
+ // Unsigned fallback — always unverified
20
+ const filename = basename(filePath).replace(/\.(md|json)$/, "");
28
21
  const parts = filename.split("_");
29
22
  const from = parts.slice(1).join("_");
30
- callback(from, content, filePath);
23
+ const wrapped = `<external_message from="${from}" verified="false" status="UNVERIFIED">\n${raw}\n</external_message>`;
24
+ callback(from, wrapped, filePath, false);
31
25
  }
32
26
  catch { }
27
+ };
28
+ const watcher = watch(inboxDir, {
29
+ ignoreInitial: true,
30
+ awaitWriteFinish: { stabilityThreshold: 500 },
33
31
  });
32
+ watcher.on("add", handleFile);
33
+ watcher.on("change", handleFile);
34
34
  return () => watcher.close();
35
35
  }
36
36
  export function watchContext(storeRoot, callback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Persistent, shareable, portable context for AI agents. Decentralized mesh via files.",
5
5
  "license": "MIT",
6
6
  "type": "module",