meshingly-mcp 0.1.0 → 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.
@@ -1,95 +1,98 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import { generateSigningKeys, } from "./crypto.js";
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
- const SKILL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
41
-
42
- export function loadConfig(env = process.env) {
43
- const file = join(SKILL_DIR, "meshingly.config.json");
44
- const fromFile = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
45
- const cryptKey = env.MESHINGLY_KEY ?? fromFile.cryptKey;
46
- const serverUrl = env.MESHINGLY_URL ?? fromFile.serverUrl;
47
- if (!cryptKey || !serverUrl) {
48
- throw new Error("Missing cryptKey/serverUrl: re-download the skill from the Meshingly web app or set MESHINGLY_KEY and MESHINGLY_URL");
49
- }
50
- return {
51
- cryptKey,
52
- serverUrl,
53
- projectName: fromFile.projectName,
54
- pollIntervalSeconds: Number(env.MESHINGLY_POLL_SECONDS ?? fromFile.pollIntervalSeconds ?? 60),
55
- longPollSeconds: Number(env.MESHINGLY_LONGPOLL_SECONDS ?? fromFile.longPollSeconds ?? 0),
56
- };
57
- }
58
-
59
- const stateDir = (cwd ) => join(cwd, ".meshingly");
60
- const stateFile = (cwd , role ) => join(stateDir(cwd), `${role.replace(/[^\w-]/g, "_")}.json`);
61
-
62
- /** Loads the session for `role`, assigning a new Session ID on first use. */
63
- export function loadOrCreateSession(role , cwd = process.cwd()) {
64
- const file = stateFile(cwd, role);
65
- if (existsSync(file)) {
66
- const session = JSON.parse(readFileSync(file, "utf8")) ;
67
- // State files written before signing existed get keys on first load.
68
- if (!session.signingKeys || !session.peers || !session.peerInfo) {
69
- session.signingKeys ??= generateSigningKeys();
70
- session.peers ??= {};
71
- session.peerInfo ??= {};
72
- saveSession(session, cwd);
73
- }
74
- return session;
75
- }
76
- const session = {
77
- sessionId: randomUUID(),
78
- role,
79
- cursor: 0,
80
- createdAt: new Date().toISOString(),
81
- signingKeys: generateSigningKeys(),
82
- peers: {},
83
- peerInfo: {},
84
- };
85
- saveSession(session, cwd);
86
- return session;
87
- }
88
-
89
- export function saveSession(session , cwd = process.cwd()) {
90
- mkdirSync(stateDir(cwd), { recursive: true });
91
- const file = stateFile(cwd, session.role);
92
- const tmp = `${file}.tmp`;
93
- writeFileSync(tmp, JSON.stringify(session, null, 2), { mode: 0o600 });
94
- renameSync(tmp, file);
95
- }
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { generateSigningKeys, } from "./crypto.js";
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ const SKILL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
44
+
45
+ export function loadConfig(env = process.env) {
46
+ const file = join(SKILL_DIR, "meshingly.config.json");
47
+ const fromFile = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
48
+ const cryptKey = env.MESHINGLY_KEY ?? fromFile.cryptKey;
49
+ const serverUrl = env.MESHINGLY_URL ?? fromFile.serverUrl;
50
+ if (!cryptKey || !serverUrl) {
51
+ throw new Error("Missing cryptKey/serverUrl: re-download the skill from the Meshingly web app or set MESHINGLY_KEY and MESHINGLY_URL");
52
+ }
53
+ return {
54
+ cryptKey,
55
+ serverUrl,
56
+ projectName: fromFile.projectName,
57
+ pollIntervalSeconds: Number(env.MESHINGLY_POLL_SECONDS ?? fromFile.pollIntervalSeconds ?? 60),
58
+ longPollSeconds: Number(env.MESHINGLY_LONGPOLL_SECONDS ?? fromFile.longPollSeconds ?? 0),
59
+ };
60
+ }
61
+
62
+ const stateDir = (cwd ) => join(cwd, ".meshingly");
63
+ const stateFile = (cwd , role ) => join(stateDir(cwd), `${role.replace(/[^\w-]/g, "_")}.json`);
64
+
65
+ /** Loads the session for `role`, assigning a new Session ID on first use. */
66
+ export function loadOrCreateSession(role , cwd = process.cwd()) {
67
+ const file = stateFile(cwd, role);
68
+ if (existsSync(file)) {
69
+ const session = JSON.parse(readFileSync(file, "utf8")) ;
70
+ // State files written before signing existed get keys on first load.
71
+ if (!session.signingKeys || !session.peers || !session.peerInfo) {
72
+ session.signingKeys ??= generateSigningKeys();
73
+ session.peers ??= {};
74
+ session.peerInfo ??= {};
75
+ saveSession(session, cwd);
76
+ }
77
+ return session;
78
+ }
79
+ const session = {
80
+ sessionId: randomUUID(),
81
+ role,
82
+ cursor: 0,
83
+ createdAt: new Date().toISOString(),
84
+ signingKeys: generateSigningKeys(),
85
+ peers: {},
86
+ peerInfo: {},
87
+ };
88
+ saveSession(session, cwd);
89
+ return session;
90
+ }
91
+
92
+ export function saveSession(session , cwd = process.cwd()) {
93
+ mkdirSync(stateDir(cwd), { recursive: true });
94
+ const file = stateFile(cwd, session.role);
95
+ const tmp = `${file}.tmp`;
96
+ writeFileSync(tmp, JSON.stringify(session, null, 2), { mode: 0o600 });
97
+ renameSync(tmp, file);
98
+ }
@@ -1,143 +1,143 @@
1
- import {
2
- createCipheriv,
3
- createDecipheriv,
4
- createPrivateKey,
5
- createPublicKey,
6
- generateKeyPairSync,
7
- hkdfSync,
8
- randomBytes,
9
- sign as edSign,
10
- verify as edVerify,
11
- } from "node:crypto";
12
-
13
- const KEY_BYTES = 32;
14
- const SALT_BYTES = 16;
15
- const IV_BYTES = 12;
16
- const TAG_BYTES = 16;
17
-
18
- const INFO_CHANNEL = "meshingly/v1/channel";
19
- const INFO_AUTH = "meshingly/v1/auth";
20
- const INFO_MESSAGE_KEY = "meshingly/v1/message-key";
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
- export class CryptoError extends Error {
38
- constructor(message ) {
39
- super(message);
40
- this.name = "CryptoError";
41
- }
42
- }
43
-
44
- const toB64u = (buf ) => Buffer.from(buf).toString("base64url");
45
- const fromB64u = (s ) => Buffer.from(s, "base64url");
46
-
47
- export function generateMasterKey() {
48
- return toB64u(randomBytes(KEY_BYTES));
49
- }
50
-
51
- export function parseMasterKey(encoded ) {
52
- const key = fromB64u(encoded);
53
- if (key.length !== KEY_BYTES) {
54
- throw new CryptoError(`Crypt key must decode to ${KEY_BYTES} bytes`);
55
- }
56
- return key;
57
- }
58
-
59
- function hkdf(master , salt , info , length ) {
60
- return Buffer.from(hkdfSync("sha256", master, salt, info, length));
61
- }
62
-
63
- /** Derives routing id and auth token so the master key never leaves the machine. */
64
- export function deriveChannel(masterKey ) {
65
- const master = parseMasterKey(masterKey);
66
- const empty = Buffer.alloc(0);
67
- return {
68
- channelId: toB64u(hkdf(master, empty, INFO_CHANNEL, 16)),
69
- authToken: toB64u(hkdf(master, empty, INFO_AUTH, 32)),
70
- };
71
- }
72
-
73
- /**
74
- * AES-256-GCM with a fresh subkey per message (HKDF over a random salt),
75
- * so a random 96-bit nonce collision can never repeat a (key, nonce) pair.
76
- * `aad` binds the ciphertext to its routing metadata.
77
- */
78
- export function encrypt(masterKey , plaintext , aad ) {
79
- const master = parseMasterKey(masterKey);
80
- const salt = randomBytes(SALT_BYTES);
81
- const iv = randomBytes(IV_BYTES);
82
- const cipher = createCipheriv("aes-256-gcm", hkdf(master, salt, INFO_MESSAGE_KEY, KEY_BYTES), iv, {
83
- authTagLength: TAG_BYTES,
84
- });
85
- cipher.setAAD(Buffer.from(aad, "utf8"));
86
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
87
- return { v: 1, salt: toB64u(salt), iv: toB64u(iv), ct: toB64u(ct), tag: toB64u(cipher.getAuthTag()) };
88
- }
89
-
90
- export function decrypt(masterKey , envelope , aad ) {
91
- if (envelope?.v !== 1) throw new CryptoError("Unsupported envelope version");
92
- const master = parseMasterKey(masterKey);
93
- const salt = fromB64u(envelope.salt);
94
- const iv = fromB64u(envelope.iv);
95
- const tag = fromB64u(envelope.tag);
96
- if (salt.length !== SALT_BYTES || iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
97
- throw new CryptoError("Malformed envelope");
98
- }
99
- try {
100
- const decipher = createDecipheriv("aes-256-gcm", hkdf(master, salt, INFO_MESSAGE_KEY, KEY_BYTES), iv, {
101
- authTagLength: TAG_BYTES,
102
- });
103
- decipher.setAAD(Buffer.from(aad, "utf8"));
104
- decipher.setAuthTag(tag);
105
- return Buffer.concat([decipher.update(fromB64u(envelope.ct)), decipher.final()]).toString("utf8");
106
- } catch {
107
- throw new CryptoError("Decryption failed (wrong key, tampered data or mismatched metadata)");
108
- }
109
- }
110
-
111
- export function buildAad(channelId , messageId , senderSessionId ) {
112
- return `meshingly/v1|${channelId}|${messageId}|${senderSessionId}`;
113
- }
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
- export function generateSigningKeys() {
123
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
124
- return {
125
- publicKey: toB64u(publicKey.export({ type: "spki", format: "der" })),
126
- privateKey: toB64u(privateKey.export({ type: "pkcs8", format: "der" })),
127
- };
128
- }
129
-
130
- export function signMessage(privateKey , data ) {
131
- const key = createPrivateKey({ key: fromB64u(privateKey), format: "der", type: "pkcs8" });
132
- return toB64u(edSign(null, Buffer.from(data, "utf8"), key));
133
- }
134
-
135
- /** Returns false (never throws) for malformed keys or signatures. */
136
- export function verifyMessage(publicKey , data , signature ) {
137
- try {
138
- const key = createPublicKey({ key: fromB64u(publicKey), format: "der", type: "spki" });
139
- return edVerify(null, Buffer.from(data, "utf8"), key, fromB64u(signature));
140
- } catch {
141
- return false;
142
- }
143
- }
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createPrivateKey,
5
+ createPublicKey,
6
+ generateKeyPairSync,
7
+ hkdfSync,
8
+ randomBytes,
9
+ sign as edSign,
10
+ verify as edVerify,
11
+ } from "node:crypto";
12
+
13
+ const KEY_BYTES = 32;
14
+ const SALT_BYTES = 16;
15
+ const IV_BYTES = 12;
16
+ const TAG_BYTES = 16;
17
+
18
+ const INFO_CHANNEL = "meshingly/v1/channel";
19
+ const INFO_AUTH = "meshingly/v1/auth";
20
+ const INFO_MESSAGE_KEY = "meshingly/v1/message-key";
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+ export class CryptoError extends Error {
38
+ constructor(message ) {
39
+ super(message);
40
+ this.name = "CryptoError";
41
+ }
42
+ }
43
+
44
+ const toB64u = (buf ) => Buffer.from(buf).toString("base64url");
45
+ const fromB64u = (s ) => Buffer.from(s, "base64url");
46
+
47
+ export function generateMasterKey() {
48
+ return toB64u(randomBytes(KEY_BYTES));
49
+ }
50
+
51
+ export function parseMasterKey(encoded ) {
52
+ const key = fromB64u(encoded);
53
+ if (key.length !== KEY_BYTES) {
54
+ throw new CryptoError(`Crypt key must decode to ${KEY_BYTES} bytes`);
55
+ }
56
+ return key;
57
+ }
58
+
59
+ function hkdf(master , salt , info , length ) {
60
+ return Buffer.from(hkdfSync("sha256", master, salt, info, length));
61
+ }
62
+
63
+ /** Derives routing id and auth token so the master key never leaves the machine. */
64
+ export function deriveChannel(masterKey ) {
65
+ const master = parseMasterKey(masterKey);
66
+ const empty = Buffer.alloc(0);
67
+ return {
68
+ channelId: toB64u(hkdf(master, empty, INFO_CHANNEL, 16)),
69
+ authToken: toB64u(hkdf(master, empty, INFO_AUTH, 32)),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * AES-256-GCM with a fresh subkey per message (HKDF over a random salt),
75
+ * so a random 96-bit nonce collision can never repeat a (key, nonce) pair.
76
+ * `aad` binds the ciphertext to its routing metadata.
77
+ */
78
+ export function encrypt(masterKey , plaintext , aad ) {
79
+ const master = parseMasterKey(masterKey);
80
+ const salt = randomBytes(SALT_BYTES);
81
+ const iv = randomBytes(IV_BYTES);
82
+ const cipher = createCipheriv("aes-256-gcm", hkdf(master, salt, INFO_MESSAGE_KEY, KEY_BYTES), iv, {
83
+ authTagLength: TAG_BYTES,
84
+ });
85
+ cipher.setAAD(Buffer.from(aad, "utf8"));
86
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
87
+ return { v: 1, salt: toB64u(salt), iv: toB64u(iv), ct: toB64u(ct), tag: toB64u(cipher.getAuthTag()) };
88
+ }
89
+
90
+ export function decrypt(masterKey , envelope , aad ) {
91
+ if (envelope?.v !== 1) throw new CryptoError("Unsupported envelope version");
92
+ const master = parseMasterKey(masterKey);
93
+ const salt = fromB64u(envelope.salt);
94
+ const iv = fromB64u(envelope.iv);
95
+ const tag = fromB64u(envelope.tag);
96
+ if (salt.length !== SALT_BYTES || iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
97
+ throw new CryptoError("Malformed envelope");
98
+ }
99
+ try {
100
+ const decipher = createDecipheriv("aes-256-gcm", hkdf(master, salt, INFO_MESSAGE_KEY, KEY_BYTES), iv, {
101
+ authTagLength: TAG_BYTES,
102
+ });
103
+ decipher.setAAD(Buffer.from(aad, "utf8"));
104
+ decipher.setAuthTag(tag);
105
+ return Buffer.concat([decipher.update(fromB64u(envelope.ct)), decipher.final()]).toString("utf8");
106
+ } catch {
107
+ throw new CryptoError("Decryption failed (wrong key, tampered data or mismatched metadata)");
108
+ }
109
+ }
110
+
111
+ export function buildAad(channelId , messageId , senderSessionId ) {
112
+ return `meshingly/v1|${channelId}|${messageId}|${senderSessionId}`;
113
+ }
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+ export function generateSigningKeys() {
123
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
124
+ return {
125
+ publicKey: toB64u(publicKey.export({ type: "spki", format: "der" })),
126
+ privateKey: toB64u(privateKey.export({ type: "pkcs8", format: "der" })),
127
+ };
128
+ }
129
+
130
+ export function signMessage(privateKey , data ) {
131
+ const key = createPrivateKey({ key: fromB64u(privateKey), format: "der", type: "pkcs8" });
132
+ return toB64u(edSign(null, Buffer.from(data, "utf8"), key));
133
+ }
134
+
135
+ /** Returns false (never throws) for malformed keys or signatures. */
136
+ export function verifyMessage(publicKey , data , signature ) {
137
+ try {
138
+ const key = createPublicKey({ key: fromB64u(publicKey), format: "der", type: "spki" });
139
+ return edVerify(null, Buffer.from(data, "utf8"), key, fromB64u(signature));
140
+ } catch {
141
+ return false;
142
+ }
143
+ }