@zeph-to/mcp-server 1.16.0 → 2.1.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 +20 -3
- package/dist/api-client.d.ts +7 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +16 -0
- package/dist/crypto.d.ts +65 -42
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +169 -149
- package/dist/e2e-fallback.d.ts +18 -5
- package/dist/e2e-fallback.d.ts.map +1 -1
- package/dist/e2e-fallback.js +38 -11
- package/dist/index.js +7 -2
- package/dist/response-files.d.ts +36 -0
- package/dist/response-files.d.ts.map +1 -0
- package/dist/response-files.js +75 -0
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +21 -25
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +45 -33
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +8 -2
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +44 -40
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Saving the files a user attached to a hook answer.
|
|
4
|
+
*
|
|
5
|
+
* `zeph_ask` and `zeph_input` hand the agent a JSON result, and an agent
|
|
6
|
+
* cannot look at an S3 key — so an answered screenshot only becomes useful
|
|
7
|
+
* once it is a path on this machine. This module downloads each attachment
|
|
8
|
+
* to `~/.zeph/attachments/hook-<eventId>/` and returns the absolute paths,
|
|
9
|
+
* which the tool then names in its result for the agent to read.
|
|
10
|
+
*
|
|
11
|
+
* The files are plaintext by contract: the hook route carries no sender key,
|
|
12
|
+
* so the server rejects an encrypted attachment on a response rather than
|
|
13
|
+
* let one arrive here as bytes nothing can open.
|
|
14
|
+
*
|
|
15
|
+
* Failure is per file, never fatal. A question that was answered has been
|
|
16
|
+
* answered; losing one image must not turn that into a tool error and make
|
|
17
|
+
* the user answer again.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.saveResponseFiles = exports.attachmentNote = void 0;
|
|
21
|
+
const node_fs_1 = require("node:fs");
|
|
22
|
+
const node_os_1 = require("node:os");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
/** Shared with the `zeph listener` daemon, which writes push attachments here. */
|
|
25
|
+
const ATTACHMENTS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.zeph', 'attachments');
|
|
26
|
+
/**
|
|
27
|
+
* Make a filesystem-safe single path segment: take the basename (drops any
|
|
28
|
+
* `../` prefix), strip control characters and embedded separators, remove
|
|
29
|
+
* leading dots, and cap the length. Empty or dot-only names use the fallback.
|
|
30
|
+
*/
|
|
31
|
+
const safeSegment = (raw, fallback) => {
|
|
32
|
+
const cleaned = (0, node_path_1.basename)(raw)
|
|
33
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
34
|
+
.replace(/[/\\]/g, '_')
|
|
35
|
+
.replace(/^\.+/, '')
|
|
36
|
+
.trim();
|
|
37
|
+
return (cleaned || fallback).slice(0, 200);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The part of a tool result that tells the agent about saved attachments.
|
|
41
|
+
* Spread into the result object; empty when nothing was attached, so a plain
|
|
42
|
+
* answer keeps the shape it has always had.
|
|
43
|
+
*
|
|
44
|
+
* The instruction is explicit because a bare list of paths in a JSON result
|
|
45
|
+
* reads as metadata — the agent has to be told these are the user's answer
|
|
46
|
+
* and that opening them is part of reading it.
|
|
47
|
+
*/
|
|
48
|
+
const attachmentNote = (paths) => paths.length
|
|
49
|
+
? {
|
|
50
|
+
attachments: paths,
|
|
51
|
+
attachmentsNote: `The user attached ${paths.length} file(s) to this answer. Read each path above to see them.`,
|
|
52
|
+
}
|
|
53
|
+
: {};
|
|
54
|
+
exports.attachmentNote = attachmentNote;
|
|
55
|
+
const saveResponseFiles = async (client, eventId, files, deps = {}) => {
|
|
56
|
+
if (!files?.length)
|
|
57
|
+
return [];
|
|
58
|
+
const fetchBytes = deps.fetchBytes ?? ((key) => client.downloadFile(key));
|
|
59
|
+
const dir = (0, node_path_1.join)(deps.dir ?? ATTACHMENTS_DIR, safeSegment(`hook-${eventId}`, 'hook'));
|
|
60
|
+
const paths = [];
|
|
61
|
+
for (const [i, file] of files.entries()) {
|
|
62
|
+
try {
|
|
63
|
+
const bytes = await fetchBytes(file.fileKey);
|
|
64
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
65
|
+
const abs = (0, node_path_1.join)(dir, safeSegment(file.fileName || `file-${i}`, `file-${i}`));
|
|
66
|
+
(0, node_fs_1.writeFileSync)(abs, bytes);
|
|
67
|
+
paths.push(abs);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(`[zeph] attachment "${file.fileName}" failed to save: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return paths;
|
|
74
|
+
};
|
|
75
|
+
exports.saveResponseFiles = saveResponseFiles;
|
package/dist/tools/ask.d.ts.map
CHANGED
|
@@ -1 +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;AAItD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAwBxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,
|
|
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;AAItD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAwBxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SA+H7H,CAAC"}
|
package/dist/tools/ask.js
CHANGED
|
@@ -5,9 +5,9 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const error_format_js_1 = require("../error-format.js");
|
|
6
6
|
const poll_js_1 = require("../poll.js");
|
|
7
7
|
const config_js_1 = require("../config.js");
|
|
8
|
-
const crypto_js_1 = require("../crypto.js");
|
|
9
8
|
const mime_js_1 = require("../mime.js");
|
|
10
9
|
const sanitize_js_1 = require("../sanitize.js");
|
|
10
|
+
const response_files_js_1 = require("../response-files.js");
|
|
11
11
|
// The device feed shows a short preview of the body. Anything longer than
|
|
12
12
|
// this gets truncated there, so we attach the full text as a file — the
|
|
13
13
|
// user can always open the complete content instead of squinting at a
|
|
@@ -23,7 +23,7 @@ const buildAskMarkdown = (title, body, actions) => {
|
|
|
23
23
|
};
|
|
24
24
|
const registerAskTool = (server, client, config, waiter) => {
|
|
25
25
|
server.registerTool('zeph_ask', {
|
|
26
|
-
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.',
|
|
26
|
+
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. The user may also attach screenshots or files to the answer: those arrive as local absolute paths in the `attachments` field of the result, and reading them is part of reading the answer. NOTE: unlike zeph_notify and zeph_file, this tool is never end-to-end encrypted — the hook route it uses cannot carry the sender key — so do not put secrets in the question or expect a private answer.',
|
|
27
27
|
annotations: {
|
|
28
28
|
readOnlyHint: false,
|
|
29
29
|
destructiveHint: false,
|
|
@@ -77,30 +77,21 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
77
77
|
if (exceedsPreview && cleanBody) {
|
|
78
78
|
const fileName = 'response.md';
|
|
79
79
|
const fileType = (0, mime_js_1.inferMimeType)(fileName);
|
|
80
|
-
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
81
80
|
// Self-contained Markdown so the file alone tells the whole story.
|
|
82
81
|
const fileMarkdown = buildAskMarkdown(title, cleanBody, effectiveActions);
|
|
83
82
|
const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
fileEncryptedKey = encrypted.encryptedKey;
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
console.error('[Crypto] File encryption failed, sending plaintext:', err);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
|
|
101
|
-
await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
|
|
83
|
+
// Deliberately NOT encrypted, unlike zeph_notify / zeph_file.
|
|
84
|
+
//
|
|
85
|
+
// This attachment rides `POST /hooks/:id/trigger`, which creates a
|
|
86
|
+
// plaintext push: it neither accepts nor persists `isEncrypted` or
|
|
87
|
+
// `senderPublicKey` (apps/server/src/functions/hooks.ts). Clients
|
|
88
|
+
// gate decryption on both, so an encrypted attachment here would be
|
|
89
|
+
// downloaded as raw ciphertext named response.md. Encrypting this
|
|
90
|
+
// path needs the hook route to carry the sender key first.
|
|
91
|
+
const upload = await client.requestUpload({ fileName, fileType, fileSize: fileBytes });
|
|
92
|
+
await client.uploadToS3(upload.data.uploadUrl, fileMarkdown, fileType);
|
|
102
93
|
triggerBody = cleanBody.slice(0, PREVIEW_LENGTH) + '...';
|
|
103
|
-
files = [{ fileKey: upload.data.fileKey, fileName, fileSize: fileBytes, fileType
|
|
94
|
+
files = [{ fileKey: upload.data.fileKey, fileName, fileSize: fileBytes, fileType }];
|
|
104
95
|
}
|
|
105
96
|
const trigger = await client.triggerHook(config.hookId, {
|
|
106
97
|
title: pushTitle,
|
|
@@ -122,9 +113,14 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
122
113
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again or use zeph_notify for one-way communication');
|
|
123
114
|
}
|
|
124
115
|
const response = event.data.response;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
// Saved before the button branch returns: a user can tap a quick reply
|
|
117
|
+
// AND attach a screenshot, and dropping the files there would lose them
|
|
118
|
+
// silently for the one caller that never looks at `value`.
|
|
119
|
+
const attachments = await (0, response_files_js_1.saveResponseFiles)(client, trigger.data.eventId, response?.files);
|
|
120
|
+
if (response?.actionId) {
|
|
121
|
+
return (0, error_format_js_1.textResult)({ actionId: response.actionId, timedOut: false, ...(0, response_files_js_1.attachmentNote)(attachments) });
|
|
122
|
+
}
|
|
123
|
+
return (0, error_format_js_1.textResult)({ value: response?.value ?? '', timedOut: false, ...(0, response_files_js_1.attachmentNote)(attachments) });
|
|
128
124
|
}
|
|
129
125
|
catch (err) {
|
|
130
126
|
return (0, error_format_js_1.formatToolError)(err);
|
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":"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,
|
|
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,SAmIjG,CAAC"}
|
package/dist/tools/file.js
CHANGED
|
@@ -81,53 +81,65 @@ const registerFileTool = (server, client, config) => {
|
|
|
81
81
|
// Runs a second time as plaintext if the server refuses E2E (Pro-only,
|
|
82
82
|
// ADR-0008). The retry re-uploads the file unencrypted, so the whole
|
|
83
83
|
// upload-then-send sequence has to sit inside this closure.
|
|
84
|
-
const send = async (
|
|
85
|
-
|
|
86
|
-
// Step 1:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let
|
|
91
|
-
if (
|
|
84
|
+
const send = async (recipients) => {
|
|
85
|
+
const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title ?? fileName);
|
|
86
|
+
// Step 1: Encrypt the attachment and the push body together, before
|
|
87
|
+
// anything is uploaded. Doing them one at a time around the upload let
|
|
88
|
+
// a failure land in between and ship ciphertext under a push with no
|
|
89
|
+
// `isEncrypted` — an attachment no client would even try to open.
|
|
90
|
+
let encrypted = null;
|
|
91
|
+
if (recipients) {
|
|
92
92
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
fileIv = encrypted.iv;
|
|
98
|
-
fileEncryptedKey = encrypted.encryptedKey;
|
|
93
|
+
encrypted = {
|
|
94
|
+
file: await (0, crypto_js_1.encryptFileForDevices)(body, recipients),
|
|
95
|
+
push: await (0, crypto_js_1.encryptPushBodyForDevices)({ title: pushTitle }, recipients),
|
|
96
|
+
};
|
|
99
97
|
}
|
|
100
98
|
catch (err) {
|
|
101
|
-
console.error('[Crypto]
|
|
99
|
+
console.error('[Crypto] Encryption failed, sending plaintext:', err);
|
|
102
100
|
}
|
|
103
101
|
}
|
|
102
|
+
const uploadContent = encrypted?.file.ciphertext ?? body;
|
|
103
|
+
const uploadType = encrypted ? 'application/octet-stream' : (0, mime_js_1.inferMimeType)(fileName);
|
|
104
|
+
const uploadSize = encrypted ? encrypted.file.ciphertext.length : originalSize;
|
|
104
105
|
// Step 2: Request upload URL
|
|
105
|
-
const upload = await client.requestUpload({ fileName, fileType, fileSize: uploadSize });
|
|
106
|
+
const upload = await client.requestUpload({ fileName, fileType: uploadType, fileSize: uploadSize });
|
|
106
107
|
// Step 3: Upload content to S3
|
|
107
|
-
await client.uploadToS3(upload.data.uploadUrl, uploadContent,
|
|
108
|
-
// Step 4: Send
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
title: pushTitle,
|
|
108
|
+
await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadType);
|
|
109
|
+
// Step 4: Send the push. `fileType` on the descriptor stays the real
|
|
110
|
+
// type — it drives how the client renders the decrypted bytes.
|
|
111
|
+
const pushPayload = {
|
|
112
|
+
title: encrypted ? undefined : pushTitle,
|
|
112
113
|
type: 'file',
|
|
113
|
-
files: [{
|
|
114
|
+
files: [{
|
|
115
|
+
fileKey: upload.data.fileKey,
|
|
116
|
+
fileName,
|
|
117
|
+
fileSize: originalSize,
|
|
118
|
+
fileType: (0, mime_js_1.inferMimeType)(fileName),
|
|
119
|
+
iv: encrypted?.file.iv,
|
|
120
|
+
deviceKeyMap: encrypted?.file.deviceKeyMap,
|
|
121
|
+
}],
|
|
114
122
|
targetDeviceId: targetDeviceId ?? config.deviceId,
|
|
115
123
|
sessionId: config.sessionId,
|
|
124
|
+
...(encrypted && {
|
|
125
|
+
body: encrypted.push.body,
|
|
126
|
+
isEncrypted: encrypted.push.isEncrypted,
|
|
127
|
+
deviceKeyMap: encrypted.push.deviceKeyMap,
|
|
128
|
+
senderPublicKey: encrypted.push.senderPublicKey,
|
|
129
|
+
}),
|
|
116
130
|
};
|
|
117
|
-
if (canEncrypt) {
|
|
118
|
-
try {
|
|
119
|
-
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle });
|
|
120
|
-
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
131
|
const result = await client.sendPush(pushPayload);
|
|
127
|
-
|
|
132
|
+
// Report what actually went out — an encryption failure above falls
|
|
133
|
+
// back to plaintext, so the recipient list alone would over-claim.
|
|
134
|
+
return (0, error_format_js_1.textResult)({
|
|
135
|
+
pushId: result.data.pushId,
|
|
136
|
+
fileKey: upload.data.fileKey,
|
|
137
|
+
fileSize: originalSize,
|
|
138
|
+
encrypted: !!encrypted,
|
|
139
|
+
});
|
|
128
140
|
};
|
|
129
141
|
try {
|
|
130
|
-
return await (0, e2e_fallback_js_1.withPlaintextFallback)(send);
|
|
142
|
+
return await (0, e2e_fallback_js_1.withPlaintextFallback)(client, send);
|
|
131
143
|
}
|
|
132
144
|
catch (err) {
|
|
133
145
|
return (0, error_format_js_1.formatToolError)(err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAgE/H,CAAC"}
|
package/dist/tools/input.js
CHANGED
|
@@ -6,9 +6,10 @@ const error_format_js_1 = require("../error-format.js");
|
|
|
6
6
|
const poll_js_1 = require("../poll.js");
|
|
7
7
|
const config_js_1 = require("../config.js");
|
|
8
8
|
const sanitize_js_1 = require("../sanitize.js");
|
|
9
|
+
const response_files_js_1 = require("../response-files.js");
|
|
9
10
|
const registerInputTool = (server, client, config, waiter) => {
|
|
10
11
|
server.registerTool('zeph_input', {
|
|
11
|
-
description: 'Request text input from the user via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
|
|
12
|
+
description: 'Request text input from the user via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable. The user may also attach screenshots or files: those arrive as local absolute paths in the `attachments` field of the result, and reading them is part of reading the answer.',
|
|
12
13
|
annotations: {
|
|
13
14
|
readOnlyHint: false,
|
|
14
15
|
destructiveHint: false,
|
|
@@ -46,7 +47,12 @@ const registerInputTool = (server, client, config, waiter) => {
|
|
|
46
47
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
47
48
|
if (!event)
|
|
48
49
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again with a longer timeout');
|
|
49
|
-
|
|
50
|
+
const attachments = await (0, response_files_js_1.saveResponseFiles)(client, trigger.data.eventId, event.data.response?.files);
|
|
51
|
+
return (0, error_format_js_1.textResult)({
|
|
52
|
+
value: event.data.response?.value ?? '',
|
|
53
|
+
timedOut: false,
|
|
54
|
+
...(0, response_files_js_1.attachmentNote)(attachments),
|
|
55
|
+
});
|
|
50
56
|
}
|
|
51
57
|
catch (err) {
|
|
52
58
|
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;AAUrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,
|
|
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,SAuInG,CAAC"}
|
package/dist/tools/notify.js
CHANGED
|
@@ -32,7 +32,7 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
32
32
|
}, async ({ title, body, url, priority, targetDeviceId }) => {
|
|
33
33
|
// Runs a second time as plaintext if the server refuses E2E (Pro-only,
|
|
34
34
|
// ADR-0008) — hence everything after the encryption decision lives here.
|
|
35
|
-
const send = async (
|
|
35
|
+
const send = async (recipients) => {
|
|
36
36
|
const deviceId = targetDeviceId ?? config.deviceId;
|
|
37
37
|
const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
|
|
38
38
|
// Strip any tool-call markup that leaked into the body argument.
|
|
@@ -45,70 +45,74 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
45
45
|
// Self-contained Markdown so the file alone carries the full text.
|
|
46
46
|
const fileMarkdown = `# ${title}\n\n${cleanBody}`;
|
|
47
47
|
const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
if (
|
|
48
|
+
const preview = cleanBody.slice(0, PREVIEW_LENGTH) + '...';
|
|
49
|
+
// Encrypt the attachment and the push body together, before anything
|
|
50
|
+
// is uploaded. Doing them one at a time around the upload let a
|
|
51
|
+
// failure land in between and ship ciphertext under a push with no
|
|
52
|
+
// `isEncrypted` — an attachment no client would even try to open.
|
|
53
|
+
let encrypted = null;
|
|
54
|
+
if (recipients) {
|
|
55
55
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
fileEncryptedKey = encrypted.encryptedKey;
|
|
61
|
-
fileEncrypted = true;
|
|
56
|
+
encrypted = {
|
|
57
|
+
file: await (0, crypto_js_1.encryptFileForDevices)(fileMarkdown, recipients),
|
|
58
|
+
push: await (0, crypto_js_1.encryptPushBodyForDevices)({ title: pushTitle, body: preview, url }, recipients),
|
|
59
|
+
};
|
|
62
60
|
}
|
|
63
61
|
catch (err) {
|
|
64
|
-
console.error('[Crypto]
|
|
62
|
+
console.error('[Crypto] Encryption failed, sending plaintext:', err);
|
|
65
63
|
}
|
|
66
64
|
}
|
|
65
|
+
const uploadContent = encrypted?.file.ciphertext ?? fileMarkdown;
|
|
66
|
+
const uploadContentType = encrypted ? 'application/octet-stream' : fileType;
|
|
67
67
|
const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
|
|
68
68
|
await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
title:
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const pushPayload = {
|
|
70
|
+
title: encrypted ? undefined : pushTitle,
|
|
71
|
+
body: encrypted ? encrypted.push.body : preview,
|
|
72
|
+
// Dropped alongside the title when encrypted: the url is already
|
|
73
|
+
// sealed inside the ciphertext, and for a link push it is the whole
|
|
74
|
+
// payload. Leaving the plaintext copy here would hand the server the
|
|
75
|
+
// one thing `isEncrypted` promises it cannot see.
|
|
76
|
+
url: encrypted ? undefined : url,
|
|
75
77
|
type: 'file',
|
|
76
78
|
priority,
|
|
77
|
-
files: [{
|
|
79
|
+
files: [{
|
|
80
|
+
fileKey: upload.data.fileKey,
|
|
81
|
+
fileName,
|
|
82
|
+
fileSize: fileBytes,
|
|
83
|
+
fileType,
|
|
84
|
+
iv: encrypted?.file.iv,
|
|
85
|
+
deviceKeyMap: encrypted?.file.deviceKeyMap,
|
|
86
|
+
}],
|
|
78
87
|
targetDeviceId: deviceId,
|
|
79
88
|
sessionId: config.sessionId,
|
|
89
|
+
...(encrypted && {
|
|
90
|
+
isEncrypted: encrypted.push.isEncrypted,
|
|
91
|
+
deviceKeyMap: encrypted.push.deviceKeyMap,
|
|
92
|
+
senderPublicKey: encrypted.push.senderPublicKey,
|
|
93
|
+
}),
|
|
80
94
|
};
|
|
81
|
-
let pushEncrypted = false;
|
|
82
|
-
if (canEncrypt) {
|
|
83
|
-
try {
|
|
84
|
-
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body: preview, url });
|
|
85
|
-
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
86
|
-
pushEncrypted = true;
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
95
|
const result = await client.sendPush(pushPayload);
|
|
93
96
|
// Report what actually went out — an encryption failure above falls
|
|
94
|
-
// back to plaintext, so
|
|
95
|
-
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, autoFile: true, encrypted:
|
|
97
|
+
// back to plaintext, so the recipient list alone would over-claim.
|
|
98
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, autoFile: true, encrypted: !!encrypted });
|
|
96
99
|
}
|
|
97
100
|
// Short body — encrypt push only
|
|
98
101
|
let pushPayload = {
|
|
99
102
|
title: pushTitle,
|
|
100
103
|
body: cleanBody,
|
|
101
|
-
url,
|
|
104
|
+
url, // replaced below when the encrypted envelope takes over
|
|
102
105
|
type: 'hook',
|
|
103
106
|
priority,
|
|
104
107
|
targetDeviceId: deviceId,
|
|
105
108
|
sessionId: config.sessionId,
|
|
106
109
|
};
|
|
107
110
|
let pushEncrypted = false;
|
|
108
|
-
if (
|
|
111
|
+
if (recipients) {
|
|
109
112
|
try {
|
|
110
|
-
const enc = await (0, crypto_js_1.
|
|
111
|
-
|
|
113
|
+
const enc = await (0, crypto_js_1.encryptPushBodyForDevices)({ title: pushTitle, body: cleanBody, url }, recipients);
|
|
114
|
+
// `url: undefined` for the same reason as `title` — see the file branch.
|
|
115
|
+
pushPayload = { ...pushPayload, title: undefined, url: undefined, body: enc.body, isEncrypted: enc.isEncrypted, deviceKeyMap: enc.deviceKeyMap, senderPublicKey: enc.senderPublicKey };
|
|
112
116
|
pushEncrypted = true;
|
|
113
117
|
}
|
|
114
118
|
catch (err) {
|
|
@@ -119,7 +123,7 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
119
123
|
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, encrypted: pushEncrypted });
|
|
120
124
|
};
|
|
121
125
|
try {
|
|
122
|
-
return await (0, e2e_fallback_js_1.withPlaintextFallback)(send);
|
|
126
|
+
return await (0, e2e_fallback_js_1.withPlaintextFallback)(client, send);
|
|
123
127
|
}
|
|
124
128
|
catch (err) {
|
|
125
129
|
return (0, error_format_js_1.formatToolError)(err);
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface HookEventResponse {
|
|
|
23
23
|
response?: {
|
|
24
24
|
actionId?: string;
|
|
25
25
|
value?: string;
|
|
26
|
+
/** Screenshots and files the user attached to the answer — plaintext,
|
|
27
|
+
* since the hook route carries no key this end could decrypt with. */
|
|
28
|
+
files?: AttachedFile[];
|
|
26
29
|
respondedDeviceId?: string;
|
|
27
30
|
};
|
|
28
31
|
};
|
|
@@ -89,13 +92,24 @@ export interface UploadRequestResponse {
|
|
|
89
92
|
uploadUrl: string;
|
|
90
93
|
};
|
|
91
94
|
}
|
|
95
|
+
export interface DownloadUrlResponse {
|
|
96
|
+
data: {
|
|
97
|
+
downloadUrl: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
92
100
|
export interface AttachedFile {
|
|
93
101
|
fileKey: string;
|
|
94
102
|
fileName: string;
|
|
95
103
|
fileSize: number;
|
|
96
104
|
fileType: string;
|
|
97
105
|
iv?: string;
|
|
98
|
-
|
|
106
|
+
/**
|
|
107
|
+
* E2E: the file AES key wrapped once per recipient device, keyed by
|
|
108
|
+
* `deviceId`. Each value is a JSON string `{ encryptedKey, keyIv }`.
|
|
109
|
+
* (The superseded account-wide `encryptedKey` field is not written here —
|
|
110
|
+
* nothing can unwrap it since key escrow was removed.)
|
|
111
|
+
*/
|
|
112
|
+
deviceKeyMap?: Record<string, string>;
|
|
99
113
|
}
|
|
100
114
|
export interface ToolError {
|
|
101
115
|
error: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
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;mFACuE;YACvE,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;YACvB,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;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,UAAU,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,SAAS,EAAE,OAAO,GAAG,MAAM,CAAC;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|