kojee-mcp 0.5.4 → 0.5.6
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 +24 -5
- package/dist/{chunk-62KH6VNQ.js → chunk-2BDAM3TH.js} +61 -160
- package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
- package/dist/chunk-6SK6ITFE.js +142 -0
- package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
- package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
- package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
- package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
- package/dist/cli.js +19 -24
- package/dist/control-token-4BUCTYQB.js +13 -0
- package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
- package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
- package/dist/ensure-join-7AEDJMPE.js +96 -0
- package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
- package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
- package/dist/index.d.ts +18 -15
- package/dist/index.js +7 -4
- package/dist/lib.d.ts +427 -0
- package/dist/lib.js +44 -0
- package/dist/reconnect-scheduler-JSXCJKQP.js +26 -0
- package/dist/resubscribe-G5OGDZJD.js +6 -0
- package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
- package/dist/{stop-hook-GO363SMD.js → stop-hook-TRAMQYNE.js} +15 -7
- package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
- package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
- package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
- package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
- package/dist/{wizard-Z5JA3YPV.js → wizard-OSOAY4GO.js} +4 -4
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -218,8 +218,8 @@ in it is Hermes-specific — and it is **OFF by default**.
|
|
|
218
218
|
|---|---|
|
|
219
219
|
| `KOJEE_WEBHOOK_URL` | Receiver endpoint (http/https). **Unset ⇒ sink OFF** (zero behavior change). |
|
|
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
|
-
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `
|
|
222
|
-
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a retryable failure —
|
|
221
|
+
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `30000`). A timed-out attempt is **never retried** — see the retry policy below. |
|
|
222
|
+
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a **retryable** failure — connection errors (refused / reset / DNS) / 5xx / 408 / 429 (default `2`). Timeouts are **not** in this class. |
|
|
223
223
|
| `KOJEE_WEBHOOK_SIGNATURE_HEADER` | Header name carrying the signature (default `X-Kojee-Signature`). |
|
|
224
224
|
| `KOJEE_WEBHOOK_SIGNATURE_PREFIX` | Literal string prepended to the hex digest (default empty — bare hex). |
|
|
225
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. |
|
|
@@ -275,10 +275,29 @@ constant beside it):
|
|
|
275
275
|
cursor on restart, so the same event may arrive more than once. There is **no
|
|
276
276
|
exactly-once** promise; the receiver's dedupe is what makes redelivery safe.
|
|
277
277
|
|
|
278
|
+
**Retry policy (0.5.6 — anti-storm).** A delivery attempt that **times out is
|
|
279
|
+
never retried**: the receiver may well have processed the event and just
|
|
280
|
+
answered slowly (the canonical receiver spawns an agent session *before*
|
|
281
|
+
responding), so a re-POST risks duplicate side effects by design. The sink logs
|
|
282
|
+
it as `delivered-unconfirmed (receiver slow)` and moves on. Retries happen
|
|
283
|
+
**only on genuine non-delivery**: connection errors (refused / reset / DNS —
|
|
284
|
+
the request never reached a receiver) and 5xx / 408 / 429 responses (the
|
|
285
|
+
receiver answered that it did *not* process the event), up to
|
|
286
|
+
`KOJEE_WEBHOOK_MAX_RETRIES` (default `2`). For those retried cases delivery is
|
|
287
|
+
still **at-least-once** and every redelivery carries the same
|
|
288
|
+
`X-Kojee-Delivery` id and identical body bytes — **dedupe by event id remains
|
|
289
|
+
the receiver's responsibility** (the `recipe.ts` contract). This fix removes
|
|
290
|
+
the timeout-driven storm, not the at-least-once semantics. If your receiver
|
|
291
|
+
does slow work, the robust pattern is still: verify the signature, dedupe,
|
|
292
|
+
**respond `202` immediately**, then process.
|
|
293
|
+
|
|
278
294
|
The sink is isolated and fire-and-forget: a slow, hanging, or failing webhook can
|
|
279
|
-
never delay or break the Monitor (event-log) or Channel wake paths
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
never delay or break the Monitor (event-log) or Channel wake paths (those run
|
|
296
|
+
before the webhook push). A slow delivery only delays *later webhook events*
|
|
297
|
+
behind it in the sink's own FIFO, and that backlog is bounded: the queue caps at
|
|
298
|
+
1000 (overflow logs + drops the newest; the resubscribe-replay redelivers after
|
|
299
|
+
a restart). The status log redacts the secret and strips any basic-auth
|
|
300
|
+
credentials embedded in `KOJEE_WEBHOOK_URL`.
|
|
282
301
|
|
|
283
302
|
## Wake Continuity (0.5.4)
|
|
284
303
|
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
buildCatchUpNote,
|
|
3
|
+
buildMonitorSpawn,
|
|
4
|
+
buildReplyRecipe
|
|
5
|
+
} from "./chunk-X672ZN7V.js";
|
|
4
6
|
import {
|
|
5
7
|
deriveDiscoveryKey,
|
|
6
8
|
findClaudeAncestorPid
|
|
7
9
|
} from "./chunk-BJMASMKX.js";
|
|
8
10
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
AuthModule
|
|
12
|
+
} from "./chunk-6SK6ITFE.js";
|
|
13
|
+
import {
|
|
14
|
+
GatewayClient
|
|
15
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
13
16
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
loadKeystore,
|
|
17
|
-
saveKeystore
|
|
18
|
-
} from "./chunk-36L3GCU3.js";
|
|
17
|
+
startEventStream
|
|
18
|
+
} from "./chunk-UEGQGXPY.js";
|
|
19
19
|
import {
|
|
20
20
|
translateToolCallResult
|
|
21
21
|
} from "./chunk-LDZXU3DW.js";
|
|
@@ -25,139 +25,6 @@ import fs2 from "fs";
|
|
|
25
25
|
import os from "os";
|
|
26
26
|
import path2 from "path";
|
|
27
27
|
|
|
28
|
-
// src/auth/auth-module.ts
|
|
29
|
-
import { calculateJwkThumbprint } from "jose";
|
|
30
|
-
import crypto from "crypto";
|
|
31
|
-
|
|
32
|
-
// src/auth/registration.ts
|
|
33
|
-
async function registerKey(brokerUrl, token, publicJwk) {
|
|
34
|
-
const url = `${brokerUrl}/api/v1/bots/keys/register/`;
|
|
35
|
-
const response = await fetch(url, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: {
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
Authorization: `Bearer ${token}`
|
|
40
|
-
},
|
|
41
|
-
body: JSON.stringify({ public_jwk: publicJwk })
|
|
42
|
-
});
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
const body = await response.text();
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Key registration failed (${response.status}): ${body}`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return await response.json();
|
|
50
|
-
}
|
|
51
|
-
async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
|
|
52
|
-
const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
|
|
53
|
-
const response = await fetch(url, {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: {
|
|
56
|
-
"Content-Type": "application/json",
|
|
57
|
-
Authorization: `Bearer ${token}`
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({
|
|
60
|
-
bot_key_id: botKeyId,
|
|
61
|
-
challenge,
|
|
62
|
-
signature
|
|
63
|
-
})
|
|
64
|
-
});
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
const body = await response.text();
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Key confirmation failed (${response.status}): ${body}`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return await response.json();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/auth/auth-module.ts
|
|
75
|
-
async function signChallengeRaw(privateKey, data) {
|
|
76
|
-
const signer = crypto.createSign("SHA256");
|
|
77
|
-
signer.update(data);
|
|
78
|
-
signer.end();
|
|
79
|
-
const derSignature = signer.sign(
|
|
80
|
-
privateKey
|
|
81
|
-
);
|
|
82
|
-
return derSignature.toString("base64url");
|
|
83
|
-
}
|
|
84
|
-
var AuthModule = class {
|
|
85
|
-
constructor(token, brokerUrl, keystorePath) {
|
|
86
|
-
this.token = token;
|
|
87
|
-
this.brokerUrl = brokerUrl;
|
|
88
|
-
this.keystorePath = keystorePath;
|
|
89
|
-
}
|
|
90
|
-
token;
|
|
91
|
-
brokerUrl;
|
|
92
|
-
keystorePath;
|
|
93
|
-
privateKey = null;
|
|
94
|
-
publicJwk = null;
|
|
95
|
-
kid = null;
|
|
96
|
-
/**
|
|
97
|
-
* Ensure we have an enrolled keypair. Either loads from disk or
|
|
98
|
-
* performs the full enrollment flow.
|
|
99
|
-
*/
|
|
100
|
-
async ensureEnrolled() {
|
|
101
|
-
const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
|
|
102
|
-
if (existing) {
|
|
103
|
-
this.privateKey = existing.privateKey;
|
|
104
|
-
this.publicJwk = existing.publicJwk;
|
|
105
|
-
this.kid = existing.kid;
|
|
106
|
-
console.error("[auth] Loaded existing keypair from keystore");
|
|
107
|
-
return {
|
|
108
|
-
privateKey: existing.privateKey,
|
|
109
|
-
publicJwk: existing.publicJwk,
|
|
110
|
-
kid: existing.kid
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
console.error("[auth] No valid keystore found, enrolling new keypair...");
|
|
114
|
-
const { privateKey, publicJwk } = await generateES256KeyPair();
|
|
115
|
-
const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
|
|
116
|
-
console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
|
|
117
|
-
const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
118
|
-
const challengeData = `${regResult.challenge}.${thumbprint}`;
|
|
119
|
-
const signature = await signChallengeRaw(privateKey, challengeData);
|
|
120
|
-
const confirmResult = await confirmKey(
|
|
121
|
-
this.brokerUrl,
|
|
122
|
-
this.token,
|
|
123
|
-
regResult.bot_key_id,
|
|
124
|
-
regResult.challenge,
|
|
125
|
-
signature
|
|
126
|
-
);
|
|
127
|
-
if (!confirmResult.key_confirmed) {
|
|
128
|
-
throw new Error("Key enrollment failed: confirmation was rejected");
|
|
129
|
-
}
|
|
130
|
-
console.error("[auth] Key enrollment confirmed");
|
|
131
|
-
await saveKeystore(
|
|
132
|
-
privateKey,
|
|
133
|
-
publicJwk,
|
|
134
|
-
regResult.bot_key_id,
|
|
135
|
-
this.brokerUrl,
|
|
136
|
-
this.keystorePath
|
|
137
|
-
);
|
|
138
|
-
this.privateKey = privateKey;
|
|
139
|
-
this.publicJwk = publicJwk;
|
|
140
|
-
this.kid = regResult.bot_key_id;
|
|
141
|
-
return {
|
|
142
|
-
privateKey,
|
|
143
|
-
publicJwk,
|
|
144
|
-
kid: regResult.bot_key_id
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
getPrivateKey() {
|
|
148
|
-
if (!this.privateKey) throw new Error("Not enrolled yet");
|
|
149
|
-
return this.privateKey;
|
|
150
|
-
}
|
|
151
|
-
getPublicJwk() {
|
|
152
|
-
if (!this.publicJwk) throw new Error("Not enrolled yet");
|
|
153
|
-
return this.publicJwk;
|
|
154
|
-
}
|
|
155
|
-
getKid() {
|
|
156
|
-
if (!this.kid) throw new Error("Not enrolled yet");
|
|
157
|
-
return this.kid;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
28
|
// src/tool-registry.ts
|
|
162
29
|
var ToolRegistry = class {
|
|
163
30
|
constructor(gateway) {
|
|
@@ -269,7 +136,19 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
|
269
136
|
const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
|
|
270
137
|
return intro + monitorSection + listenSection + advice;
|
|
271
138
|
}
|
|
272
|
-
function
|
|
139
|
+
async function executeToolCall(registry, name, args, hooks) {
|
|
140
|
+
const rawResult = await registry.callTool(name, args);
|
|
141
|
+
const result = translateToolCallResult(rawResult);
|
|
142
|
+
if (name === "tandem_join" && !result.isError) {
|
|
143
|
+
try {
|
|
144
|
+
hooks?.onTandemJoin?.();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
|
|
273
152
|
const capabilities = { tools: {} };
|
|
274
153
|
if (adapter.supportsChannels) {
|
|
275
154
|
capabilities.experimental = { "claude/channel": {} };
|
|
@@ -291,8 +170,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
|
|
|
291
170
|
});
|
|
292
171
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
293
172
|
const { name, arguments: args } = request.params;
|
|
294
|
-
const
|
|
295
|
-
const result = translateToolCallResult(rawResult);
|
|
173
|
+
const result = await executeToolCall(registry, name, args ?? {}, hooks);
|
|
296
174
|
return { content: result.content, isError: result.isError };
|
|
297
175
|
});
|
|
298
176
|
return server;
|
|
@@ -420,6 +298,7 @@ async function listTandemIds(gateway) {
|
|
|
420
298
|
return list.map((t) => {
|
|
421
299
|
if (typeof t === "string") return t;
|
|
422
300
|
const obj = t;
|
|
301
|
+
if (obj?.my_membership?.is_member !== true) return void 0;
|
|
423
302
|
return obj?.tandem_id ?? obj?.id;
|
|
424
303
|
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
425
304
|
} catch {
|
|
@@ -437,6 +316,19 @@ async function startProxy(config) {
|
|
|
437
316
|
console.error(
|
|
438
317
|
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
439
318
|
);
|
|
319
|
+
let activeStreamHandle = null;
|
|
320
|
+
const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-JSXCJKQP.js");
|
|
321
|
+
const joinReconnect = createJoinReconnectScheduler({
|
|
322
|
+
reconnect: () => activeStreamHandle?.reconnect()
|
|
323
|
+
});
|
|
324
|
+
const onTandemJoin = () => joinReconnect.requestReconnect();
|
|
325
|
+
const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
|
|
326
|
+
await ensureJoinTandems({
|
|
327
|
+
gateway,
|
|
328
|
+
env: process.env["KOJEE_TANDEMS"],
|
|
329
|
+
listTandems: () => listTandemIds(gateway),
|
|
330
|
+
onJoined: () => joinReconnect.requestReconnect()
|
|
331
|
+
});
|
|
440
332
|
let tandemMembershipCount = -1;
|
|
441
333
|
try {
|
|
442
334
|
const bootIds = await listTandemIds(gateway);
|
|
@@ -448,16 +340,16 @@ async function startProxy(config) {
|
|
|
448
340
|
let server;
|
|
449
341
|
if (adapter.supportsChannels) {
|
|
450
342
|
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
451
|
-
const { startHookServer } = await import("./hook-server-
|
|
343
|
+
const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
|
|
452
344
|
const {
|
|
453
345
|
writeDiscoveryByKey,
|
|
454
346
|
cleanupDiscoveryByKey,
|
|
455
347
|
sweepStaleDiscovery
|
|
456
348
|
} = await import("./session-discovery-FNMJGFPM.js");
|
|
457
349
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
458
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
459
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
460
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
350
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
351
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
352
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
461
353
|
sweepStaleDiscovery();
|
|
462
354
|
sweepStaleEventLogs();
|
|
463
355
|
const ccPid = await findClaudeAncestorPid();
|
|
@@ -487,14 +379,16 @@ async function startProxy(config) {
|
|
|
487
379
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
488
380
|
});
|
|
489
381
|
}
|
|
490
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path
|
|
491
|
-
|
|
382
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
|
|
383
|
+
onTandemJoin
|
|
384
|
+
});
|
|
385
|
+
const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
|
|
492
386
|
let controlToken = null;
|
|
493
387
|
try {
|
|
494
388
|
controlToken = issueControlToken();
|
|
495
389
|
} catch (err) {
|
|
496
390
|
console.error(
|
|
497
|
-
"[kojee-mcp] control token write failed \u2014 POST /send disabled:",
|
|
391
|
+
"[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
|
|
498
392
|
err.message
|
|
499
393
|
);
|
|
500
394
|
}
|
|
@@ -504,7 +398,10 @@ async function startProxy(config) {
|
|
|
504
398
|
port: 0,
|
|
505
399
|
queue,
|
|
506
400
|
adapter,
|
|
507
|
-
|
|
401
|
+
// 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
|
|
402
|
+
// reads (GET /poll, GET /status). When token issuance failed both stay
|
|
403
|
+
// available-but-degraded: /send answers 503, the reads stay open.
|
|
404
|
+
...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
|
|
508
405
|
getStreamState: () => streamHandle ? streamHandle.getState() : {
|
|
509
406
|
connected: false,
|
|
510
407
|
connectedSince: null,
|
|
@@ -584,6 +481,7 @@ async function startProxy(config) {
|
|
|
584
481
|
};
|
|
585
482
|
})()
|
|
586
483
|
});
|
|
484
|
+
activeStreamHandle = streamHandle;
|
|
587
485
|
const cancelStream = streamHandle;
|
|
588
486
|
process.stdin.on("end", () => {
|
|
589
487
|
cancelStream();
|
|
@@ -597,9 +495,9 @@ async function startProxy(config) {
|
|
|
597
495
|
});
|
|
598
496
|
} else if (needsWebhookEventStream()) {
|
|
599
497
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
600
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
601
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
602
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
498
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
499
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
500
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
603
501
|
sweepStaleEventLogs();
|
|
604
502
|
const ccPid = await findClaudeAncestorPid();
|
|
605
503
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
@@ -627,7 +525,9 @@ async function startProxy(config) {
|
|
|
627
525
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
628
526
|
});
|
|
629
527
|
}
|
|
630
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount
|
|
528
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
|
|
529
|
+
onTandemJoin
|
|
530
|
+
});
|
|
631
531
|
process.on("exit", () => eventLog.cleanup());
|
|
632
532
|
const streamHandle = await startEventStream({
|
|
633
533
|
brokerUrl: config.url,
|
|
@@ -649,6 +549,7 @@ async function startProxy(config) {
|
|
|
649
549
|
};
|
|
650
550
|
})()
|
|
651
551
|
});
|
|
552
|
+
activeStreamHandle = streamHandle;
|
|
652
553
|
process.stdin.on("end", () => {
|
|
653
554
|
streamHandle();
|
|
654
555
|
void webhookSink?.stop();
|
|
@@ -698,7 +599,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
698
599
|
}
|
|
699
600
|
|
|
700
601
|
export {
|
|
701
|
-
AuthModule,
|
|
702
602
|
VERSION,
|
|
603
|
+
listTandemIds,
|
|
703
604
|
startProxy
|
|
704
605
|
};
|
|
@@ -14,10 +14,18 @@ import {
|
|
|
14
14
|
|
|
15
15
|
// src/auth/keystore.ts
|
|
16
16
|
import { importJWK, exportJWK, generateKeyPair } from "jose";
|
|
17
|
+
import crypto from "crypto";
|
|
17
18
|
import fs from "fs";
|
|
18
19
|
import os from "os";
|
|
19
20
|
import path from "path";
|
|
20
21
|
var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
|
|
22
|
+
function defaultPairedKeystorePath() {
|
|
23
|
+
return path.join(os.homedir(), ".kojee", "keypair.json");
|
|
24
|
+
}
|
|
25
|
+
function deriveKeystorePath(token) {
|
|
26
|
+
const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
27
|
+
return path.join(os.homedir(), ".kojee", `keypair-${hash}.json`);
|
|
28
|
+
}
|
|
21
29
|
async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
22
30
|
if (!fs.existsSync(keystorePath)) {
|
|
23
31
|
return null;
|
|
@@ -61,7 +69,7 @@ async function generateES256KeyPair() {
|
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
// src/gateway-client.ts
|
|
64
|
-
import
|
|
72
|
+
import crypto2 from "crypto";
|
|
65
73
|
var GatewayClient = class {
|
|
66
74
|
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
67
75
|
this.brokerUrl = brokerUrl;
|
|
@@ -97,7 +105,7 @@ var GatewayClient = class {
|
|
|
97
105
|
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
98
106
|
*/
|
|
99
107
|
static deriveSessionId(token) {
|
|
100
|
-
const hash =
|
|
108
|
+
const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
|
|
101
109
|
return hash.slice(0, 16);
|
|
102
110
|
}
|
|
103
111
|
/**
|
|
@@ -206,6 +214,8 @@ var GatewayClient = class {
|
|
|
206
214
|
};
|
|
207
215
|
|
|
208
216
|
export {
|
|
217
|
+
defaultPairedKeystorePath,
|
|
218
|
+
deriveKeystorePath,
|
|
209
219
|
loadKeystore,
|
|
210
220
|
saveKeystore,
|
|
211
221
|
generateES256KeyPair,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateES256KeyPair,
|
|
3
|
+
loadKeystore,
|
|
4
|
+
saveKeystore
|
|
5
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
6
|
+
|
|
7
|
+
// src/auth/auth-module.ts
|
|
8
|
+
import { calculateJwkThumbprint } from "jose";
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
|
|
11
|
+
// src/auth/registration.ts
|
|
12
|
+
async function registerKey(brokerUrl, token, publicJwk) {
|
|
13
|
+
const url = `${brokerUrl}/api/v1/bots/keys/register/`;
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
Authorization: `Bearer ${token}`
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({ public_jwk: publicJwk })
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Key registration failed (${response.status}): ${body}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return await response.json();
|
|
29
|
+
}
|
|
30
|
+
async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
|
|
31
|
+
const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: `Bearer ${token}`
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
bot_key_id: botKeyId,
|
|
40
|
+
challenge,
|
|
41
|
+
signature
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Key confirmation failed (${response.status}): ${body}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return await response.json();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/auth/auth-module.ts
|
|
54
|
+
async function signChallengeRaw(privateKey, data) {
|
|
55
|
+
const signer = crypto.createSign("SHA256");
|
|
56
|
+
signer.update(data);
|
|
57
|
+
signer.end();
|
|
58
|
+
const derSignature = signer.sign(
|
|
59
|
+
privateKey
|
|
60
|
+
);
|
|
61
|
+
return derSignature.toString("base64url");
|
|
62
|
+
}
|
|
63
|
+
var AuthModule = class {
|
|
64
|
+
constructor(token, brokerUrl, keystorePath) {
|
|
65
|
+
this.token = token;
|
|
66
|
+
this.brokerUrl = brokerUrl;
|
|
67
|
+
this.keystorePath = keystorePath;
|
|
68
|
+
}
|
|
69
|
+
token;
|
|
70
|
+
brokerUrl;
|
|
71
|
+
keystorePath;
|
|
72
|
+
privateKey = null;
|
|
73
|
+
publicJwk = null;
|
|
74
|
+
kid = null;
|
|
75
|
+
/**
|
|
76
|
+
* Ensure we have an enrolled keypair. Either loads from disk or
|
|
77
|
+
* performs the full enrollment flow.
|
|
78
|
+
*/
|
|
79
|
+
async ensureEnrolled() {
|
|
80
|
+
const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
|
|
81
|
+
if (existing) {
|
|
82
|
+
this.privateKey = existing.privateKey;
|
|
83
|
+
this.publicJwk = existing.publicJwk;
|
|
84
|
+
this.kid = existing.kid;
|
|
85
|
+
console.error("[auth] Loaded existing keypair from keystore");
|
|
86
|
+
return {
|
|
87
|
+
privateKey: existing.privateKey,
|
|
88
|
+
publicJwk: existing.publicJwk,
|
|
89
|
+
kid: existing.kid
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
console.error("[auth] No valid keystore found, enrolling new keypair...");
|
|
93
|
+
const { privateKey, publicJwk } = await generateES256KeyPair();
|
|
94
|
+
const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
|
|
95
|
+
console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
|
|
96
|
+
const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
97
|
+
const challengeData = `${regResult.challenge}.${thumbprint}`;
|
|
98
|
+
const signature = await signChallengeRaw(privateKey, challengeData);
|
|
99
|
+
const confirmResult = await confirmKey(
|
|
100
|
+
this.brokerUrl,
|
|
101
|
+
this.token,
|
|
102
|
+
regResult.bot_key_id,
|
|
103
|
+
regResult.challenge,
|
|
104
|
+
signature
|
|
105
|
+
);
|
|
106
|
+
if (!confirmResult.key_confirmed) {
|
|
107
|
+
throw new Error("Key enrollment failed: confirmation was rejected");
|
|
108
|
+
}
|
|
109
|
+
console.error("[auth] Key enrollment confirmed");
|
|
110
|
+
await saveKeystore(
|
|
111
|
+
privateKey,
|
|
112
|
+
publicJwk,
|
|
113
|
+
regResult.bot_key_id,
|
|
114
|
+
this.brokerUrl,
|
|
115
|
+
this.keystorePath
|
|
116
|
+
);
|
|
117
|
+
this.privateKey = privateKey;
|
|
118
|
+
this.publicJwk = publicJwk;
|
|
119
|
+
this.kid = regResult.bot_key_id;
|
|
120
|
+
return {
|
|
121
|
+
privateKey,
|
|
122
|
+
publicJwk,
|
|
123
|
+
kid: regResult.bot_key_id
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
getPrivateKey() {
|
|
127
|
+
if (!this.privateKey) throw new Error("Not enrolled yet");
|
|
128
|
+
return this.privateKey;
|
|
129
|
+
}
|
|
130
|
+
getPublicJwk() {
|
|
131
|
+
if (!this.publicJwk) throw new Error("Not enrolled yet");
|
|
132
|
+
return this.publicJwk;
|
|
133
|
+
}
|
|
134
|
+
getKid() {
|
|
135
|
+
if (!this.kid) throw new Error("Not enrolled yet");
|
|
136
|
+
return this.kid;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
AuthModule
|
|
142
|
+
};
|
|
@@ -16,7 +16,12 @@ function issueControlToken(filePath = controlTokenPath()) {
|
|
|
16
16
|
const dir = path.dirname(filePath);
|
|
17
17
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18
18
|
secureDir(dir);
|
|
19
|
-
|
|
19
|
+
try {
|
|
20
|
+
fs.unlinkSync(filePath);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code !== "ENOENT") throw err;
|
|
23
|
+
}
|
|
24
|
+
fs.writeFileSync(filePath, token + "\n", { mode: 384, flag: "wx" });
|
|
20
25
|
secureFile(filePath);
|
|
21
26
|
return token;
|
|
22
27
|
}
|
|
@@ -28,8 +33,14 @@ function loadControlToken(filePath = controlTokenPath()) {
|
|
|
28
33
|
return null;
|
|
29
34
|
}
|
|
30
35
|
}
|
|
36
|
+
function controlTokenAuthHeaders(filePath) {
|
|
37
|
+
const token = loadControlToken(filePath);
|
|
38
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export {
|
|
32
42
|
controlTokenPath,
|
|
33
43
|
issueControlToken,
|
|
34
|
-
loadControlToken
|
|
44
|
+
loadControlToken,
|
|
45
|
+
controlTokenAuthHeaders
|
|
35
46
|
};
|