@zeph-to/mcp-server 1.15.2 → 2.0.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 -7
- package/dist/api-client.d.ts +1 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/crypto.d.ts +65 -42
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +169 -143
- 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 +8 -3
- package/dist/mime.d.ts.map +1 -1
- package/dist/mime.js +20 -0
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +12 -22
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +105 -38
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +44 -40
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- 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
|
|
@@ -323,9 +329,16 @@ Create an API key with the **MCP** preset in Settings > API Keys for the correct
|
|
|
323
329
|
|
|
324
330
|
## Encryption
|
|
325
331
|
|
|
326
|
-
Push bodies are encrypted with AES-256-GCM.
|
|
332
|
+
Push bodies and file attachments are encrypted with AES-256-GCM. This server holds its own ECDH P-256 keypair, generated on first use and stored in `~/.config/zeph/device-keys.json` — the private half never leaves the machine. Each push is encrypted once, and its AES key is wrapped separately for every device on your account using ECDH against that device's public key.
|
|
333
|
+
|
|
334
|
+
Toggle encryption in the Zeph app (Settings → Encryption); when it is off, pushes go out as plaintext. No configuration needed. The opt-in is read once at startup, so **turning it on while this server is running takes effect only after a restart.**
|
|
335
|
+
|
|
336
|
+
**Threat model:** against a passive backend — a leaked snapshot, an operator reading the table — the stored ciphertext and wrapped keys are useless, so push contents stay private. Three limits worth knowing:
|
|
337
|
+
- **No protection from an active malicious operator.** Recipient public keys come from `GET /devices` on that same server, unsigned and unpinned. A backend that injects a device record carrying its own key gets the message key wrapped for it, and reads everything. Closing this needs out-of-band device verification (ADR-0007 Phase 4, not built).
|
|
338
|
+
- **No forward secrecy.** The ECDH secret for a given sender/device pair is static, so compromising either private key opens every past push wrapped for that pair.
|
|
339
|
+
- **`senderPublicKey` is unsigned**, so a swapped one makes a push undecryptable — that direction fails closed rather than leaking.
|
|
327
340
|
|
|
328
|
-
|
|
341
|
+
A device that has not registered a per-device public key cannot be sent to; it is skipped, and if no device qualifies the push goes out in the clear rather than arriving as something nothing can open.
|
|
329
342
|
|
|
330
343
|
## License
|
|
331
344
|
|
package/dist/api-client.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ export declare class ZephApiClient {
|
|
|
20
20
|
sessionId?: string;
|
|
21
21
|
files?: AttachedFile[];
|
|
22
22
|
isEncrypted?: boolean;
|
|
23
|
-
encryptedKey?: string;
|
|
24
23
|
senderPublicKey?: string;
|
|
24
|
+
deviceKeyMap?: Record<string, string>;
|
|
25
25
|
}): Promise<PushResponse>;
|
|
26
26
|
triggerHook(hookId: string, params: {
|
|
27
27
|
title: string;
|
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,0BAA0B,EAC1B,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,YAAY,EACb,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,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,57 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Per-device encryption for the MCP server — self-contained ECDH P-256 +
|
|
3
3
|
* AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
|
|
4
4
|
* dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
|
|
5
5
|
* (the `crypto` global only exists unflagged from Node 19, so we import it).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* How it works (ADR-0007):
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* • Protection against a leaked DB snapshot taken without the key store
|
|
15
|
-
* • Cross-device readability (all your devices share one keypair)
|
|
16
|
-
* What it does NOT give you:
|
|
17
|
-
* • Protection against the Zeph backend itself
|
|
18
|
-
* • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
|
|
19
|
-
* ECDH(self, self), which collapses to a static derived key. A single
|
|
20
|
-
* device compromise (since all your devices share the same keypair)
|
|
21
|
-
* lets the attacker decrypt every past push for which they have the
|
|
22
|
-
* ciphertext. The per-message AES key is random, but its wrap key is
|
|
23
|
-
* static, so wrapped keys are decryptable forever.
|
|
9
|
+
* This process holds its own ECDH keypair. The private half is generated
|
|
10
|
+
* here, written to ~/.config/zeph/device-keys.json, and never leaves the
|
|
11
|
+
* host — the server only ever sees public keys. A push is encrypted once
|
|
12
|
+
* with a random AES key, and that key is wrapped separately for each of the
|
|
13
|
+
* user's registered devices using ECDH(this host, that device).
|
|
24
14
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
15
|
+
* That makes it end-to-end in the standard sense: the backend stores
|
|
16
|
+
* ciphertext plus wrapped keys it cannot unwrap.
|
|
17
|
+
*
|
|
18
|
+
* What it still does not give you:
|
|
19
|
+
* • Forward secrecy — the ECDH secret for a given (sender, device) pair is
|
|
20
|
+
* static, so a compromise of either private key retroactively opens every
|
|
21
|
+
* push wrapped for that pair. The per-message AES key is random; its wrap
|
|
22
|
+
* key is not.
|
|
23
|
+
* • Authenticity beyond the key pairing — nothing signs `senderPublicKey`,
|
|
24
|
+
* so a server that swapped it could make a push undecryptable, though not
|
|
25
|
+
* readable.
|
|
26
|
+
*
|
|
27
|
+
* Superseded scheme: a single account-wide keypair whose private half the
|
|
28
|
+
* backend escrowed so it could sync to new devices. Key escrow was removed
|
|
29
|
+
* server-side (zeph@8a6d21b), which left this client waiting for a private key
|
|
30
|
+
* the API stopped returning — encryption was silently off for months. Nothing
|
|
31
|
+
* here asks for that key any more.
|
|
29
32
|
*/
|
|
33
|
+
/** One target device, as returned by `GET /devices`. */
|
|
34
|
+
export interface DeviceRecipient {
|
|
35
|
+
deviceId: string;
|
|
36
|
+
/** Base64 SPKI of that device's per-device public key. */
|
|
37
|
+
publicKey: string;
|
|
38
|
+
}
|
|
39
|
+
/** deviceId → JSON `{ encryptedKey, keyIv }`, the wire shape the clients parse. */
|
|
40
|
+
export type DeviceKeyMap = Record<string, string>;
|
|
30
41
|
/**
|
|
31
42
|
* Initialize crypto.
|
|
32
43
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
44
|
+
* Encryption turns on only when the account has explicitly opted in —
|
|
45
|
+
* `encryptionEnabled` from `GET /users/me/keys` is the single authoritative
|
|
46
|
+
* signal (ADR-0008), set from the Zeph app. Server unreachable, flag off, or
|
|
47
|
+
* the hard opt-out below all leave the cache empty and every send plaintext.
|
|
37
48
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* anywhere" path; combined with a transient fetch failure, that silently
|
|
42
|
-
* turned encryption on without user consent and locked the account into
|
|
43
|
-
* an "encryption enabled" state on the server.
|
|
49
|
+
* When it is on, this host generates its own keypair on first use and keeps
|
|
50
|
+
* it. Unlike the superseded scheme this asks the server for nothing but the
|
|
51
|
+
* flag: the private key is created here and stays here.
|
|
44
52
|
*
|
|
45
53
|
* Opt-out: `ZEPH_DISABLE_ENCRYPTION=1` forces crypto off regardless of
|
|
46
|
-
* server state
|
|
47
|
-
* never want encryption.
|
|
54
|
+
* server state.
|
|
48
55
|
*
|
|
49
|
-
* Safe to call concurrently — deduplicates to single init.
|
|
50
|
-
* Returns
|
|
56
|
+
* Safe to call concurrently — deduplicates to a single init.
|
|
57
|
+
* Returns this host's public key when encryption is active, '' otherwise.
|
|
51
58
|
*
|
|
52
59
|
* NOTE: when `apiKey` is provided, `baseUrl` is required.
|
|
53
60
|
*/
|
|
54
61
|
export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Keep only devices this host can actually encrypt for.
|
|
64
|
+
*
|
|
65
|
+
* A device without a public key has never run a build that registers one, and
|
|
66
|
+
* a device still advertising the account-wide key has not migrated to
|
|
67
|
+
* per-device E2E — wrapping for either produces a push it cannot open, which
|
|
68
|
+
* is worse than sending plaintext it can read.
|
|
69
|
+
*/
|
|
70
|
+
export declare const selectRecipients: (devices: {
|
|
71
|
+
deviceId: string;
|
|
72
|
+
publicKey?: string;
|
|
73
|
+
}[]) => DeviceRecipient[];
|
|
55
74
|
export declare const getKeyPair: () => CryptoKeyPair | null;
|
|
56
75
|
export declare const getPublicKey: () => string | null;
|
|
57
76
|
/**
|
|
@@ -65,24 +84,28 @@ export declare const getPublicKey: () => string | null;
|
|
|
65
84
|
*/
|
|
66
85
|
export declare const disableCrypto: () => void;
|
|
67
86
|
/**
|
|
68
|
-
* Encrypt push body for
|
|
87
|
+
* Encrypt a push body for the given recipient devices.
|
|
88
|
+
*
|
|
89
|
+
* Returns the wire fields the API expects: `body` carries the ciphertext and
|
|
90
|
+
* IV, `deviceKeyMap` the per-device wrapped keys, `senderPublicKey` the half
|
|
91
|
+
* recipients need to derive the same secret back.
|
|
69
92
|
*/
|
|
70
|
-
export declare const
|
|
93
|
+
export declare const encryptPushBodyForDevices: (input: {
|
|
71
94
|
title?: string;
|
|
72
95
|
body?: string;
|
|
73
96
|
url?: string;
|
|
74
|
-
}) => Promise<{
|
|
97
|
+
}, recipients: DeviceRecipient[]) => Promise<{
|
|
75
98
|
body: string;
|
|
76
|
-
|
|
99
|
+
deviceKeyMap: DeviceKeyMap;
|
|
77
100
|
senderPublicKey: string;
|
|
78
101
|
isEncrypted: true;
|
|
79
102
|
}>;
|
|
80
103
|
/**
|
|
81
|
-
* Encrypt file content for
|
|
104
|
+
* Encrypt file content for the given recipient devices.
|
|
82
105
|
*/
|
|
83
|
-
export declare const
|
|
106
|
+
export declare const encryptFileForDevices: (content: string | Buffer, recipients: DeviceRecipient[]) => Promise<{
|
|
84
107
|
ciphertext: Buffer;
|
|
85
108
|
iv: string;
|
|
86
|
-
|
|
109
|
+
deviceKeyMap: DeviceKeyMap;
|
|
87
110
|
}>;
|
|
88
111
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAuEH,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mFAAmF;AACnF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA6ElD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CA2D5E,CAAC;AAuCF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAClD,eAAe,EAG8C,CAAC;AAEjE,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,IAGhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAkBA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,GAAG,MAAM,EACxB,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAE,CAsBxE,CAAC"}
|