@zeph-to/mcp-server 1.15.1 → 1.16.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/README.md +11 -5
- package/dist/crypto.d.ts +11 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +23 -2
- package/dist/e2e-fallback.d.ts +18 -0
- package/dist/e2e-fallback.d.ts.map +1 -0
- package/dist/e2e-fallback.js +37 -0
- package/dist/index.js +1 -1
- package/dist/mime.d.ts.map +1 -1
- package/dist/mime.js +20 -0
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +69 -8
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,15 +138,21 @@ priority: "normal"
|
|
|
138
138
|
|
|
139
139
|
### zeph_file
|
|
140
140
|
|
|
141
|
-
Send a
|
|
141
|
+
Send a file to the user's device. Either `filePath` (a file already on disk) or
|
|
142
|
+
`content` (text you generated) is required.
|
|
142
143
|
|
|
143
144
|
```
|
|
144
|
-
|
|
145
|
-
content: "{\"status\": \"ok\"}"
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
filePath: "/abs/path/screenshot.png" (images, PDFs, logs — anything on disk)
|
|
146
|
+
content: "{\"status\": \"ok\"}" (text only; requires fileName)
|
|
147
|
+
fileName: "report.json" (required with content; defaults to basename of filePath)
|
|
148
|
+
title: "Build Report" (optional, defaults to fileName)
|
|
149
|
+
targetDeviceId: "dev_..." (optional)
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
Images are delivered with their real mime type and render inline on the device.
|
|
153
|
+
Never base64 a binary file into `content` — pass `filePath` and the server reads
|
|
154
|
+
the bytes off disk.
|
|
155
|
+
|
|
150
156
|
Returns: `{ pushId: "...", fileKey: "...", fileSize: 42 }`
|
|
151
157
|
|
|
152
158
|
### zeph_session_rename
|
package/dist/crypto.d.ts
CHANGED
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
|
|
55
55
|
export declare const getKeyPair: () => CryptoKeyPair | null;
|
|
56
56
|
export declare const getPublicKey: () => string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Drop the cached keys so every later send goes out as plaintext.
|
|
59
|
+
*
|
|
60
|
+
* Used when the server refuses an encrypted send with `PRO_REQUIRED`
|
|
61
|
+
* (ADR-0008): E2E is Pro-only, and this server initialized crypto once at
|
|
62
|
+
* startup — a downgrade after that is only visible at send time. Same end
|
|
63
|
+
* state as the `ZEPH_DISABLE_ENCRYPTION` opt-out. Keys on disk are untouched;
|
|
64
|
+
* a restart after an upgrade re-adopts them.
|
|
65
|
+
*/
|
|
66
|
+
export declare const disableCrypto: () => void;
|
|
57
67
|
/**
|
|
58
68
|
* Encrypt push body for self (all own devices).
|
|
59
69
|
*/
|
|
@@ -70,7 +80,7 @@ export declare const encryptPushBodyForSelf: (input: {
|
|
|
70
80
|
/**
|
|
71
81
|
* Encrypt file content for self (all own devices).
|
|
72
82
|
*/
|
|
73
|
-
export declare const encryptFileForSelf: (content: string) => Promise<{
|
|
83
|
+
export declare const encryptFileForSelf: (content: string | Buffer) => Promise<{
|
|
74
84
|
ciphertext: Buffer;
|
|
75
85
|
iv: string;
|
|
76
86
|
encryptedKey: string;
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAiKH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AAmCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAAO,IAIhC,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;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,GAAG,MAAM,KACvB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
|
package/dist/crypto.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* sensitive-but-not-secret.
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.encryptFileForSelf = exports.encryptPushBodyForSelf = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
|
|
32
|
+
exports.encryptFileForSelf = exports.encryptPushBodyForSelf = exports.disableCrypto = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
|
|
33
33
|
/// <reference lib="dom" />
|
|
34
34
|
const fs_1 = require("fs");
|
|
35
35
|
const os_1 = require("os");
|
|
@@ -78,7 +78,13 @@ const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey) => {
|
|
|
78
78
|
};
|
|
79
79
|
// ─── File encryption ───
|
|
80
80
|
const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey) => {
|
|
81
|
-
|
|
81
|
+
// Binary attachments arrive as a Buffer and must be encrypted byte for byte —
|
|
82
|
+
// running them through TextEncoder would UTF-8 mangle every non-ASCII byte.
|
|
83
|
+
// Both branches are views; subtle.encrypt honours byteOffset/byteLength, so a
|
|
84
|
+
// Buffer carved out of Node's pool encrypts only its own bytes.
|
|
85
|
+
const buffer = typeof content === 'string'
|
|
86
|
+
? new TextEncoder().encode(content)
|
|
87
|
+
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
82
88
|
const fileKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
83
89
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
84
90
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, fileKey, buffer);
|
|
@@ -241,6 +247,21 @@ const getKeyPair = () => cachedKeyPair;
|
|
|
241
247
|
exports.getKeyPair = getKeyPair;
|
|
242
248
|
const getPublicKey = () => cachedExportedPublicKey;
|
|
243
249
|
exports.getPublicKey = getPublicKey;
|
|
250
|
+
/**
|
|
251
|
+
* Drop the cached keys so every later send goes out as plaintext.
|
|
252
|
+
*
|
|
253
|
+
* Used when the server refuses an encrypted send with `PRO_REQUIRED`
|
|
254
|
+
* (ADR-0008): E2E is Pro-only, and this server initialized crypto once at
|
|
255
|
+
* startup — a downgrade after that is only visible at send time. Same end
|
|
256
|
+
* state as the `ZEPH_DISABLE_ENCRYPTION` opt-out. Keys on disk are untouched;
|
|
257
|
+
* a restart after an upgrade re-adopts them.
|
|
258
|
+
*/
|
|
259
|
+
const disableCrypto = () => {
|
|
260
|
+
cachedKeyPair = null;
|
|
261
|
+
cachedExportedPublicKey = null;
|
|
262
|
+
cachedOwnPublicKey = null;
|
|
263
|
+
};
|
|
264
|
+
exports.disableCrypto = disableCrypto;
|
|
244
265
|
/**
|
|
245
266
|
* Encrypt push body for self (all own devices).
|
|
246
267
|
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a send, and repeat it unencrypted if the server says E2E needs Pro.
|
|
3
|
+
*
|
|
4
|
+
* E2E is Pro-only (ADR-0008) and `POST /pushes/send` rejects `isEncrypted`
|
|
5
|
+
* from a free account with 403 `PRO_REQUIRED`. Crypto initializes once at
|
|
6
|
+
* server startup, so a downgrade after that is only visible at send time — and
|
|
7
|
+
* a notification must not be lost over a billing state change. The keys are
|
|
8
|
+
* dropped before the retry, so later sends skip encryption outright and the
|
|
9
|
+
* retry rebuilds the whole payload (a file re-uploads as plaintext instead of
|
|
10
|
+
* leaving an undecryptable blob in S3).
|
|
11
|
+
*
|
|
12
|
+
* `send` receives whether it may encrypt, and must be safe to run twice — the
|
|
13
|
+
* encrypted first upload is left orphaned in S3, which is the accepted cost of
|
|
14
|
+
* not shipping an unreadable attachment. The retry is not itself retried: a
|
|
15
|
+
* second `PRO_REQUIRED` propagates.
|
|
16
|
+
*/
|
|
17
|
+
export declare const withPlaintextFallback: <T>(send: (canEncrypt: boolean) => Promise<T>) => Promise<T>;
|
|
18
|
+
//# sourceMappingURL=e2e-fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e-fallback.d.ts","sourceRoot":"","sources":["../src/e2e-fallback.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,GAAU,CAAC,EAC3C,MAAM,CAAC,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KACxC,OAAO,CAAC,CAAC,CAYX,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withPlaintextFallback = void 0;
|
|
4
|
+
const api_client_js_1 = require("./api-client.js");
|
|
5
|
+
const crypto_js_1 = require("./crypto.js");
|
|
6
|
+
/**
|
|
7
|
+
* Run a send, and repeat it unencrypted if the server says E2E needs Pro.
|
|
8
|
+
*
|
|
9
|
+
* E2E is Pro-only (ADR-0008) and `POST /pushes/send` rejects `isEncrypted`
|
|
10
|
+
* from a free account with 403 `PRO_REQUIRED`. Crypto initializes once at
|
|
11
|
+
* server startup, so a downgrade after that is only visible at send time — and
|
|
12
|
+
* a notification must not be lost over a billing state change. The keys are
|
|
13
|
+
* dropped before the retry, so later sends skip encryption outright and the
|
|
14
|
+
* retry rebuilds the whole payload (a file re-uploads as plaintext instead of
|
|
15
|
+
* leaving an undecryptable blob in S3).
|
|
16
|
+
*
|
|
17
|
+
* `send` receives whether it may encrypt, and must be safe to run twice — the
|
|
18
|
+
* encrypted first upload is left orphaned in S3, which is the accepted cost of
|
|
19
|
+
* not shipping an unreadable attachment. The retry is not itself retried: a
|
|
20
|
+
* second `PRO_REQUIRED` propagates.
|
|
21
|
+
*/
|
|
22
|
+
const withPlaintextFallback = async (send) => {
|
|
23
|
+
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
24
|
+
if (!canEncrypt)
|
|
25
|
+
return send(false);
|
|
26
|
+
try {
|
|
27
|
+
return await send(true);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (!(err instanceof api_client_js_1.ApiError) || err.code !== 'PRO_REQUIRED')
|
|
31
|
+
throw err;
|
|
32
|
+
(0, crypto_js_1.disableCrypto)();
|
|
33
|
+
console.error('[Crypto] End-to-end encryption requires Zeph Pro — resending as plaintext.');
|
|
34
|
+
return send(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.withPlaintextFallback = withPlaintextFallback;
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const createServer = (config) => {
|
|
|
49
49
|
'- zeph_dismiss: Mark a push as read',
|
|
50
50
|
'- zeph_dismiss_all: Clear all notifications',
|
|
51
51
|
'- zeph_broadcast: Send to all subscribers of a channel',
|
|
52
|
-
'- zeph_file: Send a
|
|
52
|
+
'- zeph_file: Send a file — pass filePath for anything on disk (images, PDFs, logs), or content for generated text',
|
|
53
53
|
'- zeph_prompt: Ask user to choose from options (requires ZEPH_HOOK_ID)',
|
|
54
54
|
'- zeph_input: Request text input from user (requires ZEPH_HOOK_ID)',
|
|
55
55
|
'- 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.',
|
package/dist/mime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsCH,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MAGhD,CAAC"}
|
package/dist/mime.js
CHANGED
|
@@ -22,6 +22,26 @@ const EXT_TO_MIME = {
|
|
|
22
22
|
js: 'text/javascript',
|
|
23
23
|
py: 'text/x-python',
|
|
24
24
|
sh: 'text/x-shellscript',
|
|
25
|
+
// Keep this image set in sync with IMAGE_EXTENSIONS in the Zeph client
|
|
26
|
+
// (`libs/shared/src/utils/file.ts`) — an extension the client calls an image
|
|
27
|
+
// but this map doesn't will be labelled text/plain and decoded as UTF-8 on
|
|
28
|
+
// open, which corrupts it. Separate repos, so nothing enforces the match.
|
|
29
|
+
png: 'image/png',
|
|
30
|
+
jpg: 'image/jpeg',
|
|
31
|
+
jpeg: 'image/jpeg',
|
|
32
|
+
gif: 'image/gif',
|
|
33
|
+
webp: 'image/webp',
|
|
34
|
+
heic: 'image/heic',
|
|
35
|
+
heif: 'image/heif',
|
|
36
|
+
bmp: 'image/bmp',
|
|
37
|
+
avif: 'image/avif',
|
|
38
|
+
ico: 'image/x-icon',
|
|
39
|
+
tiff: 'image/tiff',
|
|
40
|
+
tif: 'image/tiff',
|
|
41
|
+
// No svg entry on purpose: an SVG is a script-bearing document, and the
|
|
42
|
+
// clients open decrypted attachments from a same-origin blob URL. It falls
|
|
43
|
+
// through to text/plain, which the text viewer handles fine.
|
|
44
|
+
pdf: 'application/pdf',
|
|
25
45
|
};
|
|
26
46
|
const inferMimeType = (fileName) => {
|
|
27
47
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
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":"
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AA4BrE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAoHjG,CAAC"}
|
package/dist/tools/file.js
CHANGED
|
@@ -2,37 +2,95 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerFileTool = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
5
7
|
const config_js_1 = require("../config.js");
|
|
6
8
|
const error_format_js_1 = require("../error-format.js");
|
|
7
9
|
const crypto_js_1 = require("../crypto.js");
|
|
10
|
+
const e2e_fallback_js_1 = require("../e2e-fallback.js");
|
|
8
11
|
const mime_js_1 = require("../mime.js");
|
|
12
|
+
/**
|
|
13
|
+
* Collapse the two accepted input shapes into one payload.
|
|
14
|
+
*
|
|
15
|
+
* A `filePath` stays a Buffer end to end — images and PDFs only survive the
|
|
16
|
+
* trip as raw bytes, and reading from disk keeps a several-hundred-KB
|
|
17
|
+
* attachment out of the tool call itself (base64 in the arguments would
|
|
18
|
+
* inflate it by a third and burn the agent's context).
|
|
19
|
+
*/
|
|
20
|
+
const resolvePayload = async (args) => {
|
|
21
|
+
if (args.filePath) {
|
|
22
|
+
const body = await (0, promises_1.readFile)(args.filePath);
|
|
23
|
+
return { fileName: args.fileName ?? (0, node_path_1.basename)(args.filePath), body, size: body.byteLength };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
fileName: args.fileName,
|
|
27
|
+
body: args.content,
|
|
28
|
+
size: new TextEncoder().encode(args.content).byteLength,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
9
31
|
const registerFileTool = (server, client, config) => {
|
|
10
32
|
server.registerTool('zeph_file', {
|
|
11
|
-
description: 'Send a
|
|
33
|
+
description: 'Send a file to the user\'s device. Pass `filePath` to send a file that already exists on disk — images (png/jpg/gif/webp/heic), PDFs, logs, anything. Pass `content` instead to send text you generated. Images render inline on the device.',
|
|
12
34
|
annotations: {
|
|
13
35
|
readOnlyHint: false,
|
|
14
36
|
destructiveHint: false,
|
|
15
37
|
openWorldHint: true,
|
|
16
38
|
},
|
|
17
39
|
inputSchema: {
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
filePath: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Absolute path to a local file to send. Required for images, PDFs, and any other binary — never base64 a file into `content`.'),
|
|
44
|
+
content: zod_1.z.string().optional().describe('Text content of the file. Use only for text you generated; requires `fileName`.'),
|
|
45
|
+
fileName: zod_1.z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('File name with extension (e.g., "report.txt"). Required with `content`; defaults to the basename of `filePath`.'),
|
|
20
49
|
title: zod_1.z.string().optional().describe('Notification title (defaults to fileName)'),
|
|
21
50
|
targetDeviceId: zod_1.z.string().optional().describe('Target device ID. Omit to use configured default or send to all devices.'),
|
|
22
51
|
},
|
|
23
|
-
}, async ({ fileName, content, title, targetDeviceId }) => {
|
|
52
|
+
}, async ({ filePath, fileName: fileNameArg, content, title, targetDeviceId }) => {
|
|
53
|
+
if (!filePath && content === undefined) {
|
|
54
|
+
return (0, error_format_js_1.errorResult)({
|
|
55
|
+
error: 'INVALID_INPUT',
|
|
56
|
+
message: 'Either filePath or content is required',
|
|
57
|
+
suggestion: 'Pass filePath for a file on disk (images, PDFs), or content + fileName for generated text',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!filePath && !fileNameArg) {
|
|
61
|
+
return (0, error_format_js_1.errorResult)({
|
|
62
|
+
error: 'INVALID_INPUT',
|
|
63
|
+
message: 'fileName is required when sending content',
|
|
64
|
+
suggestion: 'Add fileName with an extension, e.g. "report.md"',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
let payload;
|
|
24
68
|
try {
|
|
25
|
-
|
|
69
|
+
// Resolved once, outside `send`, so the plaintext retry below doesn't
|
|
70
|
+
// read the file from disk a second time.
|
|
71
|
+
payload = await resolvePayload({ filePath, content, fileName: fileNameArg });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return (0, error_format_js_1.errorResult)({
|
|
75
|
+
error: 'FILE_READ_FAILED',
|
|
76
|
+
message: err instanceof Error ? err.message : String(err),
|
|
77
|
+
suggestion: 'Check that filePath is an absolute path to a readable file',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const { fileName, body, size: originalSize } = payload;
|
|
81
|
+
// Runs a second time as plaintext if the server refuses E2E (Pro-only,
|
|
82
|
+
// ADR-0008). The retry re-uploads the file unencrypted, so the whole
|
|
83
|
+
// upload-then-send sequence has to sit inside this closure.
|
|
84
|
+
const send = async (canEncrypt) => {
|
|
26
85
|
let fileType = (0, mime_js_1.inferMimeType)(fileName);
|
|
27
|
-
const originalSize = new TextEncoder().encode(content).byteLength;
|
|
28
86
|
// Step 1: Optionally encrypt file content
|
|
29
|
-
let uploadContent =
|
|
87
|
+
let uploadContent = body;
|
|
30
88
|
let uploadSize = originalSize;
|
|
31
89
|
let fileIv;
|
|
32
90
|
let fileEncryptedKey;
|
|
33
91
|
if (canEncrypt) {
|
|
34
92
|
try {
|
|
35
|
-
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(
|
|
93
|
+
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
|
|
36
94
|
uploadContent = encrypted.ciphertext;
|
|
37
95
|
uploadSize = encrypted.ciphertext.length;
|
|
38
96
|
fileType = 'application/octet-stream';
|
|
@@ -67,6 +125,9 @@ const registerFileTool = (server, client, config) => {
|
|
|
67
125
|
}
|
|
68
126
|
const result = await client.sendPush(pushPayload);
|
|
69
127
|
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, fileSize: originalSize, encrypted: canEncrypt });
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
return await (0, e2e_fallback_js_1.withPlaintextFallback)(send);
|
|
70
131
|
}
|
|
71
132
|
catch (err) {
|
|
72
133
|
return (0, error_format_js_1.formatToolError)(err);
|
|
@@ -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,EAAmB,KAAK,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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAUrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAgInG,CAAC"}
|
package/dist/tools/notify.js
CHANGED
|
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const error_format_js_1 = require("../error-format.js");
|
|
6
6
|
const config_js_1 = require("../config.js");
|
|
7
7
|
const crypto_js_1 = require("../crypto.js");
|
|
8
|
+
const e2e_fallback_js_1 = require("../e2e-fallback.js");
|
|
8
9
|
const mime_js_1 = require("../mime.js");
|
|
9
10
|
const sanitize_js_1 = require("../sanitize.js");
|
|
10
11
|
// The device feed shows a short preview of the body. Anything longer gets
|
|
@@ -29,14 +30,15 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
29
30
|
targetDeviceId: zod_1.z.string().optional().describe('Target device ID. Omit to use configured default or send to all devices.'),
|
|
30
31
|
},
|
|
31
32
|
}, async ({ title, body, url, priority, targetDeviceId }) => {
|
|
32
|
-
|
|
33
|
+
// Runs a second time as plaintext if the server refuses E2E (Pro-only,
|
|
34
|
+
// ADR-0008) — hence everything after the encryption decision lives here.
|
|
35
|
+
const send = async (canEncrypt) => {
|
|
33
36
|
const deviceId = targetDeviceId ?? config.deviceId;
|
|
34
37
|
const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
|
|
35
38
|
// Strip any tool-call markup that leaked into the body argument.
|
|
36
39
|
const cleanBody = (0, sanitize_js_1.sanitizeText)(body);
|
|
37
40
|
// Attach a file whenever the body would be clipped in the feed preview.
|
|
38
41
|
const isLongBody = !!cleanBody && cleanBody.length > PREVIEW_LENGTH;
|
|
39
|
-
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
40
42
|
if (isLongBody && cleanBody) {
|
|
41
43
|
const fileName = 'response.md';
|
|
42
44
|
const fileType = (0, mime_js_1.inferMimeType)(fileName);
|
|
@@ -115,6 +117,9 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
115
117
|
}
|
|
116
118
|
const result = await client.sendPush(pushPayload);
|
|
117
119
|
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, encrypted: pushEncrypted });
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
return await (0, e2e_fallback_js_1.withPlaintextFallback)(send);
|
|
118
123
|
}
|
|
119
124
|
catch (err) {
|
|
120
125
|
return (0, error_format_js_1.formatToolError)(err);
|
package/package.json
CHANGED