@zeph-to/mcp-server 1.2.2 → 1.4.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/api-client.d.ts +8 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +4 -2
- package/dist/crypto.d.ts +59 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +252 -0
- package/dist/index.js +15 -3
- package/dist/tools/ask.d.ts +5 -0
- package/dist/tools/ask.d.ts.map +1 -0
- package/dist/tools/ask.js +74 -0
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +62 -31
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +52 -11
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare class ZephApiClient {
|
|
|
10
10
|
private readonly baseUrl;
|
|
11
11
|
constructor(config: McpServerConfig);
|
|
12
12
|
sendPush(params: {
|
|
13
|
-
title
|
|
13
|
+
title?: string;
|
|
14
14
|
body?: string;
|
|
15
15
|
url?: string;
|
|
16
16
|
type?: string;
|
|
@@ -22,7 +22,12 @@ export declare class ZephApiClient {
|
|
|
22
22
|
fileName: string;
|
|
23
23
|
fileSize: number;
|
|
24
24
|
fileType: string;
|
|
25
|
+
iv?: string;
|
|
26
|
+
encryptedKey?: string;
|
|
25
27
|
}[];
|
|
28
|
+
isEncrypted?: boolean;
|
|
29
|
+
encryptedKey?: string;
|
|
30
|
+
senderPublicKey?: string;
|
|
26
31
|
}): Promise<PushResponse>;
|
|
27
32
|
triggerHook(hookId: string, params: {
|
|
28
33
|
title: string;
|
|
@@ -34,7 +39,7 @@ export declare class ZephApiClient {
|
|
|
34
39
|
timeout?: number;
|
|
35
40
|
fallback?: string;
|
|
36
41
|
metadata?: Record<string, unknown>;
|
|
37
|
-
hookType?: 'one-way' | 'interactive' | 'input';
|
|
42
|
+
hookType?: 'one-way' | 'interactive' | 'input' | 'combo';
|
|
38
43
|
}): Promise<HookTriggerResponse>;
|
|
39
44
|
getHookEvent(hookId: string, eventId: string): Promise<HookEventResponse>;
|
|
40
45
|
listDevices(): Promise<DevicesResponse>;
|
|
@@ -50,7 +55,7 @@ export declare class ZephApiClient {
|
|
|
50
55
|
fileType: string;
|
|
51
56
|
fileSize: number;
|
|
52
57
|
}): Promise<UploadRequestResponse>;
|
|
53
|
-
uploadToS3(url: string, content: string, contentType: string): Promise<void>;
|
|
58
|
+
uploadToS3(url: string, content: string | Buffer, contentType: string): Promise<void>;
|
|
54
59
|
private request;
|
|
55
60
|
}
|
|
56
61
|
//# sourceMappingURL=api-client.d.ts.map
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;CAKjC;AAKD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAK7B,QAAQ,CAAC,MAAM,EAAE;QACrB,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -55,10 +55,12 @@ class ZephApiClient {
|
|
|
55
55
|
return this.request('POST', '/files/upload-request', params);
|
|
56
56
|
}
|
|
57
57
|
async uploadToS3(url, content, contentType) {
|
|
58
|
+
const isText = typeof content === 'string';
|
|
59
|
+
const body = isText ? content : new Uint8Array(content);
|
|
58
60
|
const response = await fetch(url, {
|
|
59
61
|
method: 'PUT',
|
|
60
|
-
headers: { 'Content-Type': `${contentType}; charset=utf-8` },
|
|
61
|
-
body
|
|
62
|
+
headers: { 'Content-Type': isText ? `${contentType}; charset=utf-8` : contentType },
|
|
63
|
+
body,
|
|
62
64
|
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
63
65
|
});
|
|
64
66
|
if (!response.ok) {
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E encryption for MCP server — self-contained ECDH P-256 + AES-256-GCM
|
|
3
|
+
* Mirrors @zeph/crypto API but bundled inline (no external dependency).
|
|
4
|
+
* Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Initialize crypto: sync keys with server, then fallback to local/generate.
|
|
8
|
+
* Server is source of truth for per-user key pair.
|
|
9
|
+
* Safe to call concurrently — deduplicates to single init.
|
|
10
|
+
* Returns the exported public key (Base64 SPKI).
|
|
11
|
+
*/
|
|
12
|
+
export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
|
|
13
|
+
export declare const getKeyPair: () => CryptoKeyPair | null;
|
|
14
|
+
export declare const getPublicKey: () => string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Encrypt push body for a recipient.
|
|
17
|
+
* Returns fields ready to merge into the sendPush payload.
|
|
18
|
+
*/
|
|
19
|
+
export declare const encryptPushBody: (input: {
|
|
20
|
+
title?: string;
|
|
21
|
+
body?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
}, recipientPublicKeyRaw: string) => Promise<{
|
|
24
|
+
body: string;
|
|
25
|
+
encryptedKey: string;
|
|
26
|
+
senderPublicKey: string;
|
|
27
|
+
isEncrypted: true;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt push body for self (all own devices).
|
|
31
|
+
*/
|
|
32
|
+
export declare const encryptPushBodyForSelf: (input: {
|
|
33
|
+
title?: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
}) => Promise<{
|
|
37
|
+
body: string;
|
|
38
|
+
encryptedKey: string;
|
|
39
|
+
senderPublicKey: string;
|
|
40
|
+
isEncrypted: true;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Encrypt file content for a recipient.
|
|
44
|
+
* Returns encrypted buffer + key material for file attachment metadata.
|
|
45
|
+
*/
|
|
46
|
+
export declare const encryptFileForRecipient: (content: string, recipientPublicKeyRaw: string) => Promise<{
|
|
47
|
+
ciphertext: Buffer;
|
|
48
|
+
iv: string;
|
|
49
|
+
encryptedKey: string;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Encrypt file content for self (all own devices).
|
|
53
|
+
*/
|
|
54
|
+
export declare const encryptFileForSelf: (content: string) => Promise<{
|
|
55
|
+
ciphertext: Buffer;
|
|
56
|
+
iv: string;
|
|
57
|
+
encryptedKey: string;
|
|
58
|
+
}>;
|
|
59
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAwD5E,CAAC;AA4BF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* E2E encryption for MCP server — self-contained ECDH P-256 + AES-256-GCM
|
|
4
|
+
* Mirrors @zeph/crypto API but bundled inline (no external dependency).
|
|
5
|
+
* Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
|
|
9
|
+
/// <reference lib="dom" />
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const path_1 = require("path");
|
|
12
|
+
// ─── Base64 helpers ───
|
|
13
|
+
const toBase64 = (buffer) => {
|
|
14
|
+
const bytes = new Uint8Array(buffer);
|
|
15
|
+
let binary = '';
|
|
16
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
17
|
+
binary += String.fromCharCode(bytes[i]);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
};
|
|
21
|
+
const fromBase64 = (base64) => {
|
|
22
|
+
const binary = atob(base64);
|
|
23
|
+
const bytes = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i++) {
|
|
25
|
+
bytes[i] = binary.charCodeAt(i);
|
|
26
|
+
}
|
|
27
|
+
return bytes.buffer;
|
|
28
|
+
};
|
|
29
|
+
// ─── ECDH key management ───
|
|
30
|
+
const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
|
|
31
|
+
const generateKeyPair = async () => crypto.subtle.generateKey(ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
|
|
32
|
+
const exportKeyPair = async (keyPair) => {
|
|
33
|
+
const [publicRaw, privateRaw] = await Promise.all([
|
|
34
|
+
crypto.subtle.exportKey('spki', keyPair.publicKey),
|
|
35
|
+
crypto.subtle.exportKey('pkcs8', keyPair.privateKey),
|
|
36
|
+
]);
|
|
37
|
+
return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
|
|
38
|
+
};
|
|
39
|
+
const importPublicKey = async (base64) => crypto.subtle.importKey('spki', fromBase64(base64), ECDH_PARAMS, true, []);
|
|
40
|
+
const importPrivateKey = async (base64) => crypto.subtle.importKey('pkcs8', fromBase64(base64), ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
|
|
41
|
+
const importKeyPair = async (exported) => {
|
|
42
|
+
const [publicKey, privateKey] = await Promise.all([
|
|
43
|
+
importPublicKey(exported.publicKey),
|
|
44
|
+
importPrivateKey(exported.privateKey),
|
|
45
|
+
]);
|
|
46
|
+
return { publicKey, privateKey };
|
|
47
|
+
};
|
|
48
|
+
const deriveAesKey = async (privateKey, publicKey) => crypto.subtle.deriveKey({ name: 'ECDH', public: publicKey }, privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
49
|
+
const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey) => {
|
|
50
|
+
const messageKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
51
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
52
|
+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, messageKey, new TextEncoder().encode(plaintext));
|
|
53
|
+
const sharedKey = await deriveAesKey(senderPrivateKey, recipientPublicKey);
|
|
54
|
+
const rawMessageKey = await crypto.subtle.exportKey('raw', messageKey);
|
|
55
|
+
const keyIv = crypto.getRandomValues(new Uint8Array(12));
|
|
56
|
+
const encryptedKey = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: keyIv }, sharedKey, rawMessageKey);
|
|
57
|
+
return {
|
|
58
|
+
ciphertext: toBase64(ciphertext),
|
|
59
|
+
iv: toBase64(iv.buffer),
|
|
60
|
+
encryptedKey: toBase64(encryptedKey),
|
|
61
|
+
keyIv: toBase64(keyIv.buffer),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
// ─── File encryption ───
|
|
65
|
+
const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey) => {
|
|
66
|
+
const buffer = new TextEncoder().encode(content).buffer;
|
|
67
|
+
const fileKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
68
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
69
|
+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, fileKey, buffer);
|
|
70
|
+
const sharedKey = await deriveAesKey(senderPrivateKey, recipientPublicKey);
|
|
71
|
+
const rawFileKey = await crypto.subtle.exportKey('raw', fileKey);
|
|
72
|
+
const keyIv = crypto.getRandomValues(new Uint8Array(12));
|
|
73
|
+
const encryptedKey = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: keyIv }, sharedKey, rawFileKey);
|
|
74
|
+
return {
|
|
75
|
+
ciphertext: Buffer.from(ciphertext),
|
|
76
|
+
iv: toBase64(iv.buffer),
|
|
77
|
+
encryptedKey: toBase64(encryptedKey),
|
|
78
|
+
keyIv: toBase64(keyIv.buffer),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
// ─── Key persistence (~/.config/zeph/keys.json) ───
|
|
82
|
+
const KEYS_DIR = (0, path_1.join)(process.env.HOME ?? '~', '.config', 'zeph');
|
|
83
|
+
const KEYS_PATH = (0, path_1.join)(KEYS_DIR, 'keys.json');
|
|
84
|
+
const loadStoredKeys = () => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse((0, fs_1.readFileSync)(KEYS_PATH, 'utf-8'));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const storeKeys = (exported) => {
|
|
93
|
+
(0, fs_1.mkdirSync)(KEYS_DIR, { recursive: true, mode: 0o700 });
|
|
94
|
+
(0, fs_1.writeFileSync)(KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
|
|
95
|
+
};
|
|
96
|
+
// ─── Cached state ───
|
|
97
|
+
let cachedKeyPair = null;
|
|
98
|
+
let cachedExportedPublicKey = null;
|
|
99
|
+
let cachedOwnPublicKey = null;
|
|
100
|
+
let initPromise = null;
|
|
101
|
+
/**
|
|
102
|
+
* Initialize crypto: sync keys with server, then fallback to local/generate.
|
|
103
|
+
* Server is source of truth for per-user key pair.
|
|
104
|
+
* Safe to call concurrently — deduplicates to single init.
|
|
105
|
+
* Returns the exported public key (Base64 SPKI).
|
|
106
|
+
*/
|
|
107
|
+
const initCrypto = (apiKey, baseUrl) => {
|
|
108
|
+
if (initPromise)
|
|
109
|
+
return initPromise;
|
|
110
|
+
initPromise = (async () => {
|
|
111
|
+
const stored = loadStoredKeys();
|
|
112
|
+
// Try server sync if API key available
|
|
113
|
+
if (apiKey) {
|
|
114
|
+
const serverKeys = await fetchServerKeys(apiKey, baseUrl);
|
|
115
|
+
if (serverKeys) {
|
|
116
|
+
if (!stored || stored.publicKey !== serverKeys.publicKey) {
|
|
117
|
+
storeKeys(serverKeys);
|
|
118
|
+
}
|
|
119
|
+
cachedKeyPair = await importKeyPair(serverKeys);
|
|
120
|
+
cachedExportedPublicKey = serverKeys.publicKey;
|
|
121
|
+
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
122
|
+
return serverKeys.publicKey;
|
|
123
|
+
}
|
|
124
|
+
if (stored) {
|
|
125
|
+
await uploadServerKeys(stored, apiKey, baseUrl);
|
|
126
|
+
cachedKeyPair = await importKeyPair(stored);
|
|
127
|
+
cachedExportedPublicKey = stored.publicKey;
|
|
128
|
+
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
129
|
+
return stored.publicKey;
|
|
130
|
+
}
|
|
131
|
+
const keyPair = await generateKeyPair();
|
|
132
|
+
const exported = await exportKeyPair(keyPair);
|
|
133
|
+
storeKeys(exported);
|
|
134
|
+
await uploadServerKeys(exported, apiKey, baseUrl);
|
|
135
|
+
cachedKeyPair = keyPair;
|
|
136
|
+
cachedExportedPublicKey = exported.publicKey;
|
|
137
|
+
cachedOwnPublicKey = keyPair.publicKey;
|
|
138
|
+
return exported.publicKey;
|
|
139
|
+
}
|
|
140
|
+
// No API key — local-only mode
|
|
141
|
+
if (stored) {
|
|
142
|
+
cachedKeyPair = await importKeyPair(stored);
|
|
143
|
+
cachedExportedPublicKey = stored.publicKey;
|
|
144
|
+
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
145
|
+
return stored.publicKey;
|
|
146
|
+
}
|
|
147
|
+
const keyPair = await generateKeyPair();
|
|
148
|
+
const exported = await exportKeyPair(keyPair);
|
|
149
|
+
storeKeys(exported);
|
|
150
|
+
cachedKeyPair = keyPair;
|
|
151
|
+
cachedExportedPublicKey = exported.publicKey;
|
|
152
|
+
cachedOwnPublicKey = keyPair.publicKey;
|
|
153
|
+
return exported.publicKey;
|
|
154
|
+
})().catch((err) => {
|
|
155
|
+
initPromise = null;
|
|
156
|
+
throw err;
|
|
157
|
+
});
|
|
158
|
+
return initPromise;
|
|
159
|
+
};
|
|
160
|
+
exports.initCrypto = initCrypto;
|
|
161
|
+
// ─── Server key sync helpers ───
|
|
162
|
+
const fetchServerKeys = async (apiKey, baseUrl) => {
|
|
163
|
+
try {
|
|
164
|
+
const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
|
|
165
|
+
const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
return null;
|
|
168
|
+
const json = await res.json();
|
|
169
|
+
const keys = json.data?.encryptionKeys;
|
|
170
|
+
return keys?.publicKey && keys?.privateKey ? keys : null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const uploadServerKeys = async (keys, apiKey, baseUrl) => {
|
|
177
|
+
try {
|
|
178
|
+
const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
|
|
179
|
+
await fetch(url, {
|
|
180
|
+
method: 'PUT',
|
|
181
|
+
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(keys),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch { /* non-critical */ }
|
|
186
|
+
};
|
|
187
|
+
const getKeyPair = () => cachedKeyPair;
|
|
188
|
+
exports.getKeyPair = getKeyPair;
|
|
189
|
+
const getPublicKey = () => cachedExportedPublicKey;
|
|
190
|
+
exports.getPublicKey = getPublicKey;
|
|
191
|
+
/**
|
|
192
|
+
* Encrypt push body for a recipient.
|
|
193
|
+
* Returns fields ready to merge into the sendPush payload.
|
|
194
|
+
*/
|
|
195
|
+
const encryptPushBody = async (input, recipientPublicKeyRaw) => {
|
|
196
|
+
if (!cachedKeyPair || !cachedExportedPublicKey)
|
|
197
|
+
throw new Error('Crypto not initialized');
|
|
198
|
+
const recipientKey = await importPublicKey(recipientPublicKeyRaw);
|
|
199
|
+
const payload = await encrypt(JSON.stringify({ title: input.title, body: input.body, url: input.url }), cachedKeyPair.privateKey, recipientKey);
|
|
200
|
+
return {
|
|
201
|
+
body: JSON.stringify({ ciphertext: payload.ciphertext, iv: payload.iv }),
|
|
202
|
+
encryptedKey: JSON.stringify({ encryptedKey: payload.encryptedKey, keyIv: payload.keyIv }),
|
|
203
|
+
senderPublicKey: cachedExportedPublicKey,
|
|
204
|
+
isEncrypted: true,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
exports.encryptPushBody = encryptPushBody;
|
|
208
|
+
/**
|
|
209
|
+
* Encrypt push body for self (all own devices).
|
|
210
|
+
*/
|
|
211
|
+
const encryptPushBodyForSelf = async (input) => {
|
|
212
|
+
if (!cachedKeyPair || !cachedExportedPublicKey || !cachedOwnPublicKey)
|
|
213
|
+
throw new Error('Crypto not initialized');
|
|
214
|
+
const payload = await encrypt(JSON.stringify({ title: input.title, body: input.body, url: input.url }), cachedKeyPair.privateKey, cachedOwnPublicKey);
|
|
215
|
+
return {
|
|
216
|
+
body: JSON.stringify({ ciphertext: payload.ciphertext, iv: payload.iv }),
|
|
217
|
+
encryptedKey: JSON.stringify({ encryptedKey: payload.encryptedKey, keyIv: payload.keyIv }),
|
|
218
|
+
senderPublicKey: cachedExportedPublicKey,
|
|
219
|
+
isEncrypted: true,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
exports.encryptPushBodyForSelf = encryptPushBodyForSelf;
|
|
223
|
+
/**
|
|
224
|
+
* Encrypt file content for a recipient.
|
|
225
|
+
* Returns encrypted buffer + key material for file attachment metadata.
|
|
226
|
+
*/
|
|
227
|
+
const encryptFileForRecipient = async (content, recipientPublicKeyRaw) => {
|
|
228
|
+
if (!cachedKeyPair)
|
|
229
|
+
throw new Error('Crypto not initialized');
|
|
230
|
+
const recipientKey = await importPublicKey(recipientPublicKeyRaw);
|
|
231
|
+
const result = await encryptFileContent(content, cachedKeyPair.privateKey, recipientKey);
|
|
232
|
+
return {
|
|
233
|
+
ciphertext: result.ciphertext,
|
|
234
|
+
iv: result.iv,
|
|
235
|
+
encryptedKey: JSON.stringify({ encryptedKey: result.encryptedKey, keyIv: result.keyIv }),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
exports.encryptFileForRecipient = encryptFileForRecipient;
|
|
239
|
+
/**
|
|
240
|
+
* Encrypt file content for self (all own devices).
|
|
241
|
+
*/
|
|
242
|
+
const encryptFileForSelf = async (content) => {
|
|
243
|
+
if (!cachedKeyPair || !cachedOwnPublicKey)
|
|
244
|
+
throw new Error('Crypto not initialized');
|
|
245
|
+
const result = await encryptFileContent(content, cachedKeyPair.privateKey, cachedOwnPublicKey);
|
|
246
|
+
return {
|
|
247
|
+
ciphertext: result.ciphertext,
|
|
248
|
+
iv: result.iv,
|
|
249
|
+
encryptedKey: JSON.stringify({ encryptedKey: result.encryptedKey, keyIv: result.keyIv }),
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
exports.encryptFileForSelf = encryptFileForSelf;
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
7
7
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
8
|
const config_js_1 = require("./config.js");
|
|
9
9
|
const api_client_js_1 = require("./api-client.js");
|
|
10
|
+
const crypto_js_1 = require("./crypto.js");
|
|
10
11
|
const notify_js_1 = require("./tools/notify.js");
|
|
11
12
|
const prompt_js_1 = require("./tools/prompt.js");
|
|
12
13
|
const input_js_1 = require("./tools/input.js");
|
|
@@ -15,6 +16,7 @@ const list_js_1 = require("./tools/list.js");
|
|
|
15
16
|
const dismiss_js_1 = require("./tools/dismiss.js");
|
|
16
17
|
const broadcast_js_1 = require("./tools/broadcast.js");
|
|
17
18
|
const file_js_1 = require("./tools/file.js");
|
|
19
|
+
const ask_js_1 = require("./tools/ask.js");
|
|
18
20
|
const devices_js_1 = require("./resources/devices.js");
|
|
19
21
|
const channels_js_1 = require("./resources/channels.js");
|
|
20
22
|
const getVersion = () => {
|
|
@@ -26,8 +28,7 @@ const getVersion = () => {
|
|
|
26
28
|
return '0.0.0';
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
|
-
const createServer = () => {
|
|
30
|
-
const config = (0, config_js_1.loadConfig)();
|
|
31
|
+
const createServer = (config) => {
|
|
31
32
|
const client = new api_client_js_1.ZephApiClient(config);
|
|
32
33
|
const server = new mcp_js_1.McpServer({
|
|
33
34
|
name: 'zeph',
|
|
@@ -46,6 +47,7 @@ const createServer = () => {
|
|
|
46
47
|
'- zeph_file: Send a text file (logs, reports, code)',
|
|
47
48
|
'- zeph_prompt: Ask user to choose from options (requires ZEPH_HOOK_ID)',
|
|
48
49
|
'- zeph_input: Request text input from user (requires ZEPH_HOOK_ID)',
|
|
50
|
+
'- zeph_ask: Ask user with buttons + text input combined (requires ZEPH_HOOK_ID). Prefer this over zeph_prompt/zeph_input when you need both options and free-text.',
|
|
49
51
|
'',
|
|
50
52
|
'Resources:',
|
|
51
53
|
'- zeph://devices: Check which devices are online',
|
|
@@ -61,12 +63,22 @@ const createServer = () => {
|
|
|
61
63
|
(0, file_js_1.registerFileTool)(server, client, config);
|
|
62
64
|
(0, prompt_js_1.registerPromptTool)(server, client, config);
|
|
63
65
|
(0, input_js_1.registerInputTool)(server, client, config);
|
|
66
|
+
(0, ask_js_1.registerAskTool)(server, client, config);
|
|
64
67
|
(0, devices_js_1.registerDevicesResource)(server, client);
|
|
65
68
|
(0, channels_js_1.registerChannelsResource)(server, client);
|
|
66
69
|
return server;
|
|
67
70
|
};
|
|
68
71
|
const main = async () => {
|
|
69
|
-
const
|
|
72
|
+
const config = (0, config_js_1.loadConfig)();
|
|
73
|
+
// Initialize E2E encryption keys (sync with server)
|
|
74
|
+
try {
|
|
75
|
+
const publicKey = await (0, crypto_js_1.initCrypto)(config.apiKey, config.baseUrl);
|
|
76
|
+
console.error(`[Crypto] E2E encryption ready (publicKey: ${publicKey.slice(0, 20)}...)`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error('[Crypto] E2E encryption unavailable:', err);
|
|
80
|
+
}
|
|
81
|
+
const server = createServer(config);
|
|
70
82
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
71
83
|
await server.connect(transport);
|
|
72
84
|
console.error('Zeph MCP Server running on stdio');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ZephApiClient } from '../api-client.js';
|
|
3
|
+
import type { McpServerConfig } from '../config.js';
|
|
4
|
+
export declare const registerAskTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
|
|
5
|
+
//# sourceMappingURL=ask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/tools/ask.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA+EhG,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerAskTool = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const error_format_js_1 = require("../error-format.js");
|
|
6
|
+
const poll_js_1 = require("../poll.js");
|
|
7
|
+
const registerAskTool = (server, client, config) => {
|
|
8
|
+
server.registerTool('zeph_ask', {
|
|
9
|
+
description: 'Ask the user a question with optional quick-reply buttons and a text input field. Combines prompt (buttons) and input (text) in a single notification. The user can either tap a button or type a response. Blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
|
|
10
|
+
annotations: {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: false,
|
|
13
|
+
openWorldHint: true,
|
|
14
|
+
},
|
|
15
|
+
inputSchema: {
|
|
16
|
+
title: zod_1.z.string().describe('Question or request title'),
|
|
17
|
+
body: zod_1.z.string().optional().describe('Context or instructions'),
|
|
18
|
+
actions: zod_1.z
|
|
19
|
+
.array(zod_1.z.object({
|
|
20
|
+
id: zod_1.z.string().describe('Unique action identifier'),
|
|
21
|
+
label: zod_1.z.string().describe('Display label for the button'),
|
|
22
|
+
style: zod_1.z.enum(['primary', 'secondary', 'danger']).default('secondary')
|
|
23
|
+
.describe('Button style (default: secondary)'),
|
|
24
|
+
}))
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(4)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Quick-reply buttons (1-4). Omit for text-only input'),
|
|
29
|
+
placeholder: zod_1.z.string().optional().describe('Input field placeholder hint'),
|
|
30
|
+
inputType: zod_1.z
|
|
31
|
+
.enum(['text', 'multiline'])
|
|
32
|
+
.default('text')
|
|
33
|
+
.describe('Input field type (default: text)'),
|
|
34
|
+
timeout: zod_1.z
|
|
35
|
+
.number()
|
|
36
|
+
.min(10)
|
|
37
|
+
.max(600)
|
|
38
|
+
.default(120)
|
|
39
|
+
.describe('Seconds to wait for response (default: 120)'),
|
|
40
|
+
fallback: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Action ID to auto-select on timeout'),
|
|
44
|
+
},
|
|
45
|
+
}, async ({ title, body, actions, placeholder, inputType, timeout, fallback }, ctx) => {
|
|
46
|
+
if (!config.hookId)
|
|
47
|
+
return (0, error_format_js_1.hookNotConfiguredError)();
|
|
48
|
+
try {
|
|
49
|
+
const trigger = await client.triggerHook(config.hookId, {
|
|
50
|
+
title,
|
|
51
|
+
body,
|
|
52
|
+
actions,
|
|
53
|
+
timeout,
|
|
54
|
+
fallback,
|
|
55
|
+
hookType: 'combo',
|
|
56
|
+
metadata: { placeholder, inputType },
|
|
57
|
+
});
|
|
58
|
+
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx);
|
|
59
|
+
if (!event) {
|
|
60
|
+
if (fallback)
|
|
61
|
+
return (0, error_format_js_1.textResult)({ actionId: fallback, timedOut: true });
|
|
62
|
+
return (0, error_format_js_1.timeoutError)(timeout, 'Try again or use zeph_notify for one-way communication');
|
|
63
|
+
}
|
|
64
|
+
const response = event.data.response;
|
|
65
|
+
if (response?.actionId)
|
|
66
|
+
return (0, error_format_js_1.textResult)({ actionId: response.actionId, timedOut: false });
|
|
67
|
+
return (0, error_format_js_1.textResult)({ value: response?.value ?? '', timedOut: false });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return (0, error_format_js_1.formatToolError)(err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
exports.registerAskTool = registerAskTool;
|
package/dist/tools/file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAwBpD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA0EjG,CAAC"}
|
package/dist/tools/file.js
CHANGED
|
@@ -3,6 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerFileTool = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const error_format_js_1 = require("../error-format.js");
|
|
6
|
+
const crypto_js_1 = require("../crypto.js");
|
|
7
|
+
const inferMimeType = (fileName) => {
|
|
8
|
+
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
9
|
+
const map = {
|
|
10
|
+
txt: 'text/plain',
|
|
11
|
+
json: 'application/json',
|
|
12
|
+
csv: 'text/csv',
|
|
13
|
+
md: 'text/markdown',
|
|
14
|
+
html: 'text/html',
|
|
15
|
+
xml: 'text/xml',
|
|
16
|
+
yaml: 'text/yaml',
|
|
17
|
+
yml: 'text/yaml',
|
|
18
|
+
log: 'text/plain',
|
|
19
|
+
ts: 'text/typescript',
|
|
20
|
+
js: 'text/javascript',
|
|
21
|
+
py: 'text/x-python',
|
|
22
|
+
sh: 'text/x-shellscript',
|
|
23
|
+
};
|
|
24
|
+
return map[ext ?? ''] ?? 'text/plain';
|
|
25
|
+
};
|
|
6
26
|
const registerFileTool = (server, client, config) => {
|
|
7
27
|
server.registerTool('zeph_file', {
|
|
8
28
|
description: 'Send a text file to the user\'s device. The content is uploaded and delivered as a file push. Use for logs, reports, code snippets, or any text content.',
|
|
@@ -19,20 +39,50 @@ const registerFileTool = (server, client, config) => {
|
|
|
19
39
|
},
|
|
20
40
|
}, async ({ fileName, content, title, targetDeviceId }) => {
|
|
21
41
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
43
|
+
let fileType = inferMimeType(fileName);
|
|
44
|
+
const originalSize = new TextEncoder().encode(content).byteLength;
|
|
45
|
+
// Step 1: Optionally encrypt file content
|
|
46
|
+
let uploadContent = content;
|
|
47
|
+
let uploadSize = originalSize;
|
|
48
|
+
let fileIv;
|
|
49
|
+
let fileEncryptedKey;
|
|
50
|
+
if (canEncrypt) {
|
|
51
|
+
try {
|
|
52
|
+
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(content);
|
|
53
|
+
uploadContent = encrypted.ciphertext;
|
|
54
|
+
uploadSize = encrypted.ciphertext.length;
|
|
55
|
+
fileType = 'application/octet-stream';
|
|
56
|
+
fileIv = encrypted.iv;
|
|
57
|
+
fileEncryptedKey = encrypted.encryptedKey;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error('[Crypto] File encryption failed, sending plaintext:', err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Step 2: Request upload URL
|
|
64
|
+
const upload = await client.requestUpload({ fileName, fileType, fileSize: uploadSize });
|
|
65
|
+
// Step 3: Upload content to S3
|
|
66
|
+
await client.uploadToS3(upload.data.uploadUrl, uploadContent, fileType);
|
|
67
|
+
// Step 4: Send file push (encrypt push body if possible)
|
|
68
|
+
const pushTitle = title ?? fileName;
|
|
69
|
+
let pushPayload = {
|
|
70
|
+
title: pushTitle,
|
|
31
71
|
type: 'file',
|
|
32
|
-
files: [{ fileKey: upload.data.fileKey, fileName, fileSize, fileType }],
|
|
72
|
+
files: [{ fileKey: upload.data.fileKey, fileName, fileSize: originalSize, fileType: inferMimeType(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
|
|
33
73
|
targetDeviceId: targetDeviceId ?? config.deviceId,
|
|
34
|
-
}
|
|
35
|
-
|
|
74
|
+
};
|
|
75
|
+
if (canEncrypt) {
|
|
76
|
+
try {
|
|
77
|
+
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle });
|
|
78
|
+
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const result = await client.sendPush(pushPayload);
|
|
85
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, fileSize: originalSize, encrypted: canEncrypt });
|
|
36
86
|
}
|
|
37
87
|
catch (err) {
|
|
38
88
|
return (0, error_format_js_1.formatToolError)(err);
|
|
@@ -40,22 +90,3 @@ const registerFileTool = (server, client, config) => {
|
|
|
40
90
|
});
|
|
41
91
|
};
|
|
42
92
|
exports.registerFileTool = registerFileTool;
|
|
43
|
-
const inferMimeType = (fileName) => {
|
|
44
|
-
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
45
|
-
const map = {
|
|
46
|
-
txt: 'text/plain',
|
|
47
|
-
json: 'application/json',
|
|
48
|
-
csv: 'text/csv',
|
|
49
|
-
md: 'text/markdown',
|
|
50
|
-
html: 'text/html',
|
|
51
|
-
xml: 'text/xml',
|
|
52
|
-
yaml: 'text/yaml',
|
|
53
|
-
yml: 'text/yaml',
|
|
54
|
-
log: 'text/plain',
|
|
55
|
-
ts: 'text/typescript',
|
|
56
|
-
js: 'text/javascript',
|
|
57
|
-
py: 'text/x-python',
|
|
58
|
-
sh: 'text/x-shellscript',
|
|
59
|
-
};
|
|
60
|
-
return map[ext ?? ''] ?? 'text/plain';
|
|
61
|
-
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2GnG,CAAC"}
|
package/dist/tools/notify.js
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerNotifyTool = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const error_format_js_1 = require("../error-format.js");
|
|
6
|
-
const
|
|
6
|
+
const crypto_js_1 = require("../crypto.js");
|
|
7
|
+
const BODY_FILE_THRESHOLD = 512;
|
|
7
8
|
const PREVIEW_LENGTH = 200;
|
|
8
9
|
const inferMimeType = (fileName) => {
|
|
9
10
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
@@ -12,7 +13,7 @@ const inferMimeType = (fileName) => {
|
|
|
12
13
|
};
|
|
13
14
|
const registerNotifyTool = (server, client, config) => {
|
|
14
15
|
server.registerTool('zeph_notify', {
|
|
15
|
-
description: 'Send a one-way push notification to the user\'s devices. Use this to inform the user about task completion, errors, or status updates. Long bodies (>
|
|
16
|
+
description: 'Send a one-way push notification to the user\'s devices. Use this to inform the user about task completion, errors, or status updates. Long bodies (>512B) are automatically uploaded as a file for full viewing.',
|
|
16
17
|
annotations: {
|
|
17
18
|
readOnlyHint: false,
|
|
18
19
|
destructiveHint: false,
|
|
@@ -33,33 +34,73 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
33
34
|
const deviceId = targetDeviceId ?? config.deviceId;
|
|
34
35
|
const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
|
|
35
36
|
const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
|
|
37
|
+
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
36
38
|
if (isLongBody && body) {
|
|
37
39
|
const fileName = 'response.md';
|
|
38
40
|
const fileType = inferMimeType(fileName);
|
|
39
41
|
const fileSize = bodyBytes;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
// Encrypt file content if keys available
|
|
43
|
+
let uploadContent = body;
|
|
44
|
+
let uploadContentType = fileType;
|
|
45
|
+
let fileIv;
|
|
46
|
+
let fileEncryptedKey;
|
|
47
|
+
if (canEncrypt) {
|
|
48
|
+
try {
|
|
49
|
+
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
|
|
50
|
+
uploadContent = encrypted.ciphertext;
|
|
51
|
+
uploadContentType = 'application/octet-stream';
|
|
52
|
+
fileIv = encrypted.iv;
|
|
53
|
+
fileEncryptedKey = encrypted.encryptedKey;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error('[Crypto] File encryption failed, sending plaintext:', err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileSize : uploadContent.length });
|
|
60
|
+
await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
|
|
42
61
|
const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
|
|
43
|
-
|
|
62
|
+
// Encrypt push body (title/preview/url) if keys available
|
|
63
|
+
let pushPayload = {
|
|
44
64
|
title,
|
|
45
65
|
body: preview,
|
|
46
66
|
url,
|
|
47
67
|
type: 'file',
|
|
48
68
|
priority,
|
|
49
|
-
files: [{ fileKey: upload.data.fileKey, fileName, fileSize, fileType }],
|
|
69
|
+
files: [{ fileKey: upload.data.fileKey, fileName, fileSize, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }],
|
|
50
70
|
targetDeviceId: deviceId,
|
|
51
|
-
}
|
|
52
|
-
|
|
71
|
+
};
|
|
72
|
+
if (canEncrypt) {
|
|
73
|
+
try {
|
|
74
|
+
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title, body: preview, url });
|
|
75
|
+
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result = await client.sendPush(pushPayload);
|
|
82
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, autoFile: true, encrypted: canEncrypt });
|
|
53
83
|
}
|
|
54
|
-
|
|
84
|
+
// Short body — encrypt push only
|
|
85
|
+
let pushPayload = {
|
|
55
86
|
title,
|
|
56
87
|
body,
|
|
57
88
|
url,
|
|
58
89
|
type: 'hook',
|
|
59
90
|
priority,
|
|
60
91
|
targetDeviceId: deviceId,
|
|
61
|
-
}
|
|
62
|
-
|
|
92
|
+
};
|
|
93
|
+
if (canEncrypt) {
|
|
94
|
+
try {
|
|
95
|
+
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title, body, url });
|
|
96
|
+
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const result = await client.sendPush(pushPayload);
|
|
103
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, encrypted: canEncrypt });
|
|
63
104
|
}
|
|
64
105
|
catch (err) {
|
|
65
106
|
return (0, error_format_js_1.formatToolError)(err);
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;QAC5D,QAAQ,CAAC,EAAE;YACT,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|