kojee-mcp 0.5.2 → 0.5.4
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 +117 -0
- package/dist/chunk-2MIISF2W.js +35 -0
- package/dist/chunk-36L3GCU3.js +213 -0
- package/dist/{chunk-YEC7IHIG.js → chunk-62KH6VNQ.js} +49 -381
- package/dist/{chunk-ZW4SW7LJ.js → chunk-64EOLZNI.js} +14 -5
- package/dist/chunk-HIZ4NDWN.js +141 -0
- package/dist/chunk-LDZXU3DW.js +170 -0
- package/dist/chunk-OSKHA5DS.js +185 -0
- package/dist/{chunk-C6GZ2L2W.js → chunk-X672ZN7V.js} +5 -2
- package/dist/{chunk-WBMX4CHB.js → chunk-YVUXQ4Z2.js} +4 -32
- package/dist/cli.js +37 -9
- package/dist/{codex-stop-hook-JOTBCS5K.js → codex-stop-hook-SWA53ECG.js} +1 -1
- package/dist/control-token-TYDAL477.js +35 -0
- package/dist/{doctor-TSHOMT5X.js → doctor-TXWMMSRC.js} +2 -2
- package/dist/{doctor-codex-BMI5JOO6.js → doctor-codex-3A7KYOVX.js} +10 -3
- package/dist/{hook-server-QF5JVUHV.js → hook-server-NDJSV22J.js} +85 -0
- package/dist/index.js +6 -3
- package/dist/send-cli-7QJ36YY7.js +72 -0
- package/dist/{stop-hook-SEPWWETV.js → stop-hook-GO363SMD.js} +1 -1
- package/dist/{tail-stream-BYKO4DW6.js → tail-stream-U436QL2X.js} +2 -1
- package/dist/webhook-config-UKUSI2FE.js +20 -0
- package/dist/{webhook-sink-7OYZBWXA.js → webhook-sink-GCLL2S6S.js} +12 -3
- package/dist/{wizard-7KHD5JT4.js → wizard-Z5JA3YPV.js} +63 -26
- package/package.json +1 -1
- package/dist/chunk-F7L25L2J.js +0 -60
- package/dist/webhook-config-5TLLX7RA.js +0 -10
package/README.md
CHANGED
|
@@ -220,6 +220,35 @@ in it is Hermes-specific — and it is **OFF by default**.
|
|
|
220
220
|
| `KOJEE_WEBHOOK_SECRET` | HMAC-SHA256 key for the signature header. URL set but secret unset ⇒ sink **DISABLED with an error** (the proxy NEVER sends unsigned webhooks). |
|
|
221
221
|
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `5000`). |
|
|
222
222
|
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a retryable failure — network / 5xx / 408 / 429 (default `4`). |
|
|
223
|
+
| `KOJEE_WEBHOOK_SIGNATURE_HEADER` | Header name carrying the signature (default `X-Kojee-Signature`). |
|
|
224
|
+
| `KOJEE_WEBHOOK_SIGNATURE_PREFIX` | Literal string prepended to the hex digest (default empty — bare hex). |
|
|
225
|
+
| `KOJEE_WEBHOOK_SIGNATURE_FORMAT` | Optional preset. `github` ⇒ header `X-Hub-Signature-256`, prefix `sha256=` (the GitHub-webhook convention). Explicit `_HEADER`/`_PREFIX` vars override the preset's corresponding value. Unknown values are **warned about once and ignored** — never fatal. |
|
|
226
|
+
|
|
227
|
+
**Signature emission is configurable (0.5.3)** — the HMAC *computation* never
|
|
228
|
+
changes (hex SHA-256 HMAC of the raw body bytes, keyed by
|
|
229
|
+
`KOJEE_WEBHOOK_SECRET`); only the header name and an optional digest prefix do.
|
|
230
|
+
Defaults are byte-identical to 0.5.2. For a GitHub-convention receiver (Hermes
|
|
231
|
+
and most off-the-shelf webhook verifiers):
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
export KOJEE_WEBHOOK_SIGNATURE_FORMAT=github
|
|
235
|
+
# Each POST then carries:
|
|
236
|
+
# X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body, keyed by KOJEE_WEBHOOK_SECRET>
|
|
237
|
+
# which verifies with any GitHub-style check, e.g.:
|
|
238
|
+
# expected = "sha256=" + HMAC_SHA256_hex(secret, raw_body)
|
|
239
|
+
# timing_safe_equal(header, expected)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or set the pieces individually (these override the preset):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export KOJEE_WEBHOOK_SIGNATURE_HEADER="X-Hub-Signature-256"
|
|
246
|
+
export KOJEE_WEBHOOK_SIGNATURE_PREFIX="sha256="
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The wizard can persist this non-interactively, e.g.
|
|
250
|
+
`kojee-mcp init --runtime hermes --webhook-url https://… --webhook-signature-format github`
|
|
251
|
+
(also `--webhook-signature-header` / `--webhook-signature-prefix`).
|
|
223
252
|
|
|
224
253
|
**Receiver contract** (the single source of truth is `buildWebhookReceiverNote()`
|
|
225
254
|
in `src/tandem/recipe.ts`, and the exact body shape is the `WEBHOOK_BODY_SHAPE`
|
|
@@ -251,6 +280,94 @@ never delay or break the Monitor (event-log) or Channel wake paths. The status
|
|
|
251
280
|
log redacts the secret and strips any basic-auth credentials embedded in
|
|
252
281
|
`KOJEE_WEBHOOK_URL`.
|
|
253
282
|
|
|
283
|
+
## Wake Continuity (0.5.4)
|
|
284
|
+
|
|
285
|
+
The event-stream subscription is a **connect-time snapshot of the caller's
|
|
286
|
+
memberships**. A daemon that rotates its session identity across restarts used
|
|
287
|
+
to come up with its tandem seat still bound to the OLD session — subscribed to
|
|
288
|
+
nothing, deaf to webhook wakes, while sends kept working (agent-scoped
|
|
289
|
+
fallback). Two mechanisms close this permanently:
|
|
290
|
+
|
|
291
|
+
**Ensure-join at startup.** After auth and *before* the event stream connects,
|
|
292
|
+
the daemon ensure-joins its live session (`tandem_join` is idempotent — an
|
|
293
|
+
existing seat returns `already_member`). One log line per tandem (`joined
|
|
294
|
+
fresh` vs `already seated`); failures warn and continue (a bad id never kills
|
|
295
|
+
the daemon).
|
|
296
|
+
|
|
297
|
+
| `KOJEE_TANDEMS` | Behavior |
|
|
298
|
+
|---|---|
|
|
299
|
+
| *(unset — the default)* | **Auto**: `tandem_list` (which is **principal-scoped** — it also lists rooms where only *sibling* agents of the principal sit), **filtered to rows where THIS agent holds an active seat** (`my_membership.is_member === true`; rows missing the flag are excluded — fail closed), then ensure-join each with the live session. Auto mode re-seats the agent where it already belongs; it never joins rooms the agent was not in. |
|
|
300
|
+
| `<id>,<id>,…` | Join exactly these tandem ObjectId hexes (24-hex; invalid entries warned + skipped). |
|
|
301
|
+
| `none` | Disable ensure-join entirely. |
|
|
302
|
+
|
|
303
|
+
**Stream reconnect after join.** Any successful `tandem_join` performed by the
|
|
304
|
+
daemon path (the startup ensure-join, or the MCP tool called through the proxy
|
|
305
|
+
by its agent) triggers a graceful event-stream reconnect (close + reconnect via
|
|
306
|
+
the existing backoff machinery, resuming from the per-room cursors), so the
|
|
307
|
+
subscription snapshot always includes just-acquired seats. Multiple joins in a
|
|
308
|
+
burst are debounced into one reconnect. No daemon restart needed, ever.
|
|
309
|
+
|
|
310
|
+
## Local Send Control Surface (0.5.4)
|
|
311
|
+
|
|
312
|
+
One stable, supported way to **send** a Tandem message from *outside* the proxy
|
|
313
|
+
process — for native gateway plugins, scripts, and humans. Two halves, one
|
|
314
|
+
shared core (`src/tandem/send.ts`), one envelope contract.
|
|
315
|
+
|
|
316
|
+
### CLI: `kojee-mcp send`
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
kojee-mcp send <tandem_id> --body "hello" [--reply-to <message_id>] [--kind message|status]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Uses the machine's **paired** credentials — the same `~/.kojee/config.json` +
|
|
323
|
+
`~/.kojee/keypair.json` the proxy reads, the same DPoP/GatewayClient path, the
|
|
324
|
+
same deterministic session seat. Prints **one JSON envelope** to stdout and
|
|
325
|
+
exits `0`/`1`:
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{"ok":true,"tandem_id":"T-1","message_id":"m_123","cursor":42,"text":"..."}
|
|
329
|
+
{"ok":false,"error":"content_blocked","message":"content blocked by gateway — ..."}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### HTTP: `POST /send` on the running daemon's hook-server
|
|
333
|
+
|
|
334
|
+
Lets a plugin riding a **running** daemon send without spawning Node. Find the
|
|
335
|
+
port and the bearer via the session-discovery file
|
|
336
|
+
(`~/.kojee/sessions/cc-<key>.json` → `port`, `controlTokenPath`):
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
curl -s -X POST "http://127.0.0.1:$PORT/send" \
|
|
340
|
+
-H "Authorization: Bearer $(cat ~/.kojee/control-token)" \
|
|
341
|
+
-H "Content-Type: application/json" \
|
|
342
|
+
-d '{"tandem_id":"T-1","body":"hello","reply_to":"m_0","kind":"message"}'
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Returns the **same JSON envelope** as the CLI. HTTP status mirrors the typed
|
|
346
|
+
error: `200` ok · `400 bad_request` · `401 unauthorized` ·
|
|
347
|
+
`403 content_blocked / member_cap / not_member / governance_denied` ·
|
|
348
|
+
`429 rate_limited` · `502` upstream (gateway auth/network/unknown) ·
|
|
349
|
+
`503 send_unavailable`.
|
|
350
|
+
|
|
351
|
+
**Auth.** Every endpoint that returns or writes this principal's data —
|
|
352
|
+
`POST /send`, `GET /poll`, `GET /status` — requires the same bearer: a fresh
|
|
353
|
+
random token issued at every daemon start, stored `0600` at
|
|
354
|
+
`~/.kojee/control-token` (rotates on restart — re-read the file, don't cache).
|
|
355
|
+
Presenting it proves the caller can read this user's files; other local users
|
|
356
|
+
and browser drive-by requests cannot. Only `GET /health` (a liveness ping
|
|
357
|
+
carrying no data) stays open. The in-tree consumers (the Claude Code Stop /
|
|
358
|
+
UserPromptSubmit hooks and `kojee-mcp doctor`) read the token from the path
|
|
359
|
+
advertised in the session-discovery file and send it automatically; the
|
|
360
|
+
Monitor wake path tails the event-log *file* and never touches `/poll`. If
|
|
361
|
+
the daemon could not issue a token at start (exotic FS), the reads degrade
|
|
362
|
+
open and `POST /send` answers `503` — loudly logged.
|
|
363
|
+
|
|
364
|
+
**Typed errors** (`error` field, both surfaces): `member_cap`,
|
|
365
|
+
`content_blocked` (the gateway WAF rejecting message *content* — commonly a
|
|
366
|
+
literal URL in the body; de-fang or split it; this is **not** an auth failure),
|
|
367
|
+
`gateway_auth`, `not_member`, `rate_limited`, `network`, `governance_denied`,
|
|
368
|
+
`approval_required`, `not_paired`, `not_enrolled`, `bad_request`,
|
|
369
|
+
`unauthorized`, `send_unavailable`, `send_failed` (raw text preserved).
|
|
370
|
+
|
|
254
371
|
## Development
|
|
255
372
|
|
|
256
373
|
Run tests:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/auth/dpop.ts
|
|
2
|
+
import { SignJWT, base64url } from "jose";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
|
|
5
|
+
const payload = {
|
|
6
|
+
htm: method,
|
|
7
|
+
htu: url,
|
|
8
|
+
jti: crypto.randomUUID()
|
|
9
|
+
};
|
|
10
|
+
if (nonce) {
|
|
11
|
+
payload.nonce = nonce;
|
|
12
|
+
}
|
|
13
|
+
if (accessToken) {
|
|
14
|
+
payload.ath = computeAth(accessToken);
|
|
15
|
+
}
|
|
16
|
+
const header = {
|
|
17
|
+
typ: "dpop+jwt",
|
|
18
|
+
alg: "ES256",
|
|
19
|
+
jwk: { kid }
|
|
20
|
+
};
|
|
21
|
+
return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
|
|
22
|
+
}
|
|
23
|
+
function computeAth(accessToken) {
|
|
24
|
+
const hash = crypto.createHash("sha256").update(accessToken).digest();
|
|
25
|
+
return base64url.encode(hash);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/tandem/session-id.ts
|
|
29
|
+
import { ulid } from "ulidx";
|
|
30
|
+
var MCP_SESSION_ID = ulid();
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
createDPoPProof,
|
|
34
|
+
MCP_SESSION_ID
|
|
35
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_SESSION_ID,
|
|
3
|
+
createDPoPProof
|
|
4
|
+
} from "./chunk-2MIISF2W.js";
|
|
5
|
+
import {
|
|
6
|
+
translateHttpError,
|
|
7
|
+
translateJsonRpcError,
|
|
8
|
+
translateNetworkError
|
|
9
|
+
} from "./chunk-LDZXU3DW.js";
|
|
10
|
+
import {
|
|
11
|
+
secureDir,
|
|
12
|
+
secureFile
|
|
13
|
+
} from "./chunk-BLEGIR35.js";
|
|
14
|
+
|
|
15
|
+
// src/auth/keystore.ts
|
|
16
|
+
import { importJWK, exportJWK, generateKeyPair } from "jose";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import os from "os";
|
|
19
|
+
import path from "path";
|
|
20
|
+
var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
|
|
21
|
+
async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
22
|
+
if (!fs.existsSync(keystorePath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const raw = fs.readFileSync(keystorePath, "utf-8");
|
|
26
|
+
const data = JSON.parse(raw);
|
|
27
|
+
if (expectedBrokerUrl && data.broker_url !== expectedBrokerUrl) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const privateKey = await importJWK(data.private_key_jwk, "ES256");
|
|
31
|
+
return {
|
|
32
|
+
privateKey,
|
|
33
|
+
publicJwk: data.public_jwk,
|
|
34
|
+
kid: data.kid,
|
|
35
|
+
data
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
|
|
39
|
+
const dir = path.dirname(keystorePath);
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
41
|
+
secureDir(dir);
|
|
42
|
+
const privateJwk = await exportJWK(privateKey);
|
|
43
|
+
const data = {
|
|
44
|
+
private_key_jwk: privateJwk,
|
|
45
|
+
kid,
|
|
46
|
+
broker_url: brokerUrl,
|
|
47
|
+
public_jwk: publicJwk,
|
|
48
|
+
enrolled_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
49
|
+
};
|
|
50
|
+
fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
|
|
51
|
+
mode: 384
|
|
52
|
+
});
|
|
53
|
+
secureFile(keystorePath);
|
|
54
|
+
}
|
|
55
|
+
async function generateES256KeyPair() {
|
|
56
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
57
|
+
const publicJwk = await exportJWK(publicKey);
|
|
58
|
+
publicJwk.kty = "EC";
|
|
59
|
+
publicJwk.crv = "P-256";
|
|
60
|
+
return { privateKey, publicJwk };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/gateway-client.ts
|
|
64
|
+
import crypto from "crypto";
|
|
65
|
+
var GatewayClient = class {
|
|
66
|
+
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
67
|
+
this.brokerUrl = brokerUrl;
|
|
68
|
+
this.token = token;
|
|
69
|
+
this.privateKey = privateKey;
|
|
70
|
+
this.kid = kid;
|
|
71
|
+
this.endpoint = `${brokerUrl}/mcp/messages/${sessionId}/`;
|
|
72
|
+
}
|
|
73
|
+
brokerUrl;
|
|
74
|
+
token;
|
|
75
|
+
privateKey;
|
|
76
|
+
kid;
|
|
77
|
+
currentNonce;
|
|
78
|
+
requestCounter = 0;
|
|
79
|
+
endpoint;
|
|
80
|
+
/**
|
|
81
|
+
* Expose the DPoP signing key so peer modules sharing auth state
|
|
82
|
+
* (e.g. tandem/event-stream.ts) can sign their own requests.
|
|
83
|
+
*/
|
|
84
|
+
getPrivateKey() {
|
|
85
|
+
return this.privateKey;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Expose the bot_key_id (kid) for DPoP proof headers. Paired with
|
|
89
|
+
* getPrivateKey() so peer modules can construct proofs without
|
|
90
|
+
* threading the key material through their own constructors.
|
|
91
|
+
*/
|
|
92
|
+
getKid() {
|
|
93
|
+
return this.kid;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive a deterministic session ID from the gateway token.
|
|
97
|
+
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
98
|
+
*/
|
|
99
|
+
static deriveSessionId(token) {
|
|
100
|
+
const hash = crypto.createHash("sha256").update(token + "proxy").digest("hex");
|
|
101
|
+
return hash.slice(0, 16);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth and
|
|
105
|
+
* nonce retry transparently. A 403 `step_up_required` (deprecated feature,
|
|
106
|
+
* owner ruling 2026-06-10) is no longer polled — it surfaces immediately as
|
|
107
|
+
* a structured tool error via translateHttpError.
|
|
108
|
+
*
|
|
109
|
+
* `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
|
|
110
|
+
* underlying `fetch` option — NOT placed inside `params`/`arguments`. A
|
|
111
|
+
* caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
|
|
112
|
+
* its controller's signal here so a hung backend aborts at the budget instead
|
|
113
|
+
* of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
|
|
114
|
+
* left fetch un-aborted AND serialized a junk `{}` onto the wire body.
|
|
115
|
+
*/
|
|
116
|
+
async sendRpc(method, params = {}, signal) {
|
|
117
|
+
const rpcRequest = {
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
id: ++this.requestCounter,
|
|
120
|
+
method,
|
|
121
|
+
params
|
|
122
|
+
};
|
|
123
|
+
return this.executeWithRetries(rpcRequest, signal);
|
|
124
|
+
}
|
|
125
|
+
async executeWithRetries(rpcRequest, signal) {
|
|
126
|
+
let response;
|
|
127
|
+
try {
|
|
128
|
+
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return translateNetworkError(err);
|
|
131
|
+
}
|
|
132
|
+
this.trackNonce(response);
|
|
133
|
+
if (response.status === 401) {
|
|
134
|
+
const body = await this.tryParseErrorBody(response);
|
|
135
|
+
if (body?.error === "use_dpop_nonce") {
|
|
136
|
+
console.error("[gateway] Nonce expired, retrying with fresh nonce...");
|
|
137
|
+
try {
|
|
138
|
+
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return translateNetworkError(err);
|
|
141
|
+
}
|
|
142
|
+
this.trackNonce(response);
|
|
143
|
+
} else {
|
|
144
|
+
const translated = translateHttpError(401, body?.error);
|
|
145
|
+
if (translated) return translated;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (response.status === 403) {
|
|
149
|
+
const body = await this.tryParseErrorBody(response);
|
|
150
|
+
const translated = translateHttpError(403, body?.error, body?.trigger);
|
|
151
|
+
if (translated) return translated;
|
|
152
|
+
}
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const body = await this.tryParseErrorBody(response);
|
|
155
|
+
const translated = translateHttpError(response.status, body?.error);
|
|
156
|
+
if (translated) return translated;
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: `Gateway error: ${response.status}` }],
|
|
159
|
+
isError: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const rpcResponse = await response.json();
|
|
163
|
+
if (rpcResponse.error) {
|
|
164
|
+
return translateJsonRpcError(rpcResponse.error);
|
|
165
|
+
}
|
|
166
|
+
const result = rpcResponse.result;
|
|
167
|
+
return result ?? { content: [{ type: "text", text: "No result" }] };
|
|
168
|
+
}
|
|
169
|
+
async sendHttpRequest(rpcRequest, signal) {
|
|
170
|
+
const proof = await createDPoPProof(
|
|
171
|
+
this.privateKey,
|
|
172
|
+
this.kid,
|
|
173
|
+
"POST",
|
|
174
|
+
this.endpoint,
|
|
175
|
+
this.currentNonce,
|
|
176
|
+
this.token
|
|
177
|
+
);
|
|
178
|
+
return fetch(this.endpoint, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: {
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
Authorization: `DPoP ${this.token}`,
|
|
183
|
+
DPoP: proof,
|
|
184
|
+
"Mcp-Session-Id": MCP_SESSION_ID
|
|
185
|
+
},
|
|
186
|
+
body: JSON.stringify(rpcRequest),
|
|
187
|
+
// ROUND-3 MAJOR A: the caller's AbortSignal rides HERE (a real fetch
|
|
188
|
+
// option), never inside the JSON-RPC body. `undefined` is a valid value
|
|
189
|
+
// for the fetch `signal` option (no abort wired).
|
|
190
|
+
...signal ? { signal } : {}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
trackNonce(response) {
|
|
194
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
195
|
+
if (nonce) {
|
|
196
|
+
this.currentNonce = nonce;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async tryParseErrorBody(response) {
|
|
200
|
+
try {
|
|
201
|
+
return await response.json();
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
loadKeystore,
|
|
210
|
+
saveKeystore,
|
|
211
|
+
generateES256KeyPair,
|
|
212
|
+
GatewayClient
|
|
213
|
+
};
|