granttap-mcp 0.8.63 → 0.8.64
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/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/ciphertext-recipient/README.md +9 -0
- package/packages/core/ciphertext-recipient/index.ts +6 -0
- package/packages/core/relay-client-types.ts +2 -0
- package/packages/core/relay-client.ts +14 -4
- package/packages/core/relay-envelope.ts +2 -0
- package/packages/protocol/schema.ts +1 -0
package/cursor-plugin/README.md
CHANGED
|
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
|
|
|
10
10
|
Install the supported CLI on this computer, then run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g granttap-mcp@0.8.
|
|
13
|
+
npm install -g granttap-mcp@0.8.64
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": [
|
|
6
6
|
"-e",
|
|
7
|
-
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.
|
|
7
|
+
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.64\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
|
|
|
7
7
|
const { existsSync } = require("node:fs");
|
|
8
8
|
const { delimiter, dirname, join } = require("node:path");
|
|
9
9
|
|
|
10
|
-
const PACKAGE = "granttap-mcp@0.8.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.64";
|
|
11
11
|
const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
|
|
12
12
|
const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
|
|
13
13
|
? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Ciphertext recipient routing
|
|
2
|
+
|
|
3
|
+
`publicKeyIdentifier` identifies the recipient of one encrypted envelope without
|
|
4
|
+
exposing content or private keys. RelayClient negotiates this optional routing
|
|
5
|
+
with the relay; unsupported older/self-hosted relays retain the original wire.
|
|
6
|
+
A controller advertises its own key digest during the authenticated upgrade.
|
|
7
|
+
The relay filters live and retained ciphertext for that recipient; each client
|
|
8
|
+
still authenticates the complete encrypted payload independently. Test coverage
|
|
9
|
+
lives in `tests/`. This module uses the repository Apache License 2.0.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/** Only a routing hint. Ciphertext authentication remains the authorization boundary. */
|
|
4
|
+
export function publicKeyIdentifier(publicKey: string): string {
|
|
5
|
+
return createHash('sha256').update(Buffer.from(publicKey, 'base64')).digest('hex');
|
|
6
|
+
}
|
|
@@ -43,6 +43,8 @@ export type RelayClientOptions = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
export type SendOptions = {
|
|
46
|
+
/** Set by negotiated RelayClient transport; absent for older relays. */
|
|
47
|
+
recipientRouting?: boolean;
|
|
46
48
|
/** Reply only to the authenticated requester; absent means all authorized peers. */
|
|
47
49
|
peerPublicKey?: string;
|
|
48
50
|
/** Delivery lifetime for relay hold queues. Omit only for non-expiring hello packets. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* the Cloudflare Durable Object deployment.
|
|
11
11
|
*/
|
|
12
12
|
import WebSocket from "ws";
|
|
13
|
+
import { publicKeyIdentifier } from "./ciphertext-recipient";
|
|
13
14
|
import { ManagedRelayLease, RelaySocketHealth } from "./socket-lifecycle";
|
|
14
15
|
import { OutboundFrameQueue, sendEncryptedPayload } from "./outbound-scheduler";
|
|
15
16
|
import { createHash } from "node:crypto";
|
|
@@ -26,6 +27,7 @@ type Listener = (p: Payload, peerPublicKey: string) => boolean | void | Promise<
|
|
|
26
27
|
|
|
27
28
|
export class RelayClient {
|
|
28
29
|
private ws?: WebSocket;
|
|
30
|
+
private recipientRouting = false;
|
|
29
31
|
private frames?: OutboundFrameQueue;
|
|
30
32
|
private listeners = new Set<Listener>();
|
|
31
33
|
private intentionalClose = false;
|
|
@@ -74,6 +76,7 @@ export class RelayClient {
|
|
|
74
76
|
if (this.connectPromise) return this.connectPromise;
|
|
75
77
|
|
|
76
78
|
this.intentionalClose = false;
|
|
79
|
+
this.recipientRouting = false;
|
|
77
80
|
const pending = Promise.resolve().then(() => this.opts.upgradeHeaders?.() ?? {})
|
|
78
81
|
.then(headers => new Promise<void>((resolve, reject) => {
|
|
79
82
|
// The room rides in the URL: the Cloudflare relay must pick a Durable
|
|
@@ -82,7 +85,8 @@ export class RelayClient {
|
|
|
82
85
|
url.searchParams.set("room", this.cfg.room);
|
|
83
86
|
const ws = new WebSocket(
|
|
84
87
|
url.toString(),
|
|
85
|
-
{ headers: { ...headers,
|
|
88
|
+
{ headers: { ...headers, "X-GrantTap-Recipient-Routing": "1",
|
|
89
|
+
...(this.cfg.role === "phone" ? { "X-GrantTap-Recipient-Key-Id": publicKeyIdentifier(this.cfg.myPublicKey) } : {}),
|
|
86
90
|
...(this.cfg.pushAuth ? { Authorization: `Bearer ${this.cfg.pushAuth}` } : {}) } },
|
|
87
91
|
);
|
|
88
92
|
this.ws = ws;
|
|
@@ -135,10 +139,16 @@ export class RelayClient {
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
private async onRaw(raw: string): Promise<void> {
|
|
138
|
-
const
|
|
142
|
+
const control = safeJson(raw) as { type?: string; recipientRouting?: boolean } | null;
|
|
143
|
+
if (control?.type === "relay.capabilities" && control.recipientRouting === true) {
|
|
144
|
+
this.recipientRouting = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const parsed = Envelope.safeParse(control);
|
|
139
148
|
if (!parsed.success) return;
|
|
140
149
|
const env = parsed.data;
|
|
141
150
|
if (env.room !== this.cfg.room) return;
|
|
151
|
+
if (env.recipientKeyId && env.recipientKeyId !== publicKeyIdentifier(this.cfg.myPublicKey)) return;
|
|
142
152
|
if (env.from !== this.otherRole()) return;
|
|
143
153
|
if (env.to !== this.cfg.role && env.to !== "all") return;
|
|
144
154
|
if (env.expiresAt != null && env.expiresAt <= Date.now()) return;
|
|
@@ -239,7 +249,7 @@ export class RelayClient {
|
|
|
239
249
|
return;
|
|
240
250
|
}
|
|
241
251
|
await sendEncryptedPayload(this.frames, this.cfg, key => this.peerIsAllowed(key),
|
|
242
|
-
{ payload, to, options });
|
|
252
|
+
{ payload, to, options: { ...options, recipientRouting: this.recipientRouting } });
|
|
243
253
|
}
|
|
244
254
|
|
|
245
255
|
/** Add a newly issued controller without waiting for the monitor to restart. */
|
|
@@ -261,7 +271,7 @@ export class RelayClient {
|
|
|
261
271
|
if (!peerPublicKeys(this.cfg.peerPublicKey, this.cfg.extraPeerPublicKeys).includes(peerPublicKey)
|
|
262
272
|
|| !this.peerIsAllowed(peerPublicKey)) throw new Error("controller peer is not authorized");
|
|
263
273
|
if (!this.frames) throw new Error("relay not connected");
|
|
264
|
-
await this.frames.send(sealEnvelope({ ...this.cfg, peerPublicKey }, payload, this.otherRole(), options), true);
|
|
274
|
+
await this.frames.send(sealEnvelope({ ...this.cfg, peerPublicKey }, payload, this.otherRole(), { ...options, recipientRouting: this.recipientRouting }), true);
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
setSessionKey(sessionId: string, key: string): void {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Envelope, PROTOCOL_VERSION, type Payload, type Role } from "../protocol/schema";
|
|
3
|
+
import { publicKeyIdentifier } from "./ciphertext-recipient";
|
|
3
4
|
import { seal } from "./crypto";
|
|
4
5
|
import type { PeerConfig, SendOptions } from "./relay-client-types";
|
|
5
6
|
|
|
@@ -25,6 +26,7 @@ export function sealEnvelope(
|
|
|
25
26
|
from: cfg.role,
|
|
26
27
|
to,
|
|
27
28
|
senderId: cfg.senderId,
|
|
29
|
+
recipientKeyId: options.recipientRouting ? publicKeyIdentifier(cfg.peerPublicKey) : undefined,
|
|
28
30
|
deliveryId: options.reliable === false
|
|
29
31
|
? undefined
|
|
30
32
|
: options.deliveryId ?? randomUUID(),
|
|
@@ -190,6 +190,7 @@ export type Payload = z.infer<typeof Payload>;
|
|
|
190
190
|
|
|
191
191
|
/** The routed unit. `nonce`+`box` are the sealed Payload; relay cannot open it. */
|
|
192
192
|
export const Envelope = z.object({
|
|
193
|
+
recipientKeyId: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
193
194
|
v: z.literal(PROTOCOL_VERSION),
|
|
194
195
|
room: z.string(),
|
|
195
196
|
from: Role,
|