@hwip/mcp-server 0.0.0-stage → 0.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/LICENSE +190 -0
- package/NOTICE +10 -0
- package/README.md +413 -2
- package/dist/agent-target.d.ts +45 -0
- package/dist/agent-target.js +81 -0
- package/dist/api-client.d.ts +162 -0
- package/dist/api-client.js +517 -0
- package/dist/ask-context-attach.d.ts +67 -0
- package/dist/ask-context-attach.js +147 -0
- package/dist/ask-seal.d.ts +118 -0
- package/dist/ask-seal.js +194 -0
- package/dist/command-seal.d.ts +49 -0
- package/dist/command-seal.js +90 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +147 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.js +561 -0
- package/dist/crypto.d.ts +234 -0
- package/dist/crypto.js +606 -0
- package/dist/error-format.d.ts +7 -0
- package/dist/error-format.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +30 -0
- package/dist/key-changes.d.ts +50 -0
- package/dist/key-changes.js +163 -0
- package/dist/mime.d.ts +16 -0
- package/dist/mime.js +58 -0
- package/dist/peer-key.d.ts +36 -0
- package/dist/peer-key.js +35 -0
- package/dist/poll.d.ts +24 -0
- package/dist/poll.js +156 -0
- package/dist/push-audience.d.ts +35 -0
- package/dist/push-audience.js +64 -0
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +17 -0
- package/dist/remote-state.d.ts +38 -0
- package/dist/remote-state.js +81 -0
- package/dist/resources/channels.d.ts +4 -0
- package/dist/resources/channels.js +24 -0
- package/dist/resources/devices.d.ts +4 -0
- package/dist/resources/devices.js +24 -0
- package/dist/sanitize.d.ts +41 -0
- package/dist/sanitize.js +106 -0
- package/dist/self-register.d.ts +34 -0
- package/dist/self-register.js +51 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +63 -0
- package/dist/tools/agent-local.d.ts +17 -0
- package/dist/tools/agent-local.js +139 -0
- package/dist/tools/agent-send.d.ts +6 -0
- package/dist/tools/agent-send.js +89 -0
- package/dist/tools/ask.d.ts +9 -0
- package/dist/tools/ask.js +275 -0
- package/dist/tools/broadcast.d.ts +5 -0
- package/dist/tools/broadcast.js +42 -0
- package/dist/tools/clipboard.d.ts +5 -0
- package/dist/tools/clipboard.js +47 -0
- package/dist/tools/dismiss.d.ts +5 -0
- package/dist/tools/dismiss.js +49 -0
- package/dist/tools/file.d.ts +5 -0
- package/dist/tools/file.js +169 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.js +44 -0
- package/dist/tools/lookup.d.ts +10 -0
- package/dist/tools/lookup.js +45 -0
- package/dist/tools/notify.d.ts +5 -0
- package/dist/tools/notify.js +118 -0
- package/dist/tools/rename.d.ts +5 -0
- package/dist/tools/rename.js +45 -0
- package/dist/trust-pins.d.ts +70 -0
- package/dist/trust-pins.js +269 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +2 -0
- package/dist/url-check.d.ts +11 -0
- package/dist/url-check.js +57 -0
- package/dist/ws-wait.d.ts +72 -0
- package/dist/ws-wait.js +200 -0
- package/package.json +70 -4
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-device encryption for the MCP server — self-contained ECDH P-256 +
|
|
3
|
+
* AES-256-GCM. Mirrors @hwip/crypto API but bundled inline (no external
|
|
4
|
+
* dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
|
|
5
|
+
* (the `crypto` global only exists unflagged from Node 19, so we import it).
|
|
6
|
+
*
|
|
7
|
+
* How it works (ADR-0007):
|
|
8
|
+
*
|
|
9
|
+
* This host holds one ECDH keypair, in ~/.hwip/device-keys.json — the
|
|
10
|
+
* Machine Device's, shared with `hwip listener`. Either puts its public half
|
|
11
|
+
* on the device record when the list lacks it (`self-register.ts`). Whichever
|
|
12
|
+
* process starts first creates it; the
|
|
13
|
+
* private half never leaves the host — the server only ever sees public
|
|
14
|
+
* keys. A push is encrypted once
|
|
15
|
+
* with a random AES key, and that key is wrapped separately for each of the
|
|
16
|
+
* user's registered devices using ECDH(this host, that device).
|
|
17
|
+
*
|
|
18
|
+
* That makes it end-to-end in the standard sense: the backend stores
|
|
19
|
+
* ciphertext plus wrapped keys it cannot unwrap.
|
|
20
|
+
*
|
|
21
|
+
* What it still does not give you:
|
|
22
|
+
* • Forward secrecy — the ECDH secret for a given (sender, device) pair is
|
|
23
|
+
* static, so a compromise of either private key retroactively opens every
|
|
24
|
+
* push wrapped for that pair. The per-message AES key is random; its wrap
|
|
25
|
+
* key is not.
|
|
26
|
+
* • Authenticity beyond the key pairing — nothing signs `senderPublicKey`,
|
|
27
|
+
* so a server that swapped it could make a push undecryptable, though not
|
|
28
|
+
* readable.
|
|
29
|
+
*
|
|
30
|
+
* Superseded scheme: a single account-wide keypair whose private half the
|
|
31
|
+
* backend escrowed so it could sync to new devices. Key escrow was removed
|
|
32
|
+
* server-side, which left this client waiting for a private key
|
|
33
|
+
* the API stopped returning — encryption was silently off for months. Nothing
|
|
34
|
+
* here asks for that key any more.
|
|
35
|
+
*/
|
|
36
|
+
/** One target device, as returned by `GET /devices`. */
|
|
37
|
+
export interface DeviceRecipient {
|
|
38
|
+
deviceId: string;
|
|
39
|
+
/** Base64 SPKI of that device's per-device public key. */
|
|
40
|
+
publicKey: string;
|
|
41
|
+
}
|
|
42
|
+
/** deviceId → JSON `{ encryptedKey, keyIv }`, the wire shape the clients parse. */
|
|
43
|
+
export type DeviceKeyMap = Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* Initialize crypto.
|
|
46
|
+
*
|
|
47
|
+
* This host generates its own keypair on first use and keeps it, whatever the
|
|
48
|
+
* account's plan: the keypair is this machine's identity, registered ungated so
|
|
49
|
+
* that any device can address it (ADR-0013 decision 3, amended 2026-09-16).
|
|
50
|
+
* The private key is created here and stays here. Every push is sealed (F16):
|
|
51
|
+
* there is no account setting and no host switch that turns it off.
|
|
52
|
+
*
|
|
53
|
+
* Asks the server nothing. Safe to call concurrently — deduplicates to a single init.
|
|
54
|
+
* Returns this host's public key, or '' when it has none.
|
|
55
|
+
*/
|
|
56
|
+
export declare const initCrypto: (apiKey?: string) => Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves once the start-up `initCrypto` has settled, however it went. index.ts connects the
|
|
59
|
+
* MCP transport without waiting for it, so the first send waits here instead. A start that
|
|
60
|
+
* failed (a key file that would not load) is tried once more for each send that finds no keys.
|
|
61
|
+
*/
|
|
62
|
+
export declare const cryptoReady: () => Promise<void>;
|
|
63
|
+
/** One line for stderr saying what `initCrypto` found. */
|
|
64
|
+
export declare const cryptoStatusLine: () => string;
|
|
65
|
+
/**
|
|
66
|
+
* Keep only devices this host can actually encrypt for.
|
|
67
|
+
*
|
|
68
|
+
* A device without a public key has never run a build that registers one:
|
|
69
|
+
* wrapping for it produces a push it cannot open.
|
|
70
|
+
*/
|
|
71
|
+
export declare const selectRecipients: (devices: {
|
|
72
|
+
deviceId: string;
|
|
73
|
+
publicKey?: string;
|
|
74
|
+
}[]) => DeviceRecipient[];
|
|
75
|
+
export declare const getKeyPair: () => CryptoKeyPair | null;
|
|
76
|
+
export declare const getPublicKey: () => string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Drop everything, keypair included, so this host does no crypto at all.
|
|
79
|
+
*
|
|
80
|
+
* Only the keyless path in `initCrypto` uses this. Keys on disk are untouched; a restart re-adopts them.
|
|
81
|
+
*/
|
|
82
|
+
export declare const disableCrypto: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* Encrypt a push body for the given recipient devices.
|
|
85
|
+
*
|
|
86
|
+
* Returns the wire fields the API expects: `body` carries the ciphertext and
|
|
87
|
+
* IV, `deviceKeyMap` the per-device wrapped keys, `senderPublicKey` the half
|
|
88
|
+
* recipients need to derive the same secret back.
|
|
89
|
+
*/
|
|
90
|
+
export declare const encryptPushBodyForDevices: (input: PushBodyInput, recipients: DeviceRecipient[]) => Promise<{
|
|
91
|
+
body: string;
|
|
92
|
+
deviceKeyMap: DeviceKeyMap;
|
|
93
|
+
senderPublicKey: string;
|
|
94
|
+
isEncrypted: true;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Encrypt file content for the given recipient devices.
|
|
98
|
+
*/
|
|
99
|
+
export declare const encryptFileForDevices: (content: string | Buffer, recipients: DeviceRecipient[]) => Promise<{
|
|
100
|
+
ciphertext: Buffer;
|
|
101
|
+
iv: string;
|
|
102
|
+
deviceKeyMap: DeviceKeyMap;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* The label a sealed REST command carries inside its body (M5 D8): which
|
|
106
|
+
* session on which machine it is for, and a number that only grows per
|
|
107
|
+
* sender. A sealed body without one is not a command. `from` is the id of the
|
|
108
|
+
* computer that sent it, when it names itself.
|
|
109
|
+
*/
|
|
110
|
+
export interface SealedCommandLabel {
|
|
111
|
+
n: number;
|
|
112
|
+
session: string;
|
|
113
|
+
device: string;
|
|
114
|
+
from?: string;
|
|
115
|
+
}
|
|
116
|
+
/** One button of a sealed question: `id` and `style` also reach the server in the clear, `label` never does. */
|
|
117
|
+
export interface SealedAskAction {
|
|
118
|
+
id: string;
|
|
119
|
+
label: string;
|
|
120
|
+
style?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The label a sealed question carries inside its body: the asker's own random
|
|
124
|
+
* `id` (the answer must echo it, so a relay cannot pass an old question's answer
|
|
125
|
+
* off as a new one's), the Machine Device that asks, and the wording the server
|
|
126
|
+
* must not read.
|
|
127
|
+
*/
|
|
128
|
+
export interface SealedAskLabel {
|
|
129
|
+
id: string;
|
|
130
|
+
device: string;
|
|
131
|
+
actions?: SealedAskAction[];
|
|
132
|
+
placeholder?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The summary a sealed question may carry in its body under `context` (F3): what the asking session has changed
|
|
136
|
+
* and how much of its screen the original holds. It is a new key of the same plaintext JSON — the envelope and
|
|
137
|
+
* the schema are unchanged, and a reader that does not know the key drops it with the rest of the keys it does
|
|
138
|
+
* not read. The original (the masked diff and screen) is never in the body: it is the question's sealed attachment.
|
|
139
|
+
*/
|
|
140
|
+
export interface SealedAskContext {
|
|
141
|
+
diff: 'ok' | 'no_repo' | 'failed';
|
|
142
|
+
files: number;
|
|
143
|
+
added: number;
|
|
144
|
+
removed: number;
|
|
145
|
+
listed: {
|
|
146
|
+
path: string;
|
|
147
|
+
status: 'M' | 'A' | 'D' | '?';
|
|
148
|
+
added: number;
|
|
149
|
+
removed: number;
|
|
150
|
+
binary?: true;
|
|
151
|
+
}[];
|
|
152
|
+
moreFiles: number;
|
|
153
|
+
screenLines: number;
|
|
154
|
+
masked: number;
|
|
155
|
+
cut: {
|
|
156
|
+
diff: boolean;
|
|
157
|
+
screen: boolean;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** The label a sealed answer carries: which question it answers and what was chosen. */
|
|
161
|
+
export interface SealedAnswerLabel {
|
|
162
|
+
ask: string;
|
|
163
|
+
actionId?: string;
|
|
164
|
+
exit?: true;
|
|
165
|
+
}
|
|
166
|
+
/** What `encryptPushBodyForDevices` seals. Keys left out are left out of the plaintext JSON. */
|
|
167
|
+
export interface PushBodyInput {
|
|
168
|
+
title?: string;
|
|
169
|
+
body?: string;
|
|
170
|
+
url?: string;
|
|
171
|
+
cmd?: SealedCommandLabel;
|
|
172
|
+
ask?: SealedAskLabel;
|
|
173
|
+
answer?: SealedAnswerLabel;
|
|
174
|
+
files?: SealedFileMeta[];
|
|
175
|
+
/**
|
|
176
|
+
* Named keys (the blocked prompt's answer) inside a sealed command. Only
|
|
177
|
+
* non-empty string arrays read back; the listener allowlists the names.
|
|
178
|
+
*/
|
|
179
|
+
keys?: string[];
|
|
180
|
+
/** The blocked prompt id the keys answer, inside a sealed command. */
|
|
181
|
+
ifPrompt?: string;
|
|
182
|
+
context?: SealedAskContext;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* One sealed attachment as the sealed body names it (M5): its IV — fresh per
|
|
186
|
+
* file, so an attachment from another message does not match — and what it
|
|
187
|
+
* really is. The list binds the attachments to the message, in order.
|
|
188
|
+
*/
|
|
189
|
+
export interface SealedFileMeta {
|
|
190
|
+
iv: string;
|
|
191
|
+
name: string;
|
|
192
|
+
type: string;
|
|
193
|
+
size: number;
|
|
194
|
+
}
|
|
195
|
+
/** The `code` of the errors above that a shorter choice list fixes. */
|
|
196
|
+
export declare const ASK_CHOICES_CODE = "ask-choices-over-limits";
|
|
197
|
+
/** The `context` summary of a sealed question, or `undefined` when it is absent or not well formed (the question still opens without it). */
|
|
198
|
+
export declare const askContext: (value: unknown) => SealedAskContext | undefined;
|
|
199
|
+
/**
|
|
200
|
+
* Decrypt a push body sealed by `encryptPushBodyForDevices` (any client's
|
|
201
|
+
* twin of it) for this device: `body` is the JSON `{ ciphertext, iv }`
|
|
202
|
+
* envelope, the message key comes out of this device's `deviceKeyMap` slot.
|
|
203
|
+
* Returns the `{ title, body, url }` the sender put in, the `cmd` label of a
|
|
204
|
+
* sealed command, the `ask` / `answer` label of a sealed question / answer and
|
|
205
|
+
* the `files` list and the `context` summary — each only when well formed. `context` is read from any body;
|
|
206
|
+
* whether it belongs there (a question) is the caller's to decide.
|
|
207
|
+
*/
|
|
208
|
+
export declare const decryptPushBodyForDevice: (body: string, entryJson: string, senderPublicKeyRaw: string) => Promise<{
|
|
209
|
+
title?: string;
|
|
210
|
+
body?: string;
|
|
211
|
+
url?: string;
|
|
212
|
+
cmd?: SealedCommandLabel;
|
|
213
|
+
ask?: SealedAskLabel;
|
|
214
|
+
answer?: SealedAnswerLabel;
|
|
215
|
+
files?: SealedFileMeta[];
|
|
216
|
+
keys?: string[];
|
|
217
|
+
ifPrompt?: string;
|
|
218
|
+
context?: SealedAskContext;
|
|
219
|
+
}>;
|
|
220
|
+
/**
|
|
221
|
+
* Whether two strings are the same device public key (twin of the cli's
|
|
222
|
+
* `samePublicKey`). A device key has exactly one spelling — standard padded
|
|
223
|
+
* Base64 of the 91-byte SPKI with an uncompressed point, what WebCrypto's
|
|
224
|
+
* `exportKey('spki')` and the app write — so two keys are the same exactly
|
|
225
|
+
* when their strings are. A string in any other spelling equals nothing,
|
|
226
|
+
* itself included.
|
|
227
|
+
*/
|
|
228
|
+
export declare const samePublicKey: (a: string, b: string) => boolean;
|
|
229
|
+
/**
|
|
230
|
+
* The x coordinate of a device key's point, in hex (twin of the cli's `publicKeySender`): what the listener counts
|
|
231
|
+
* sealed command numbers by in the trust file's `commandKeys`. null for a string that is not a device key.
|
|
232
|
+
*/
|
|
233
|
+
export declare const publicKeySender: (spkiBase64: string) => string | null;
|
|
234
|
+
//# sourceMappingURL=crypto.d.ts.map
|