@wolpertingerlabs/drawlatch 1.0.0-alpha.1
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/CONNECTIONS.md +197 -0
- package/INGESTORS.md +790 -0
- package/LICENSE +21 -0
- package/README.md +685 -0
- package/dist/cli/generate-keys.d.ts +15 -0
- package/dist/cli/generate-keys.js +107 -0
- package/dist/connections/anthropic.json +16 -0
- package/dist/connections/bluesky.json +26 -0
- package/dist/connections/devin.json +15 -0
- package/dist/connections/discord-bot.json +24 -0
- package/dist/connections/discord-oauth.json +16 -0
- package/dist/connections/github.json +25 -0
- package/dist/connections/google-ai.json +15 -0
- package/dist/connections/google.json +28 -0
- package/dist/connections/hex.json +14 -0
- package/dist/connections/lichess.json +15 -0
- package/dist/connections/linear.json +29 -0
- package/dist/connections/mastodon.json +25 -0
- package/dist/connections/notion.json +33 -0
- package/dist/connections/openai.json +16 -0
- package/dist/connections/openrouter.json +16 -0
- package/dist/connections/reddit.json +28 -0
- package/dist/connections/slack.json +23 -0
- package/dist/connections/stripe.json +25 -0
- package/dist/connections/telegram.json +26 -0
- package/dist/connections/trello.json +25 -0
- package/dist/connections/twitch.json +28 -0
- package/dist/connections/x.json +27 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +258 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +65 -0
- package/dist/remote/ingestors/base-ingestor.js +132 -0
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +58 -0
- package/dist/remote/ingestors/discord/discord-gateway.js +341 -0
- package/dist/remote/ingestors/discord/index.d.ts +3 -0
- package/dist/remote/ingestors/discord/index.js +3 -0
- package/dist/remote/ingestors/discord/types.d.ts +56 -0
- package/dist/remote/ingestors/discord/types.js +68 -0
- package/dist/remote/ingestors/index.d.ts +16 -0
- package/dist/remote/ingestors/index.js +20 -0
- package/dist/remote/ingestors/manager.d.ts +65 -0
- package/dist/remote/ingestors/manager.js +201 -0
- package/dist/remote/ingestors/poll/index.d.ts +2 -0
- package/dist/remote/ingestors/poll/index.js +2 -0
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +78 -0
- package/dist/remote/ingestors/poll/poll-ingestor.js +283 -0
- package/dist/remote/ingestors/registry.d.ts +32 -0
- package/dist/remote/ingestors/registry.js +46 -0
- package/dist/remote/ingestors/ring-buffer.d.ts +33 -0
- package/dist/remote/ingestors/ring-buffer.js +62 -0
- package/dist/remote/ingestors/slack/index.d.ts +3 -0
- package/dist/remote/ingestors/slack/index.js +3 -0
- package/dist/remote/ingestors/slack/socket-mode.d.ts +48 -0
- package/dist/remote/ingestors/slack/socket-mode.js +267 -0
- package/dist/remote/ingestors/slack/types.d.ts +70 -0
- package/dist/remote/ingestors/slack/types.js +72 -0
- package/dist/remote/ingestors/types.d.ts +138 -0
- package/dist/remote/ingestors/types.js +13 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +112 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +119 -0
- package/dist/remote/ingestors/webhook/github-types.d.ts +45 -0
- package/dist/remote/ingestors/webhook/github-types.js +65 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +43 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +86 -0
- package/dist/remote/ingestors/webhook/index.d.ts +8 -0
- package/dist/remote/ingestors/webhook/index.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-types.d.ts +57 -0
- package/dist/remote/ingestors/webhook/stripe-types.js +108 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.d.ts +47 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +90 -0
- package/dist/remote/ingestors/webhook/trello-types.d.ts +90 -0
- package/dist/remote/ingestors/webhook/trello-types.js +81 -0
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +60 -0
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +126 -0
- package/dist/remote/server.d.ts +103 -0
- package/dist/remote/server.js +536 -0
- package/dist/shared/config.d.ts +213 -0
- package/dist/shared/config.js +269 -0
- package/dist/shared/connections.d.ts +72 -0
- package/dist/shared/connections.js +103 -0
- package/dist/shared/crypto/channel.d.ts +95 -0
- package/dist/shared/crypto/channel.js +175 -0
- package/dist/shared/crypto/index.d.ts +3 -0
- package/dist/shared/crypto/index.js +3 -0
- package/dist/shared/crypto/keys.d.ts +92 -0
- package/dist/shared/crypto/keys.js +143 -0
- package/dist/shared/logger.d.ts +30 -0
- package/dist/shared/logger.js +74 -0
- package/dist/shared/protocol/handshake.d.ts +116 -0
- package/dist/shared/protocol/handshake.js +214 -0
- package/dist/shared/protocol/index.d.ts +3 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/messages.d.ts +46 -0
- package/dist/shared/protocol/messages.js +8 -0
- package/package.json +105 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutual authentication handshake protocol.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by the Noise NK pattern — the responder's (remote server's) public
|
|
5
|
+
* key is known in advance. The initiator (MCP proxy) proves its identity by
|
|
6
|
+
* signing a challenge, and both sides derive a shared secret via X25519 ECDH.
|
|
7
|
+
*
|
|
8
|
+
* Protocol flow:
|
|
9
|
+
*
|
|
10
|
+
* Initiator (MCP Proxy) Responder (Remote Server)
|
|
11
|
+
* ────────────────────── ────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 1. Generate ephemeral X25519 keypair
|
|
14
|
+
* Sign(ephemeral_pub || nonce_i) with Ed25519
|
|
15
|
+
* ──── HandshakeInit ────────────────►
|
|
16
|
+
* 2. Verify initiator's signature
|
|
17
|
+
* Check initiator's pubkey is authorized
|
|
18
|
+
* Generate ephemeral X25519 keypair
|
|
19
|
+
* Sign(ephemeral_pub || nonce_r || nonce_i)
|
|
20
|
+
* ◄──── HandshakeReply ──────
|
|
21
|
+
* 3. Verify responder's signature
|
|
22
|
+
* Both: ECDH(ephemeral_i, ephemeral_r) → shared secret
|
|
23
|
+
* Both: HKDF(shared_secret, transcript_hash) → session keys
|
|
24
|
+
* ──── HandshakeFinish (encrypted "ready") ──►
|
|
25
|
+
* 4. Decrypt and verify "ready"
|
|
26
|
+
* Session established ✓
|
|
27
|
+
*
|
|
28
|
+
* The handshake transcript (all messages concatenated) is hashed and used as
|
|
29
|
+
* HKDF salt, binding the session keys to the exact handshake that occurred.
|
|
30
|
+
* This prevents unknown-key-share attacks.
|
|
31
|
+
*/
|
|
32
|
+
import { type KeyBundle, type PublicKeyBundle, type SessionKeys } from '../crypto/index.js';
|
|
33
|
+
export interface HandshakeInit {
|
|
34
|
+
type: 'handshake_init';
|
|
35
|
+
/** Initiator's static Ed25519 public key (PEM) */
|
|
36
|
+
signingPubKey: string;
|
|
37
|
+
/** Initiator's ephemeral X25519 public key (PEM) */
|
|
38
|
+
ephemeralPubKey: string;
|
|
39
|
+
/** Random nonce (32 bytes, hex) */
|
|
40
|
+
nonceI: string;
|
|
41
|
+
/** Ed25519 signature over (ephemeralPubKey || nonceI) */
|
|
42
|
+
signature: string;
|
|
43
|
+
/** Protocol version */
|
|
44
|
+
version: 1;
|
|
45
|
+
}
|
|
46
|
+
export interface HandshakeReply {
|
|
47
|
+
type: 'handshake_reply';
|
|
48
|
+
/** Responder's ephemeral X25519 public key (PEM) */
|
|
49
|
+
ephemeralPubKey: string;
|
|
50
|
+
/** Random nonce (32 bytes, hex) */
|
|
51
|
+
nonceR: string;
|
|
52
|
+
/** Ed25519 signature over (ephemeralPubKey || nonceR || nonceI) */
|
|
53
|
+
signature: string;
|
|
54
|
+
}
|
|
55
|
+
export interface HandshakeFinish {
|
|
56
|
+
type: 'handshake_finish';
|
|
57
|
+
/** Encrypted "ready" payload — proves the initiator derived the right keys */
|
|
58
|
+
payload: string;
|
|
59
|
+
}
|
|
60
|
+
export type HandshakeMessage = HandshakeInit | HandshakeReply | HandshakeFinish;
|
|
61
|
+
export declare class HandshakeInitiator {
|
|
62
|
+
/** Our full key bundle */
|
|
63
|
+
private readonly ownKeys;
|
|
64
|
+
/** The remote server's known public keys */
|
|
65
|
+
private readonly peerPublicKeys;
|
|
66
|
+
private ephemeral;
|
|
67
|
+
private nonceI;
|
|
68
|
+
private transcript;
|
|
69
|
+
constructor(
|
|
70
|
+
/** Our full key bundle */
|
|
71
|
+
ownKeys: KeyBundle,
|
|
72
|
+
/** The remote server's known public keys */
|
|
73
|
+
peerPublicKeys: PublicKeyBundle);
|
|
74
|
+
/**
|
|
75
|
+
* Step 1: Create the initial handshake message.
|
|
76
|
+
*/
|
|
77
|
+
createInit(): HandshakeInit;
|
|
78
|
+
/**
|
|
79
|
+
* Step 3: Process the responder's reply and derive session keys.
|
|
80
|
+
*/
|
|
81
|
+
processReply(reply: HandshakeReply): SessionKeys;
|
|
82
|
+
/**
|
|
83
|
+
* Create the finish message (encrypted with the newly derived keys).
|
|
84
|
+
*/
|
|
85
|
+
createFinish(keys: SessionKeys): HandshakeFinish;
|
|
86
|
+
}
|
|
87
|
+
export declare class HandshakeResponder {
|
|
88
|
+
/** Our full key bundle */
|
|
89
|
+
private readonly ownKeys;
|
|
90
|
+
/** Set of authorized initiator public keys */
|
|
91
|
+
private readonly authorizedKeys;
|
|
92
|
+
private ephemeral;
|
|
93
|
+
private transcript;
|
|
94
|
+
constructor(
|
|
95
|
+
/** Our full key bundle */
|
|
96
|
+
ownKeys: KeyBundle,
|
|
97
|
+
/** Set of authorized initiator public keys */
|
|
98
|
+
authorizedKeys: PublicKeyBundle[]);
|
|
99
|
+
/**
|
|
100
|
+
* Step 2: Process the init message, verify the initiator, and create a reply.
|
|
101
|
+
*/
|
|
102
|
+
processInit(init: HandshakeInit): {
|
|
103
|
+
reply: HandshakeReply;
|
|
104
|
+
initiatorPubKey: PublicKeyBundle;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Derive session keys after sending the reply.
|
|
108
|
+
* Call this after processInit() and before verifying the finish message.
|
|
109
|
+
*/
|
|
110
|
+
deriveKeys(init: HandshakeInit): SessionKeys;
|
|
111
|
+
/**
|
|
112
|
+
* Step 4: Verify the finish message to confirm the initiator derived the right keys.
|
|
113
|
+
*/
|
|
114
|
+
verifyFinish(finish: HandshakeFinish, keys: SessionKeys): boolean;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=handshake.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutual authentication handshake protocol.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by the Noise NK pattern — the responder's (remote server's) public
|
|
5
|
+
* key is known in advance. The initiator (MCP proxy) proves its identity by
|
|
6
|
+
* signing a challenge, and both sides derive a shared secret via X25519 ECDH.
|
|
7
|
+
*
|
|
8
|
+
* Protocol flow:
|
|
9
|
+
*
|
|
10
|
+
* Initiator (MCP Proxy) Responder (Remote Server)
|
|
11
|
+
* ────────────────────── ────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 1. Generate ephemeral X25519 keypair
|
|
14
|
+
* Sign(ephemeral_pub || nonce_i) with Ed25519
|
|
15
|
+
* ──── HandshakeInit ────────────────►
|
|
16
|
+
* 2. Verify initiator's signature
|
|
17
|
+
* Check initiator's pubkey is authorized
|
|
18
|
+
* Generate ephemeral X25519 keypair
|
|
19
|
+
* Sign(ephemeral_pub || nonce_r || nonce_i)
|
|
20
|
+
* ◄──── HandshakeReply ──────
|
|
21
|
+
* 3. Verify responder's signature
|
|
22
|
+
* Both: ECDH(ephemeral_i, ephemeral_r) → shared secret
|
|
23
|
+
* Both: HKDF(shared_secret, transcript_hash) → session keys
|
|
24
|
+
* ──── HandshakeFinish (encrypted "ready") ──►
|
|
25
|
+
* 4. Decrypt and verify "ready"
|
|
26
|
+
* Session established ✓
|
|
27
|
+
*
|
|
28
|
+
* The handshake transcript (all messages concatenated) is hashed and used as
|
|
29
|
+
* HKDF salt, binding the session keys to the exact handshake that occurred.
|
|
30
|
+
* This prevents unknown-key-share attacks.
|
|
31
|
+
*/
|
|
32
|
+
import crypto from 'node:crypto';
|
|
33
|
+
import { deriveSessionKeys, EncryptedChannel, } from '../crypto/index.js';
|
|
34
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
35
|
+
function signData(privateKey, ...parts) {
|
|
36
|
+
const combined = Buffer.concat(parts.map((p) => (typeof p === 'string' ? Buffer.from(p, 'utf-8') : p)));
|
|
37
|
+
return crypto.sign(null, combined, privateKey);
|
|
38
|
+
}
|
|
39
|
+
function verifySignature(publicKey, signature, ...parts) {
|
|
40
|
+
const combined = Buffer.concat(parts.map((p) => (typeof p === 'string' ? Buffer.from(p, 'utf-8') : p)));
|
|
41
|
+
return crypto.verify(null, combined, publicKey, signature);
|
|
42
|
+
}
|
|
43
|
+
// ── Initiator (MCP Proxy side) ─────────────────────────────────────────────
|
|
44
|
+
export class HandshakeInitiator {
|
|
45
|
+
ownKeys;
|
|
46
|
+
peerPublicKeys;
|
|
47
|
+
ephemeral;
|
|
48
|
+
nonceI;
|
|
49
|
+
transcript = [];
|
|
50
|
+
constructor(
|
|
51
|
+
/** Our full key bundle */
|
|
52
|
+
ownKeys,
|
|
53
|
+
/** The remote server's known public keys */
|
|
54
|
+
peerPublicKeys) {
|
|
55
|
+
this.ownKeys = ownKeys;
|
|
56
|
+
this.peerPublicKeys = peerPublicKeys;
|
|
57
|
+
this.ephemeral = crypto.generateKeyPairSync('x25519');
|
|
58
|
+
this.nonceI = crypto.randomBytes(32);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Step 1: Create the initial handshake message.
|
|
62
|
+
*/
|
|
63
|
+
createInit() {
|
|
64
|
+
const ephemeralPubPem = this.ephemeral.publicKey.export({
|
|
65
|
+
type: 'spki',
|
|
66
|
+
format: 'pem',
|
|
67
|
+
});
|
|
68
|
+
const signature = signData(this.ownKeys.signing.privateKey, ephemeralPubPem, this.nonceI);
|
|
69
|
+
const msg = {
|
|
70
|
+
type: 'handshake_init',
|
|
71
|
+
signingPubKey: this.ownKeys.signing.publicKey.export({
|
|
72
|
+
type: 'spki',
|
|
73
|
+
format: 'pem',
|
|
74
|
+
}),
|
|
75
|
+
ephemeralPubKey: ephemeralPubPem,
|
|
76
|
+
nonceI: this.nonceI.toString('hex'),
|
|
77
|
+
signature: signature.toString('hex'),
|
|
78
|
+
version: 1,
|
|
79
|
+
};
|
|
80
|
+
// Record in transcript
|
|
81
|
+
this.transcript.push(Buffer.from(JSON.stringify(msg), 'utf-8'));
|
|
82
|
+
return msg;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Step 3: Process the responder's reply and derive session keys.
|
|
86
|
+
*/
|
|
87
|
+
processReply(reply) {
|
|
88
|
+
// Record in transcript
|
|
89
|
+
this.transcript.push(Buffer.from(JSON.stringify(reply), 'utf-8'));
|
|
90
|
+
// Verify responder's signature over (ephemeralPubKey || nonceR || nonceI)
|
|
91
|
+
const sigValid = verifySignature(this.peerPublicKeys.signing, Buffer.from(reply.signature, 'hex'), reply.ephemeralPubKey, Buffer.from(reply.nonceR, 'hex'), this.nonceI);
|
|
92
|
+
if (!sigValid) {
|
|
93
|
+
throw new Error('Handshake failed: responder signature invalid');
|
|
94
|
+
}
|
|
95
|
+
// ECDH to derive shared secret
|
|
96
|
+
const peerEphemeral = crypto.createPublicKey(reply.ephemeralPubKey);
|
|
97
|
+
const sharedSecret = crypto.diffieHellman({
|
|
98
|
+
privateKey: this.ephemeral.privateKey,
|
|
99
|
+
publicKey: peerEphemeral,
|
|
100
|
+
});
|
|
101
|
+
// Hash the full transcript
|
|
102
|
+
const transcriptHash = crypto
|
|
103
|
+
.createHash('sha256')
|
|
104
|
+
.update(Buffer.concat(this.transcript))
|
|
105
|
+
.digest();
|
|
106
|
+
return deriveSessionKeys(sharedSecret, true, transcriptHash);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create the finish message (encrypted with the newly derived keys).
|
|
110
|
+
*/
|
|
111
|
+
createFinish(keys) {
|
|
112
|
+
const channel = new EncryptedChannel(keys);
|
|
113
|
+
const readyPayload = channel.encrypt(Buffer.from(JSON.stringify({ status: 'ready', timestamp: Date.now() }), 'utf-8'));
|
|
114
|
+
return {
|
|
115
|
+
type: 'handshake_finish',
|
|
116
|
+
payload: readyPayload.toString('hex'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ── Responder (Remote Server side) ──────────────────────────────────────────
|
|
121
|
+
export class HandshakeResponder {
|
|
122
|
+
ownKeys;
|
|
123
|
+
authorizedKeys;
|
|
124
|
+
ephemeral = null;
|
|
125
|
+
transcript = [];
|
|
126
|
+
constructor(
|
|
127
|
+
/** Our full key bundle */
|
|
128
|
+
ownKeys,
|
|
129
|
+
/** Set of authorized initiator public keys */
|
|
130
|
+
authorizedKeys) {
|
|
131
|
+
this.ownKeys = ownKeys;
|
|
132
|
+
this.authorizedKeys = authorizedKeys;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Step 2: Process the init message, verify the initiator, and create a reply.
|
|
136
|
+
*/
|
|
137
|
+
processInit(init) {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime validation for untrusted input regardless of static type
|
|
139
|
+
if (init.version !== 1) {
|
|
140
|
+
throw new Error(`Unsupported handshake version: ${String(init.version)}`);
|
|
141
|
+
}
|
|
142
|
+
// Record in transcript
|
|
143
|
+
this.transcript.push(Buffer.from(JSON.stringify(init), 'utf-8'));
|
|
144
|
+
// Parse the initiator's signing public key
|
|
145
|
+
const initiatorSigningKey = crypto.createPublicKey(init.signingPubKey);
|
|
146
|
+
// Check if this key is authorized
|
|
147
|
+
const authorized = this.authorizedKeys.find((ak) => {
|
|
148
|
+
const akPem = ak.signing.export({ type: 'spki', format: 'pem' });
|
|
149
|
+
return akPem === init.signingPubKey;
|
|
150
|
+
});
|
|
151
|
+
if (!authorized) {
|
|
152
|
+
throw new Error('Handshake failed: initiator not authorized');
|
|
153
|
+
}
|
|
154
|
+
// Verify initiator's signature over (ephemeralPubKey || nonceI)
|
|
155
|
+
const sigValid = verifySignature(initiatorSigningKey, Buffer.from(init.signature, 'hex'), init.ephemeralPubKey, Buffer.from(init.nonceI, 'hex'));
|
|
156
|
+
if (!sigValid) {
|
|
157
|
+
throw new Error('Handshake failed: initiator signature invalid');
|
|
158
|
+
}
|
|
159
|
+
// Generate our ephemeral keypair
|
|
160
|
+
this.ephemeral = crypto.generateKeyPairSync('x25519');
|
|
161
|
+
const ephemeralPubPem = this.ephemeral.publicKey.export({
|
|
162
|
+
type: 'spki',
|
|
163
|
+
format: 'pem',
|
|
164
|
+
});
|
|
165
|
+
const nonceR = crypto.randomBytes(32);
|
|
166
|
+
// Sign (ephemeralPubKey || nonceR || nonceI)
|
|
167
|
+
const signature = signData(this.ownKeys.signing.privateKey, ephemeralPubPem, nonceR, Buffer.from(init.nonceI, 'hex'));
|
|
168
|
+
const reply = {
|
|
169
|
+
type: 'handshake_reply',
|
|
170
|
+
ephemeralPubKey: ephemeralPubPem,
|
|
171
|
+
nonceR: nonceR.toString('hex'),
|
|
172
|
+
signature: signature.toString('hex'),
|
|
173
|
+
};
|
|
174
|
+
// Record in transcript
|
|
175
|
+
this.transcript.push(Buffer.from(JSON.stringify(reply), 'utf-8'));
|
|
176
|
+
return { reply, initiatorPubKey: authorized };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Derive session keys after sending the reply.
|
|
180
|
+
* Call this after processInit() and before verifying the finish message.
|
|
181
|
+
*/
|
|
182
|
+
deriveKeys(init) {
|
|
183
|
+
if (!this.ephemeral) {
|
|
184
|
+
throw new Error('Must call processInit() first');
|
|
185
|
+
}
|
|
186
|
+
// ECDH with initiator's ephemeral public key
|
|
187
|
+
const peerEphemeral = crypto.createPublicKey(init.ephemeralPubKey);
|
|
188
|
+
const sharedSecret = crypto.diffieHellman({
|
|
189
|
+
privateKey: this.ephemeral.privateKey,
|
|
190
|
+
publicKey: peerEphemeral,
|
|
191
|
+
});
|
|
192
|
+
// Hash the full transcript
|
|
193
|
+
const transcriptHash = crypto
|
|
194
|
+
.createHash('sha256')
|
|
195
|
+
.update(Buffer.concat(this.transcript))
|
|
196
|
+
.digest();
|
|
197
|
+
return deriveSessionKeys(sharedSecret, false, transcriptHash);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Step 4: Verify the finish message to confirm the initiator derived the right keys.
|
|
201
|
+
*/
|
|
202
|
+
verifyFinish(finish, keys) {
|
|
203
|
+
const channel = new EncryptedChannel(keys);
|
|
204
|
+
try {
|
|
205
|
+
const payload = channel.decrypt(Buffer.from(finish.payload, 'hex'));
|
|
206
|
+
const parsed = JSON.parse(payload.toString('utf-8'));
|
|
207
|
+
return parsed.status === 'ready';
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type HandshakeInit, type HandshakeReply, type HandshakeFinish, type HandshakeMessage, HandshakeInitiator, HandshakeResponder, } from './handshake.js';
|
|
2
|
+
export { type ProxyRequest, type ProxyResponse, type PingMessage, type PongMessage, type AppMessage, } from './messages.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-layer message types exchanged over the encrypted channel.
|
|
3
|
+
*
|
|
4
|
+
* All messages are JSON-serialized, encrypted with AES-256-GCM, and sent
|
|
5
|
+
* as hex-encoded payloads over the HTTP transport.
|
|
6
|
+
*/
|
|
7
|
+
/** Request from MCP proxy → remote server */
|
|
8
|
+
export interface ProxyRequest {
|
|
9
|
+
type: 'proxy_request';
|
|
10
|
+
/** Unique request ID for correlation */
|
|
11
|
+
id: string;
|
|
12
|
+
/** The tool name the MCP client invoked */
|
|
13
|
+
toolName: string;
|
|
14
|
+
/** The tool's input parameters */
|
|
15
|
+
toolInput: Record<string, unknown>;
|
|
16
|
+
/** Timestamp (ms since epoch) */
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
/** Response from remote server → MCP proxy */
|
|
20
|
+
export interface ProxyResponse {
|
|
21
|
+
type: 'proxy_response';
|
|
22
|
+
/** Correlates to ProxyRequest.id */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Whether the operation succeeded */
|
|
25
|
+
success: boolean;
|
|
26
|
+
/** The result payload (tool output) */
|
|
27
|
+
result?: unknown;
|
|
28
|
+
/** Error message if success=false */
|
|
29
|
+
error?: string;
|
|
30
|
+
/** Timestamp */
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
/** Ping to keep the connection alive / verify the channel */
|
|
34
|
+
export interface PingMessage {
|
|
35
|
+
type: 'ping';
|
|
36
|
+
timestamp: number;
|
|
37
|
+
}
|
|
38
|
+
/** Pong response */
|
|
39
|
+
export interface PongMessage {
|
|
40
|
+
type: 'pong';
|
|
41
|
+
timestamp: number;
|
|
42
|
+
/** Echo back the ping timestamp for RTT measurement */
|
|
43
|
+
echoTimestamp: number;
|
|
44
|
+
}
|
|
45
|
+
export type AppMessage = ProxyRequest | ProxyResponse | PingMessage | PongMessage;
|
|
46
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-layer message types exchanged over the encrypted channel.
|
|
3
|
+
*
|
|
4
|
+
* All messages are JSON-serialized, encrypted with AES-256-GCM, and sent
|
|
5
|
+
* as hex-encoded payloads over the HTTP transport.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=messages.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wolpertingerlabs/drawlatch",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Encrypted MCP proxy with mutual authentication. Local MCP server forwards requests through an encrypted channel to a remote secrets-holding server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/mcp/server.js",
|
|
7
|
+
"types": "./dist/mcp/server.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"!dist/**/*.test.*",
|
|
11
|
+
"!dist/**/*.js.map",
|
|
12
|
+
"!dist/**/*.d.ts.map",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CONNECTIONS.md",
|
|
15
|
+
"INGESTORS.md"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/mcp/server.d.ts",
|
|
20
|
+
"import": "./dist/mcp/server.js"
|
|
21
|
+
},
|
|
22
|
+
"./shared/crypto": {
|
|
23
|
+
"types": "./dist/shared/crypto/index.d.ts",
|
|
24
|
+
"import": "./dist/shared/crypto/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./shared/protocol": {
|
|
27
|
+
"types": "./dist/shared/protocol/index.d.ts",
|
|
28
|
+
"import": "./dist/shared/protocol/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./shared/config": {
|
|
31
|
+
"types": "./dist/shared/config.d.ts",
|
|
32
|
+
"import": "./dist/shared/config.js"
|
|
33
|
+
},
|
|
34
|
+
"./shared/connections": {
|
|
35
|
+
"types": "./dist/shared/connections.d.ts",
|
|
36
|
+
"import": "./dist/shared/connections.js"
|
|
37
|
+
},
|
|
38
|
+
"./remote/server": {
|
|
39
|
+
"types": "./dist/remote/server.d.ts",
|
|
40
|
+
"import": "./dist/remote/server.js"
|
|
41
|
+
},
|
|
42
|
+
"./remote/ingestors": {
|
|
43
|
+
"types": "./dist/remote/ingestors/index.d.ts",
|
|
44
|
+
"import": "./dist/remote/ingestors/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc && rm -rf dist/connections && cp -r src/connections dist/connections",
|
|
49
|
+
"prepare": "npm run build",
|
|
50
|
+
"dev:remote": "tsx src/remote/server.ts",
|
|
51
|
+
"dev:mcp": "tsx src/mcp/server.ts",
|
|
52
|
+
"generate-keys": "tsx src/cli/generate-keys.ts",
|
|
53
|
+
"start:remote": "NODE_ENV=production node dist/remote/server.js",
|
|
54
|
+
"start:mcp": "node dist/mcp/server.js",
|
|
55
|
+
"redeploy:prod": "node start-server.js",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"lint": "eslint src/",
|
|
60
|
+
"lint:fix": "eslint src/ --fix",
|
|
61
|
+
"format": "prettier --write 'src/**/*.ts' '*.{json,ts,js}'",
|
|
62
|
+
"format:check": "prettier --check 'src/**/*.ts' '*.{json,ts,js}'",
|
|
63
|
+
"prepublishOnly": "npm run lint && npm test && npm run build",
|
|
64
|
+
"publish:dry-run": "npm publish --dry-run"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/WolpertingerLabs/drawlatch.git"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"keywords": [
|
|
77
|
+
"mcp",
|
|
78
|
+
"security",
|
|
79
|
+
"proxy",
|
|
80
|
+
"encryption",
|
|
81
|
+
"secrets"
|
|
82
|
+
],
|
|
83
|
+
"author": "",
|
|
84
|
+
"license": "MIT",
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
87
|
+
"dotenv": "^17.3.1",
|
|
88
|
+
"express": "^5.2.1",
|
|
89
|
+
"zod": "^4.3.6"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"@eslint/js": "^9.39.2",
|
|
93
|
+
"@types/express": "^5.0.6",
|
|
94
|
+
"@types/node": "^25.2.2",
|
|
95
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
96
|
+
"eslint": "^9.39.2",
|
|
97
|
+
"eslint-config-prettier": "^10.1.8",
|
|
98
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
99
|
+
"prettier": "^3.8.1",
|
|
100
|
+
"tsx": "^4.21.0",
|
|
101
|
+
"typescript": "^5.9.3",
|
|
102
|
+
"typescript-eslint": "^8.54.0",
|
|
103
|
+
"vitest": "^4.0.18"
|
|
104
|
+
}
|
|
105
|
+
}
|