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.
@@ -0,0 +1,35 @@
1
+ import {
2
+ secureDir,
3
+ secureFile
4
+ } from "./chunk-BLEGIR35.js";
5
+
6
+ // src/tandem/control-token.ts
7
+ import crypto from "crypto";
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path from "path";
11
+ function controlTokenPath(kojeeDir = path.join(os.homedir(), ".kojee")) {
12
+ return path.join(kojeeDir, "control-token");
13
+ }
14
+ function issueControlToken(filePath = controlTokenPath()) {
15
+ const token = crypto.randomBytes(32).toString("hex");
16
+ const dir = path.dirname(filePath);
17
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
18
+ secureDir(dir);
19
+ fs.writeFileSync(filePath, token + "\n", { mode: 384 });
20
+ secureFile(filePath);
21
+ return token;
22
+ }
23
+ function loadControlToken(filePath = controlTokenPath()) {
24
+ try {
25
+ const raw = fs.readFileSync(filePath, "utf8").trim();
26
+ return raw.length > 0 ? raw : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ export {
32
+ controlTokenPath,
33
+ issueControlToken,
34
+ loadControlToken
35
+ };
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  buildMonitorSpawn,
10
10
  buildReplyRecipe
11
- } from "./chunk-C6GZ2L2W.js";
11
+ } from "./chunk-X672ZN7V.js";
12
12
  import {
13
13
  monitorHeartbeatPath,
14
14
  statusLogPath
@@ -220,7 +220,7 @@ function formatDoctorReport(report) {
220
220
  async function runDoctor() {
221
221
  const { readRecordedRuntime } = await import("./runtime-record-WO4IECM6.js");
222
222
  if (readRecordedRuntime() === "codex") {
223
- const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-BMI5JOO6.js");
223
+ const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-3A7KYOVX.js");
224
224
  const report2 = collectCodexDoctorReport();
225
225
  console.error(formatCodexDoctorReport(report2));
226
226
  return report2.verdict === "broken" ? 1 : 0;
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  defaultCodexConfigPath,
3
3
  defaultCodexHooksPath
4
- } from "./chunk-ZW4SW7LJ.js";
4
+ } from "./chunk-64EOLZNI.js";
5
5
  import "./chunk-SQL56SEB.js";
6
6
  import {
7
7
  CODEX_LISTEN_CAP_MS
8
- } from "./chunk-C6GZ2L2W.js";
8
+ } from "./chunk-X672ZN7V.js";
9
9
  import "./chunk-BLEGIR35.js";
10
10
  import {
11
11
  resolveWebhookConfig
12
- } from "./chunk-F7L25L2J.js";
12
+ } from "./chunk-OSKHA5DS.js";
13
13
 
14
14
  // src/doctor-codex.ts
15
15
  import fs from "fs";
@@ -84,6 +84,13 @@ function collectCodexDoctorReport(deps = {}) {
84
84
  ok: true,
85
85
  detail: `enabled \u2014 ${resolution.config.redactedSummary}`
86
86
  });
87
+ if (resolution.warning) {
88
+ checks.push({
89
+ name: "webhook signature config",
90
+ ok: "warn",
91
+ detail: `${resolution.warning} \u2014 ${WIZARD_RERUN} with valid signature flags`
92
+ });
93
+ }
87
94
  } else if (resolution.error) {
88
95
  checks.push({
89
96
  name: "webhook sink",
@@ -1,4 +1,13 @@
1
+ import {
2
+ executeSend,
3
+ httpStatusForEnvelope,
4
+ parseSendRequest,
5
+ sendFailure
6
+ } from "./chunk-HIZ4NDWN.js";
7
+ import "./chunk-LDZXU3DW.js";
8
+
1
9
  // src/tandem/hook-server.ts
10
+ import crypto from "crypto";
2
11
  import { createServer } from "http";
3
12
  async function startHookServer(opts) {
4
13
  const server = createServer((req, res) => {
@@ -38,8 +47,84 @@ async function handleRequest(req, res, opts) {
38
47
  }
39
48
  return json(res, 400, { error: "unknown type", detail: "type must be 'stop' or 'user-prompt-submit'" });
40
49
  }
50
+ if (req.method === "POST" && url.pathname === "/send") {
51
+ return handleSend(req, res, opts);
52
+ }
41
53
  return json(res, 404, { error: "not_found", detail: `${req.method} ${url.pathname}` });
42
54
  }
55
+ var MAX_SEND_BODY_BYTES = 256 * 1024;
56
+ async function handleSend(req, res, opts) {
57
+ if (!opts.send) {
58
+ return respondEnvelope(
59
+ res,
60
+ sendFailure(
61
+ "send_unavailable",
62
+ "this daemon started without a send surface (no gateway/control token wired)"
63
+ )
64
+ );
65
+ }
66
+ if (!bearerMatches(req.headers.authorization, opts.send.authToken)) {
67
+ return respondEnvelope(
68
+ res,
69
+ sendFailure(
70
+ "unauthorized",
71
+ "missing or invalid bearer \u2014 read the control token from the file named by the discovery file's controlTokenPath and send it as `Authorization: Bearer <token>`"
72
+ )
73
+ );
74
+ }
75
+ let raw;
76
+ try {
77
+ raw = await readBody(req, MAX_SEND_BODY_BYTES);
78
+ } catch (err) {
79
+ return respondEnvelope(
80
+ res,
81
+ sendFailure("bad_request", err.message)
82
+ );
83
+ }
84
+ let input;
85
+ try {
86
+ input = JSON.parse(raw);
87
+ } catch {
88
+ return respondEnvelope(
89
+ res,
90
+ sendFailure("bad_request", "request body must be valid JSON")
91
+ );
92
+ }
93
+ const parsed = parseSendRequest(input);
94
+ if (!parsed.ok) {
95
+ return respondEnvelope(res, parsed);
96
+ }
97
+ const envelope = await executeSend(opts.send.gateway, parsed.request);
98
+ respondEnvelope(res, envelope);
99
+ }
100
+ function respondEnvelope(res, envelope) {
101
+ json(res, httpStatusForEnvelope(envelope), envelope);
102
+ }
103
+ function bearerMatches(header, expected) {
104
+ if (!header || !header.startsWith("Bearer ")) return false;
105
+ const presented = header.slice("Bearer ".length).trim();
106
+ if (presented.length === 0) return false;
107
+ const a = crypto.createHash("sha256").update(presented).digest();
108
+ const b = crypto.createHash("sha256").update(expected).digest();
109
+ return crypto.timingSafeEqual(a, b);
110
+ }
111
+ function readBody(req, maxBytes) {
112
+ return new Promise((resolve, reject) => {
113
+ const chunks = [];
114
+ let total = 0;
115
+ req.on("data", (chunk) => {
116
+ total += chunk.length;
117
+ if (total > maxBytes) {
118
+ reject(new Error(`request body exceeds ${maxBytes} bytes`));
119
+ req.destroy();
120
+ return;
121
+ }
122
+ chunks.push(chunk);
123
+ });
124
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
125
+ req.on("error", reject);
126
+ });
127
+ }
43
128
  function respondWithEvents(res, opts) {
44
129
  const entries = opts.queue.takeForHook();
45
130
  const events = entries.map((entry) => opts.adapter.formatTandemEvent(entry.event));
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  startProxy
3
- } from "./chunk-YEC7IHIG.js";
3
+ } from "./chunk-62KH6VNQ.js";
4
+ import "./chunk-YVUXQ4Z2.js";
4
5
  import "./chunk-BJMASMKX.js";
5
- import "./chunk-C6GZ2L2W.js";
6
- import "./chunk-WBMX4CHB.js";
6
+ import "./chunk-X672ZN7V.js";
7
+ import "./chunk-36L3GCU3.js";
8
+ import "./chunk-2MIISF2W.js";
9
+ import "./chunk-LDZXU3DW.js";
7
10
  import "./chunk-BLEGIR35.js";
8
11
  export {
9
12
  startProxy
@@ -0,0 +1,72 @@
1
+ import {
2
+ loadPairedConfig
3
+ } from "./chunk-YH27B6SW.js";
4
+ import {
5
+ GatewayClient,
6
+ loadKeystore
7
+ } from "./chunk-36L3GCU3.js";
8
+ import "./chunk-2MIISF2W.js";
9
+ import {
10
+ executeSend,
11
+ parseSendRequest,
12
+ sendFailure
13
+ } from "./chunk-HIZ4NDWN.js";
14
+ import "./chunk-LDZXU3DW.js";
15
+ import "./chunk-BLEGIR35.js";
16
+
17
+ // src/tandem/send-cli.ts
18
+ import os from "os";
19
+ import path from "path";
20
+ async function createPairedGateway(kojeeDir) {
21
+ const configPath = path.join(kojeeDir, "config.json");
22
+ const paired = loadPairedConfig(configPath);
23
+ if (!paired) {
24
+ return sendFailure(
25
+ "not_paired",
26
+ `no paired config at ${configPath} \u2014 run \`kojee-mcp pair <code> --url <broker>\` first`
27
+ );
28
+ }
29
+ const brokerUrl = paired.broker_url.replace(/\/+$/, "");
30
+ const keystorePath = path.join(kojeeDir, "keypair.json");
31
+ const keystore = await loadKeystore(keystorePath, brokerUrl).catch(() => null);
32
+ if (!keystore) {
33
+ return sendFailure(
34
+ "not_enrolled",
35
+ `no DPoP keystore for ${brokerUrl} at ${keystorePath} \u2014 start the proxy once (or re-run \`kojee-mcp pair\`) to enroll a keypair`
36
+ );
37
+ }
38
+ const sessionId = GatewayClient.deriveSessionId(paired.token);
39
+ return {
40
+ ok: true,
41
+ gateway: new GatewayClient(
42
+ brokerUrl,
43
+ paired.token,
44
+ keystore.privateKey,
45
+ keystore.kid,
46
+ sessionId
47
+ )
48
+ };
49
+ }
50
+ async function runSendCli(args, deps = {}) {
51
+ const parsed = parseSendRequest({
52
+ tandem_id: args.tandemId,
53
+ body: args.body,
54
+ ...args.replyTo !== void 0 ? { reply_to: args.replyTo } : {},
55
+ ...args.kind !== void 0 ? { kind: args.kind } : {}
56
+ });
57
+ if (!parsed.ok) {
58
+ return { exitCode: 1, envelope: parsed };
59
+ }
60
+ const kojeeDir = args.kojeeDir ?? path.join(os.homedir(), ".kojee");
61
+ const createGateway = deps.createGateway ?? createPairedGateway;
62
+ const resolution = await createGateway(kojeeDir);
63
+ if (!resolution.ok) {
64
+ return { exitCode: 1, envelope: resolution };
65
+ }
66
+ const envelope = await executeSend(resolution.gateway, parsed.request);
67
+ return { exitCode: envelope.ok ? 0 : 1, envelope };
68
+ }
69
+ export {
70
+ createPairedGateway,
71
+ runSendCli
72
+ };
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-BJMASMKX.js";
8
8
  import {
9
9
  buildMonitorNudge
10
- } from "./chunk-C6GZ2L2W.js";
10
+ } from "./chunk-X672ZN7V.js";
11
11
  import {
12
12
  monitorHeartbeatPath,
13
13
  nudgeSentinelPath
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createAdaptiveWatchdog
3
- } from "./chunk-WBMX4CHB.js";
3
+ } from "./chunk-YVUXQ4Z2.js";
4
+ import "./chunk-2MIISF2W.js";
4
5
  import {
5
6
  STATUS_LINE_PREFIX,
6
7
  monitorHeartbeatPath,
@@ -0,0 +1,20 @@
1
+ import {
2
+ WEBHOOK_DEFAULT_MAX_RETRIES,
3
+ WEBHOOK_DEFAULT_SIGNATURE_HEADER,
4
+ WEBHOOK_DEFAULT_SIGNATURE_PREFIX,
5
+ WEBHOOK_DEFAULT_TIMEOUT_MS,
6
+ WEBHOOK_SIGNATURE_FORMATS,
7
+ emissionRejectionReason,
8
+ resolveSignatureEmission,
9
+ resolveWebhookConfig
10
+ } from "./chunk-OSKHA5DS.js";
11
+ export {
12
+ WEBHOOK_DEFAULT_MAX_RETRIES,
13
+ WEBHOOK_DEFAULT_SIGNATURE_HEADER,
14
+ WEBHOOK_DEFAULT_SIGNATURE_PREFIX,
15
+ WEBHOOK_DEFAULT_TIMEOUT_MS,
16
+ WEBHOOK_SIGNATURE_FORMATS,
17
+ emissionRejectionReason,
18
+ resolveSignatureEmission,
19
+ resolveWebhookConfig
20
+ };
@@ -1,3 +1,8 @@
1
+ import {
2
+ WEBHOOK_DEFAULT_SIGNATURE_HEADER,
3
+ WEBHOOK_DEFAULT_SIGNATURE_PREFIX
4
+ } from "./chunk-OSKHA5DS.js";
5
+
1
6
  // src/tandem/webhook-sink.ts
2
7
  import crypto from "crypto";
3
8
  import { ulid } from "ulidx";
@@ -26,6 +31,8 @@ function createWebhookSink(config, options = {}) {
26
31
  const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
27
32
  const backoffMs = options.backoffMs ?? defaultBackoffMs;
28
33
  const log = options.log ?? ((line) => console.error(`[webhook] ${line}`));
34
+ const signatureHeader = config.signatureHeader ?? WEBHOOK_DEFAULT_SIGNATURE_HEADER;
35
+ const signaturePrefix = config.signaturePrefix ?? WEBHOOK_DEFAULT_SIGNATURE_PREFIX;
29
36
  const queue = [];
30
37
  let inFlight = false;
31
38
  let stopped = false;
@@ -51,7 +58,7 @@ function createWebhookSink(config, options = {}) {
51
58
  }
52
59
  function prepare(event) {
53
60
  const body = JSON.stringify(event);
54
- const signature = computeWebhookSignature(config.secret, body);
61
+ const signature = signaturePrefix + computeWebhookSignature(config.secret, body);
55
62
  const deliveryId = event.id || ulid();
56
63
  return { event, body, signature, deliveryId };
57
64
  }
@@ -63,8 +70,10 @@ function createWebhookSink(config, options = {}) {
63
70
  method: "POST",
64
71
  headers: {
65
72
  "Content-Type": "application/json",
66
- // hex SHA-256 HMAC of the RAW body bytes — the receiver re-verifies.
67
- "X-Kojee-Signature": item.signature,
73
+ // (prefix +) hex SHA-256 HMAC of the RAW body bytes — the receiver
74
+ // re-verifies. Header name defaults to X-Kojee-Signature; the
75
+ // GitHub-convention preset emits X-Hub-Signature-256 / sha256=.
76
+ [signatureHeader]: item.signature,
68
77
  "X-Kojee-Delivery": item.deliveryId
69
78
  },
70
79
  body: item.body,
@@ -1,31 +1,32 @@
1
+ import {
2
+ clearRuntimeRecord,
3
+ readRecordedRuntime,
4
+ recordRuntime
5
+ } from "./chunk-EW72ZNQL.js";
1
6
  import {
2
7
  buildCodexMcpServerTable,
3
8
  buildCodexStopHookBlock,
4
9
  removeCodexConfig,
5
10
  writeCodexConfig
6
- } from "./chunk-ZW4SW7LJ.js";
11
+ } from "./chunk-64EOLZNI.js";
12
+ import {
13
+ kojeeHomeDir
14
+ } from "./chunk-SQL56SEB.js";
7
15
  import {
8
16
  WIZARD_RUNTIMES,
9
17
  isWizardRuntime
10
18
  } from "./chunk-LVL25VLO.js";
11
- import {
12
- clearRuntimeRecord,
13
- readRecordedRuntime,
14
- recordRuntime
15
- } from "./chunk-EW72ZNQL.js";
16
- import {
17
- kojeeHomeDir
18
- } from "./chunk-SQL56SEB.js";
19
19
  import {
20
20
  CODEX_LISTEN_CAP_MS,
21
21
  buildWebhookReceiverNote
22
- } from "./chunk-C6GZ2L2W.js";
22
+ } from "./chunk-X672ZN7V.js";
23
23
  import {
24
24
  secureFile
25
25
  } from "./chunk-BLEGIR35.js";
26
26
  import {
27
+ resolveSignatureEmission,
27
28
  resolveWebhookConfig
28
- } from "./chunk-F7L25L2J.js";
29
+ } from "./chunk-OSKHA5DS.js";
29
30
 
30
31
  // src/wizard/wizard.ts
31
32
  import crypto from "crypto";
@@ -57,28 +58,55 @@ function resolveWizardWebhook(opts) {
57
58
  const url = (opts.webhookUrl ?? env["KOJEE_WEBHOOK_URL"] ?? "").trim();
58
59
  let secret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
59
60
  if (url && !secret) secret = generateWebhookSecret();
61
+ const sigFormat = (opts.webhookSignatureFormat ?? env["KOJEE_WEBHOOK_SIGNATURE_FORMAT"] ?? "").trim();
62
+ const sigHeader = (opts.webhookSignatureHeader ?? env["KOJEE_WEBHOOK_SIGNATURE_HEADER"] ?? "").trim();
63
+ const sigPrefix = opts.webhookSignaturePrefix ?? env["KOJEE_WEBHOOK_SIGNATURE_PREFIX"];
64
+ const signatureEnv = [];
65
+ if (sigFormat) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_FORMAT", sigFormat]);
66
+ if (sigHeader) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_HEADER", sigHeader]);
67
+ if (sigPrefix !== void 0) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_PREFIX", sigPrefix]);
68
+ const emission = resolveSignatureEmission(Object.fromEntries(signatureEnv));
60
69
  const resolution = resolveWebhookConfig({
61
70
  KOJEE_WEBHOOK_URL: url,
62
71
  KOJEE_WEBHOOK_SECRET: secret,
63
72
  ...env["KOJEE_WEBHOOK_TIMEOUT_MS"] !== void 0 ? { KOJEE_WEBHOOK_TIMEOUT_MS: env["KOJEE_WEBHOOK_TIMEOUT_MS"] } : {},
64
- ...env["KOJEE_WEBHOOK_MAX_RETRIES"] !== void 0 ? { KOJEE_WEBHOOK_MAX_RETRIES: env["KOJEE_WEBHOOK_MAX_RETRIES"] } : {}
73
+ ...env["KOJEE_WEBHOOK_MAX_RETRIES"] !== void 0 ? { KOJEE_WEBHOOK_MAX_RETRIES: env["KOJEE_WEBHOOK_MAX_RETRIES"] } : {},
74
+ ...Object.fromEntries(signatureEnv)
65
75
  });
76
+ const warning = resolution.warning ?? (emission.warnings.length > 0 ? emission.warnings.join("; ") : void 0);
66
77
  if (resolution.error) {
67
- return { url, secret, redactedSummary: "", error: resolution.error };
78
+ return {
79
+ url,
80
+ secret,
81
+ redactedSummary: "",
82
+ signatureEnv,
83
+ signatureHeader: emission.header,
84
+ signaturePrefix: emission.prefix,
85
+ error: resolution.error
86
+ };
68
87
  }
69
88
  return {
70
89
  url,
71
90
  secret,
72
- redactedSummary: resolution.config?.redactedSummary ?? `url=${url} secret=<redacted>`
91
+ redactedSummary: resolution.config?.redactedSummary ?? `url=${url} secret=<redacted>`,
92
+ signatureEnv,
93
+ signatureHeader: emission.header,
94
+ signaturePrefix: emission.prefix,
95
+ ...warning !== void 0 ? { warning } : {}
73
96
  };
74
97
  }
75
- function writeRuntimeEnvFile(runtime, url, secret) {
98
+ function shellSingleQuote(value) {
99
+ return `'${value.replace(/'/g, `'\\''`)}'`;
100
+ }
101
+ function writeRuntimeEnvFile(runtime, url, secret, signatureEnv = []) {
76
102
  const envPath = path.join(kojeeHomeDir(), ".kojee", `${runtime}.env`);
77
103
  const body = [
78
104
  `# kojee daemon env for runtime=${runtime} (source this before starting the daemon)`,
79
- `export KOJEE_RUNTIME="${runtime}"`,
80
- `export KOJEE_WEBHOOK_URL="${url}"`,
81
- `export KOJEE_WEBHOOK_SECRET="${secret}"`,
105
+ `export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`,
106
+ `export KOJEE_WEBHOOK_URL=${shellSingleQuote(url)}`,
107
+ `export KOJEE_WEBHOOK_SECRET=${shellSingleQuote(secret)}`,
108
+ // Signature emission overrides (0.5.3) — only when explicitly configured.
109
+ ...signatureEnv.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`),
82
110
  ""
83
111
  ].join("\n");
84
112
  fs.mkdirSync(path.dirname(envPath), { recursive: true, mode: 448 });
@@ -156,7 +184,8 @@ function configureCodex(opts) {
156
184
  ...opts.configPath ? { configPath: opts.configPath } : {},
157
185
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
158
186
  webhookUrl: url,
159
- webhookSecret: secret
187
+ webhookSecret: secret,
188
+ ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {}
160
189
  });
161
190
  recordRuntime("codex");
162
191
  const lines = [];
@@ -164,7 +193,12 @@ function configureCodex(opts) {
164
193
  lines.push("Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
165
194
  lines.push("");
166
195
  lines.push("Wrote [mcp_servers.kojee] to ~/.codex/config.toml:");
167
- lines.push(indent(buildCodexMcpServerTable({ webhookUrl: url, webhookSecret: "<redacted>" })));
196
+ lines.push(indent(buildCodexMcpServerTable({
197
+ webhookUrl: url,
198
+ webhookSecret: "<redacted>",
199
+ ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {}
200
+ })));
201
+ if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
168
202
  lines.push("");
169
203
  lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
170
204
  lines.push(indent(buildCodexStopHookBlock()));
@@ -175,7 +209,7 @@ function configureCodex(opts) {
175
209
  lines.push("Next steps (codex):");
176
210
  lines.push(" Restart Codex (or start a new `codex` / `codex exec` session) to load the MCP server and hook.");
177
211
  lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
178
- lines.push(indent(buildWebhookReceiverNote()));
212
+ lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
179
213
  lines.push(" Verify: kojee-mcp doctor");
180
214
  lines.push("");
181
215
  lines.push(CODEX_UNVERIFIED_NOTE);
@@ -191,23 +225,26 @@ function configureWebhookDaemon(runtime, opts) {
191
225
  lines.push(`Configured runtime: ${runtime}`);
192
226
  lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
193
227
  lines.push("");
228
+ if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
194
229
  if (wh.url) {
195
- const envFile = writeRuntimeEnvFile(runtime, wh.url, wh.secret);
230
+ const envFile = writeRuntimeEnvFile(runtime, wh.url, wh.secret, wh.signatureEnv);
196
231
  lines.push("Export these before starting the daemon (also written to a source-able file):");
197
- lines.push(` export KOJEE_RUNTIME="${runtime}"`);
198
- lines.push(` export KOJEE_WEBHOOK_URL="${wh.url}"`);
232
+ lines.push(` export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`);
233
+ lines.push(` export KOJEE_WEBHOOK_URL=${shellSingleQuote(wh.url)}`);
199
234
  lines.push(` export KOJEE_WEBHOOK_SECRET=<generated; in ${envFile}>`);
235
+ for (const [k, v] of wh.signatureEnv) lines.push(` export ${k}=${shellSingleQuote(v)}`);
200
236
  lines.push(` (validated: ${wh.redactedSummary})`);
201
237
  lines.push(` source ${envFile}`);
202
238
  } else {
203
239
  lines.push("Set the daemon env (no receiver URL supplied yet):");
204
- lines.push(` export KOJEE_RUNTIME="${runtime}"`);
240
+ lines.push(` export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`);
205
241
  lines.push(` export KOJEE_WEBHOOK_URL="https://YOUR-RECEIVER.local/kojee"`);
206
242
  lines.push(` export KOJEE_WEBHOOK_SECRET=<generate a hex secret>`);
243
+ for (const [k, v] of wh.signatureEnv) lines.push(` export ${k}=${shellSingleQuote(v)}`);
207
244
  }
208
245
  lines.push("");
209
246
  lines.push("Receiver contract:");
210
- lines.push(indent(buildWebhookReceiverNote()));
247
+ lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
211
248
  lines.push("");
212
249
  lines.push(`Next steps (${runtime}):`);
213
250
  lines.push(" Start the daemon with the env above. Verify: kojee-mcp doctor (after the daemon is up).");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,60 +0,0 @@
1
- // src/tandem/webhook-config.ts
2
- var WEBHOOK_DEFAULT_TIMEOUT_MS = 5e3;
3
- var WEBHOOK_DEFAULT_MAX_RETRIES = 4;
4
- function redactUrlUserinfo(rawUrl, parsed) {
5
- if (!parsed.username && !parsed.password) return rawUrl;
6
- parsed.username = "";
7
- parsed.password = "";
8
- return parsed.toString();
9
- }
10
- function parsePositiveInt(raw, fallback) {
11
- if (raw === void 0) return fallback;
12
- const n = Number(raw);
13
- if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return fallback;
14
- return n;
15
- }
16
- function resolveWebhookConfig(env = process.env) {
17
- const url = (env["KOJEE_WEBHOOK_URL"] ?? "").trim();
18
- if (!url) {
19
- return { enabled: false, config: null };
20
- }
21
- let parsed;
22
- try {
23
- parsed = new URL(url);
24
- } catch {
25
- return {
26
- enabled: false,
27
- config: null,
28
- error: `KOJEE_WEBHOOK_URL is not a valid URL \u2014 webhook sink DISABLED`
29
- };
30
- }
31
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
32
- return {
33
- enabled: false,
34
- config: null,
35
- error: `KOJEE_WEBHOOK_URL must be http(s) (got ${parsed.protocol}) \u2014 webhook sink DISABLED`
36
- };
37
- }
38
- const secret = (env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
39
- if (!secret) {
40
- return {
41
- enabled: false,
42
- config: null,
43
- error: "KOJEE_WEBHOOK_URL is set but KOJEE_WEBHOOK_SECRET is missing \u2014 webhook sink DISABLED (the proxy NEVER sends unsigned webhooks)"
44
- };
45
- }
46
- const timeoutMs = parsePositiveInt(env["KOJEE_WEBHOOK_TIMEOUT_MS"], WEBHOOK_DEFAULT_TIMEOUT_MS);
47
- const maxRetries = parsePositiveInt(env["KOJEE_WEBHOOK_MAX_RETRIES"], WEBHOOK_DEFAULT_MAX_RETRIES);
48
- const safeUrl = redactUrlUserinfo(url, parsed);
49
- const redactedSummary = `url=${safeUrl} secret=<redacted> timeoutMs=${timeoutMs} maxRetries=${maxRetries}`;
50
- return {
51
- enabled: true,
52
- config: { url, secret, timeoutMs, maxRetries, redactedSummary }
53
- };
54
- }
55
-
56
- export {
57
- WEBHOOK_DEFAULT_TIMEOUT_MS,
58
- WEBHOOK_DEFAULT_MAX_RETRIES,
59
- resolveWebhookConfig
60
- };
@@ -1,10 +0,0 @@
1
- import {
2
- WEBHOOK_DEFAULT_MAX_RETRIES,
3
- WEBHOOK_DEFAULT_TIMEOUT_MS,
4
- resolveWebhookConfig
5
- } from "./chunk-F7L25L2J.js";
6
- export {
7
- WEBHOOK_DEFAULT_MAX_RETRIES,
8
- WEBHOOK_DEFAULT_TIMEOUT_MS,
9
- resolveWebhookConfig
10
- };