@zeph-to/mcp-server 1.15.1 → 1.15.2

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/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
  */
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA0JH;;;;;;;;;;;;;;;;;;;;;;;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;;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,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA0JH;;;;;;;;;;;;;;;;;;;;;;;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,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 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");
@@ -241,6 +241,21 @@ const getKeyPair = () => cachedKeyPair;
241
241
  exports.getKeyPair = getKeyPair;
242
242
  const getPublicKey = () => cachedExportedPublicKey;
243
243
  exports.getPublicKey = getPublicKey;
244
+ /**
245
+ * Drop the cached keys so every later send goes out as plaintext.
246
+ *
247
+ * Used when the server refuses an encrypted send with `PRO_REQUIRED`
248
+ * (ADR-0008): E2E is Pro-only, and this server initialized crypto once at
249
+ * startup — a downgrade after that is only visible at send time. Same end
250
+ * state as the `ZEPH_DISABLE_ENCRYPTION` opt-out. Keys on disk are untouched;
251
+ * a restart after an upgrade re-adopts them.
252
+ */
253
+ const disableCrypto = () => {
254
+ cachedKeyPair = null;
255
+ cachedExportedPublicKey = null;
256
+ cachedOwnPublicKey = null;
257
+ };
258
+ exports.disableCrypto = disableCrypto;
244
259
  /**
245
260
  * Encrypt push body for self (all own devices).
246
261
  */
@@ -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;
@@ -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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAKrE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2EjG,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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAMrE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAiFjG,CAAC"}
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
5
5
  const config_js_1 = require("../config.js");
6
6
  const error_format_js_1 = require("../error-format.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 registerFileTool = (server, client, config) => {
10
11
  server.registerTool('zeph_file', {
@@ -21,8 +22,10 @@ const registerFileTool = (server, client, config) => {
21
22
  targetDeviceId: zod_1.z.string().optional().describe('Target device ID. Omit to use configured default or send to all devices.'),
22
23
  },
23
24
  }, async ({ fileName, content, title, targetDeviceId }) => {
24
- try {
25
- const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
25
+ // Runs a second time as plaintext if the server refuses E2E (Pro-only,
26
+ // ADR-0008). The retry re-uploads the file unencrypted, so the whole
27
+ // upload-then-send sequence has to sit inside this closure.
28
+ const send = async (canEncrypt) => {
26
29
  let fileType = (0, mime_js_1.inferMimeType)(fileName);
27
30
  const originalSize = new TextEncoder().encode(content).byteLength;
28
31
  // Step 1: Optionally encrypt file content
@@ -67,6 +70,9 @@ const registerFileTool = (server, client, config) => {
67
70
  }
68
71
  const result = await client.sendPush(pushPayload);
69
72
  return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, fileSize: originalSize, encrypted: canEncrypt });
73
+ };
74
+ try {
75
+ return await (0, e2e_fallback_js_1.withPlaintextFallback)(send);
70
76
  }
71
77
  catch (err) {
72
78
  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;AASrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2HnG,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"}
@@ -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
- try {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.15.1",
3
+ "version": "1.15.2",
4
4
  "description": "Zeph MCP server — AI agent notifications, prompts, and input via MCP protocol",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",