@zeph-to/mcp-server 1.11.1 → 1.11.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
@@ -27,13 +27,28 @@
27
27
  * sensitive-but-not-secret.
28
28
  */
29
29
  /**
30
- * Initialize crypto: sync keys with server, then fallback to local/generate.
31
- * Server is source of truth for per-user key pair.
30
+ * Initialize crypto.
31
+ *
32
+ * The MCP server is a CONSUMER of encryption keys, not a generator. Keys
33
+ * are created in the Zeph app where the user explicitly opts in (Settings
34
+ * → Encryption). This function only imports keys that the server already
35
+ * has, and only when the server confirms encryption is enabled.
36
+ *
37
+ * Any other state — server says disabled, server has no keys, server is
38
+ * unreachable — leaves encryption OFF (cache empty, no fallback). A
39
+ * previous version generated and uploaded a fresh keypair on the "no keys
40
+ * anywhere" path; combined with a transient fetch failure, that silently
41
+ * turned encryption on without user consent and locked the account into
42
+ * an "encryption enabled" state on the server.
43
+ *
44
+ * Opt-out: `ZEPH_DISABLE_ENCRYPTION=1` forces crypto off regardless of
45
+ * server state — useful while cleaning up legacy state or for users who
46
+ * never want encryption.
47
+ *
32
48
  * Safe to call concurrently — deduplicates to single init.
33
- * Returns the exported public key (Base64 SPKI).
49
+ * Returns the exported public key when encryption is active, '' otherwise.
34
50
  *
35
- * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
36
- * caller in a dev environment would silently upload keys to prod.
51
+ * NOTE: when `apiKey` is provided, `baseUrl` is required.
37
52
  */
38
53
  export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
39
54
  export declare const getKeyPair: () => CryptoKeyPair | null;
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA2JH;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAyE5E,CAAC;AAqCF,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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA4JH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA8BF,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"}
package/dist/crypto.js CHANGED
@@ -52,14 +52,8 @@ const fromBase64 = (base64) => {
52
52
  };
53
53
  // ─── ECDH key management ───
54
54
  const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
55
- const generateKeyPair = async () => crypto.subtle.generateKey(ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
56
- const exportKeyPair = async (keyPair) => {
57
- const [publicRaw, privateRaw] = await Promise.all([
58
- crypto.subtle.exportKey('spki', keyPair.publicKey),
59
- crypto.subtle.exportKey('pkcs8', keyPair.privateKey),
60
- ]);
61
- return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
62
- };
55
+ // generateKeyPair / exportKeyPair were removed in fix/no-auto-encryption.
56
+ // This module imports keys only; it never creates or exports them.
63
57
  const importPublicKey = async (base64) => crypto.subtle.importKey('spki', fromBase64(base64), ECDH_PARAMS, true, []);
64
58
  const importPrivateKey = async (base64) => crypto.subtle.importKey('pkcs8', fromBase64(base64), ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
65
59
  const importKeyPair = async (exported) => {
@@ -117,80 +111,102 @@ const storeKeys = (exported) => {
117
111
  (0, fs_1.mkdirSync)(KEYS_DIR, { recursive: true, mode: 0o700 });
118
112
  (0, fs_1.writeFileSync)(KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
119
113
  };
114
+ const deleteStoredKeys = () => {
115
+ try {
116
+ (0, fs_1.unlinkSync)(KEYS_PATH);
117
+ }
118
+ catch { /* not present — fine */ }
119
+ };
120
+ const envIsTrue = (key) => {
121
+ const v = process.env[key];
122
+ return !!v && /^(1|true|yes|on)$/i.test(v.trim());
123
+ };
120
124
  // ─── Cached state ───
121
125
  let cachedKeyPair = null;
122
126
  let cachedExportedPublicKey = null;
123
127
  let cachedOwnPublicKey = null;
124
128
  let initPromise = null;
125
129
  /**
126
- * Initialize crypto: sync keys with server, then fallback to local/generate.
127
- * Server is source of truth for per-user key pair.
130
+ * Initialize crypto.
131
+ *
132
+ * The MCP server is a CONSUMER of encryption keys, not a generator. Keys
133
+ * are created in the Zeph app where the user explicitly opts in (Settings
134
+ * → Encryption). This function only imports keys that the server already
135
+ * has, and only when the server confirms encryption is enabled.
136
+ *
137
+ * Any other state — server says disabled, server has no keys, server is
138
+ * unreachable — leaves encryption OFF (cache empty, no fallback). A
139
+ * previous version generated and uploaded a fresh keypair on the "no keys
140
+ * anywhere" path; combined with a transient fetch failure, that silently
141
+ * turned encryption on without user consent and locked the account into
142
+ * an "encryption enabled" state on the server.
143
+ *
144
+ * Opt-out: `ZEPH_DISABLE_ENCRYPTION=1` forces crypto off regardless of
145
+ * server state — useful while cleaning up legacy state or for users who
146
+ * never want encryption.
147
+ *
128
148
  * Safe to call concurrently — deduplicates to single init.
129
- * Returns the exported public key (Base64 SPKI).
149
+ * Returns the exported public key when encryption is active, '' otherwise.
130
150
  *
131
- * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
132
- * caller in a dev environment would silently upload keys to prod.
151
+ * NOTE: when `apiKey` is provided, `baseUrl` is required.
133
152
  */
134
153
  const initCrypto = (apiKey, baseUrl) => {
154
+ // Hard opt-out — skip everything, leave cache empty.
155
+ if (envIsTrue('ZEPH_DISABLE_ENCRYPTION')) {
156
+ cachedKeyPair = null;
157
+ cachedExportedPublicKey = null;
158
+ cachedOwnPublicKey = null;
159
+ return Promise.resolve('');
160
+ }
135
161
  if (apiKey && !baseUrl) {
136
162
  return Promise.reject(new Error('initCrypto: baseUrl is required when apiKey is provided. ' +
137
- 'Pass the resolved config.baseUrl to avoid silently syncing dev keys to prod.'));
163
+ 'Pass the resolved config.baseUrl to avoid talking to the wrong environment.'));
138
164
  }
139
165
  if (initPromise)
140
166
  return initPromise;
141
- // The check above guarantees baseUrl is defined when apiKey is — narrow once.
142
167
  const baseUrlRequired = apiKey ? baseUrl : baseUrl;
143
168
  initPromise = (async () => {
144
- const stored = loadStoredKeys();
145
- // Try server sync if API key available
146
169
  if (apiKey) {
147
170
  const serverResult = await fetchServerKeys(apiKey, baseUrlRequired);
148
- // Server says encryption disabled skip crypto init
149
- if (serverResult && !serverResult.encryptionEnabled) {
171
+ // The only path that turns encryption ON: server confirms enabled AND
172
+ // hands us a real keypair. Everything else leaves the cache empty.
173
+ const haveServerKeys = !!serverResult && serverResult.encryptionEnabled && !!serverResult.keys;
174
+ if (!haveServerKeys) {
150
175
  cachedKeyPair = null;
151
176
  cachedExportedPublicKey = null;
152
177
  cachedOwnPublicKey = null;
153
- return '';
154
- }
155
- if (serverResult?.keys) {
156
- if (!stored || stored.publicKey !== serverResult.keys.publicKey) {
157
- storeKeys(serverResult.keys);
178
+ // If the server is reachable and explicitly says encryption is off,
179
+ // drop any stale local cache so a future regression can't resurrect
180
+ // a keypair that the user already disabled.
181
+ if (serverResult && !serverResult.encryptionEnabled) {
182
+ deleteStoredKeys();
158
183
  }
159
- cachedKeyPair = await importKeyPair(serverResult.keys);
160
- cachedExportedPublicKey = serverResult.keys.publicKey;
161
- cachedOwnPublicKey = cachedKeyPair.publicKey;
162
- return serverResult.keys.publicKey;
184
+ return '';
163
185
  }
164
- if (stored) {
165
- await uploadServerKeys(stored, apiKey, baseUrlRequired);
166
- cachedKeyPair = await importKeyPair(stored);
167
- cachedExportedPublicKey = stored.publicKey;
168
- cachedOwnPublicKey = cachedKeyPair.publicKey;
169
- return stored.publicKey;
186
+ const keys = serverResult.keys;
187
+ const stored = loadStoredKeys();
188
+ if (!stored || stored.publicKey !== keys.publicKey) {
189
+ storeKeys(keys);
170
190
  }
171
- const keyPair = await generateKeyPair();
172
- const exported = await exportKeyPair(keyPair);
173
- storeKeys(exported);
174
- await uploadServerKeys(exported, apiKey, baseUrlRequired);
175
- cachedKeyPair = keyPair;
176
- cachedExportedPublicKey = exported.publicKey;
177
- cachedOwnPublicKey = keyPair.publicKey;
178
- return exported.publicKey;
179
- }
180
- // No API key — local-only mode
181
- if (stored) {
182
- cachedKeyPair = await importKeyPair(stored);
183
- cachedExportedPublicKey = stored.publicKey;
191
+ cachedKeyPair = await importKeyPair(keys);
192
+ cachedExportedPublicKey = keys.publicKey;
184
193
  cachedOwnPublicKey = cachedKeyPair.publicKey;
185
- return stored.publicKey;
194
+ return keys.publicKey;
186
195
  }
187
- const keyPair = await generateKeyPair();
188
- const exported = await exportKeyPair(keyPair);
189
- storeKeys(exported);
190
- cachedKeyPair = keyPair;
191
- cachedExportedPublicKey = exported.publicKey;
192
- cachedOwnPublicKey = keyPair.publicKey;
193
- return exported.publicKey;
196
+ // Local-only mode (no apiKey): load stored keys if they exist; do NOT
197
+ // generate. Used by tests and offline / pre-provisioned setups where
198
+ // a keypair has been dropped into ~/.config/zeph/keys.json out-of-band.
199
+ const stored = loadStoredKeys();
200
+ if (!stored) {
201
+ cachedKeyPair = null;
202
+ cachedExportedPublicKey = null;
203
+ cachedOwnPublicKey = null;
204
+ return '';
205
+ }
206
+ cachedKeyPair = await importKeyPair(stored);
207
+ cachedExportedPublicKey = stored.publicKey;
208
+ cachedOwnPublicKey = cachedKeyPair.publicKey;
209
+ return stored.publicKey;
194
210
  })().catch((err) => {
195
211
  initPromise = null;
196
212
  throw err;
@@ -216,17 +232,9 @@ const fetchServerKeys = async (apiKey, baseUrl) => {
216
232
  return null;
217
233
  }
218
234
  };
219
- const uploadServerKeys = async (keys, apiKey, baseUrl) => {
220
- try {
221
- const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
222
- await fetch(url, {
223
- method: 'PUT',
224
- headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
225
- body: JSON.stringify(keys),
226
- });
227
- }
228
- catch { /* non-critical */ }
229
- };
235
+ // uploadServerKeys was removed in fix/no-auto-encryption the MCP server
236
+ // must never write to /users/me/keys. Keys are created by the Zeph app
237
+ // where the user explicitly opts in.
230
238
  const getKeyPair = () => cachedKeyPair;
231
239
  exports.getKeyPair = getKeyPair;
232
240
  const getPublicKey = () => cachedExportedPublicKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.11.1",
3
+ "version": "1.11.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",