jules-orchestrator-kit 0.9.4 → 0.20.0

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.
@@ -1,9 +1,13 @@
1
1
  import { readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
3
4
 
4
5
  const DEFAULT_MAX_CHARS = 10000; // Safe threshold below Antigravity 12k & Claude 25k limits
5
6
  const DEFAULT_MAX_LINES = 250;
6
7
 
8
+ export const SENTINEL_BEGIN = "JULES_RULES_SENTINEL BEGIN";
9
+ export const SENTINEL_END = "JULES_RULES_SENTINEL END";
10
+
7
11
  /**
8
12
  * Audit character and line counts of compiled instruction files to prevent truncation.
9
13
  *
@@ -68,6 +72,103 @@ export function checkRulesBudget(root = process.cwd(), opts = {}) {
68
72
  };
69
73
  }
70
74
 
75
+ /**
76
+ * Compiles markdown/rules files into a single unified context block wrapped
77
+ * in SHA-256 and byte-length anti-truncation sentinels.
78
+ */
79
+ export function compileRules(root = process.cwd(), _opts = {}) {
80
+ const candidateFiles = [
81
+ "AGENTS.md",
82
+ "CLAUDE.md",
83
+ "GEMINI.md",
84
+ ".cursorrules",
85
+ "JULES_RULES_TEMPLATE.md",
86
+ ];
87
+
88
+ const rulesDir = join(root, ".agent", "rules");
89
+ try {
90
+ const files = readdirSync(rulesDir);
91
+ for (const f of files) {
92
+ if (f.endsWith(".md")) {
93
+ candidateFiles.push(join(".agent", "rules", f));
94
+ }
95
+ }
96
+ } catch (_) {}
97
+
98
+ const sources = [];
99
+ const sections = [];
100
+
101
+ for (const relPath of candidateFiles) {
102
+ const fullPath = join(root, relPath);
103
+ if (!existsSync(fullPath)) continue;
104
+ try {
105
+ const raw = readFileSync(fullPath, "utf-8");
106
+ const body = raw.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
107
+ if (!body) continue;
108
+ sections.push(`<!-- source: ${relPath} -->\n${body}`);
109
+ sources.push(relPath);
110
+ } catch (_) {}
111
+ }
112
+
113
+ const body = sections.join("\n\n---\n\n");
114
+ const bodyLen = Buffer.byteLength(body, "utf-8");
115
+ const sha256 = createHash("sha256").update(body).digest("hex");
116
+
117
+ const header = `<!-- ${SENTINEL_BEGIN} len=${bodyLen} sha256=${sha256} -->`;
118
+ const footer = `<!-- ${SENTINEL_END} len=${bodyLen} sha256=${sha256} -->`;
119
+ const compiled = `${header}\n${body}\n${footer}\n`;
120
+
121
+ return { compiled, body, bodyLen, sha256, sources };
122
+ }
123
+
124
+ /**
125
+ * Verifies that a compiled rule string has not been truncated or tampered with by downstream LLMs.
126
+ */
127
+ export function verifyRulesSentinel(compiled) {
128
+ const errors = [];
129
+ if (typeof compiled !== "string" || compiled.length === 0) {
130
+ return { ok: false, errors: ["empty input"] };
131
+ }
132
+
133
+ const beginRe = new RegExp(`<!--\\s*${SENTINEL_BEGIN.replace(/ /g, "\\s+")}\\s+len=(\\d+)\\s+sha256=([0-9a-f]{64})\\s*-->`);
134
+ const endRe = new RegExp(`<!--\\s*${SENTINEL_END.replace(/ /g, "\\s+")}\\s+len=(\\d+)\\s+sha256=([0-9a-f]{64})\\s*-->`);
135
+
136
+ const beginMatch = beginRe.exec(compiled);
137
+ const endMatch = endRe.exec(compiled);
138
+
139
+ if (!beginMatch) { errors.push("missing or malformed BEGIN sentinel"); return { ok: false, errors }; }
140
+ if (!endMatch) { errors.push("missing or malformed END sentinel"); return { ok: false, errors }; }
141
+
142
+ const beginLen = Number(beginMatch[1]);
143
+ const beginSha = beginMatch[2];
144
+ const endLen = Number(endMatch[1]);
145
+ const endSha = endMatch[2];
146
+
147
+ if (beginLen !== endLen) errors.push(`length mismatch: BEGIN=${beginLen} END=${endLen}`);
148
+ if (beginSha !== endSha) errors.push("checksum mismatch between BEGIN and END sentinels");
149
+
150
+ const bodyStart = beginMatch.index + beginMatch[0].length;
151
+ const bodyEnd = endMatch.index;
152
+ const body = compiled.slice(bodyStart, bodyEnd).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
153
+ const actualLen = Buffer.byteLength(body, "utf-8");
154
+
155
+ if (actualLen !== beginLen) {
156
+ errors.push(`body length ${actualLen} != declared ${beginLen} (possible truncation or injection)`);
157
+ }
158
+
159
+ const recomputed = createHash("sha256").update(body).digest("hex");
160
+ if (recomputed !== beginSha) {
161
+ errors.push(`body checksum ${recomputed} != declared ${beginSha} (content tampered or truncated)`);
162
+ }
163
+
164
+ return {
165
+ ok: errors.length === 0,
166
+ errors,
167
+ bodyLen: actualLen,
168
+ sha256: recomputed,
169
+ };
170
+ }
171
+
71
172
  function existsSync(p) {
72
173
  try {
73
174
  return statSync(p).isFile();
@@ -75,3 +176,4 @@ function existsSync(p) {
75
176
  return false;
76
177
  }
77
178
  }
179
+
package/src/security.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import { openSync, writeSync, fsyncSync, closeSync, renameSync, realpathSync, existsSync, lstatSync } from "node:fs";
2
+ import { dirname, join, basename } from "node:path";
3
+ import { randomBytes } from "node:crypto";
1
4
  import { normalizePath } from "./config.mjs";
2
5
 
3
6
  export const HIGH_CONFIDENCE_PATTERNS = [
@@ -6,34 +9,70 @@ export const HIGH_CONFIDENCE_PATTERNS = [
6
9
  /\bghu_[A-Za-z0-9_]{36,255}\b/g,
7
10
  /\bghs_[A-Za-z0-9_]{36,255}\b/g,
8
11
  /\bghr_[A-Za-z0-9_]{36,255}\b/g,
12
+ /\bgithub_pat_[A-Za-z0-9_]{22,255}\b/g,
9
13
 
10
14
  /\bAKIA[0-9A-Z]{16}\b/g,
11
15
  /\bASIA[0-9A-Z]{16}\b/g,
12
16
  /\baws_secret_access_key\s*=\s*['"]?[A-Za-z0-9\/+=]{40}['"]?/g,
13
17
 
14
- /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----[\s\S]*?-----END\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----/g,
18
+ /-----BEGIN (?:RSA|DSA|EC|OPENSSH|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|DSA|EC|OPENSSH|PRIVATE) KEY-----/g,
15
19
  /PuTTY-User-Key-File-[0-9]:[^\n]+/g,
16
20
 
17
21
  /\bsk_live_[0-9a-zA-Z]{24,99}\b/g,
18
22
  /\brk_live_[0-9a-zA-Z]{24,99}\b/g,
19
23
  /\bnpm_[0-9a-zA-Z]{36}\b/g,
20
- /\bglpat-[0-9a-zA-Z_-]{20,}\b/g,
21
- /\bGOCSPX-[0-9a-zA-Z_-]{28}\b/g,
24
+ /\bglpat-[0-9a-zA-Z_-]{20,99}\b/g,
25
+ /\bGOCSPX-[0-9a-zA-Z_-]{28,99}\b/g,
22
26
 
23
27
  /\bAIzaSy[A-Za-z0-9_-]{33}\b/g,
28
+ /\bya29\.[A-Za-z0-9_-]{20,255}\b/g,
29
+ /\b(?:sk-ant-api03-|sk-proj-)[A-Za-z0-9_-]{32,255}\b/g,
24
30
 
25
- /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_]+\/B[A-Za-z0-9_]+\/[A-Za-z0-9_]+/g,
31
+ /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_]{8,}\/B[A-Za-z0-9_]{8,}\/[A-Za-z0-9_]{24,}/g,
32
+ /\bxox[baprs]-[0-9a-zA-Z-]{10,48}\b/g,
26
33
 
27
34
  /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
28
35
  ];
29
36
 
30
37
  export const LOW_CONFIDENCE_PATTERNS = [
31
- /Bearer\s+[A-Za-z0-9._~+/-]{10,}/gi,
32
- /Authorization:\s*Bearer\s+[A-Za-z0-9._~+/-]+=*/gi,
38
+ /Bearer\s+[A-Za-z0-9._~+/-]{10,255}/gi,
39
+ /Authorization:\s*Bearer\s+[A-Za-z0-9._~+/-]{10,255}/gi,
33
40
  /\bsk_test_[0-9a-zA-Z]{24,99}\b/g,
34
41
  /(?:api[_-]?key|secret|password|passwd|token|auth[_-]?token)\s*[:=]\s*['"]([^'"]{8,128})['"]/gi,
35
42
  ];
36
43
 
44
+ /**
45
+ * TOCTOU-safe atomic file write using O_CREAT|O_EXCL|O_WRONLY temp file in target dir.
46
+ */
47
+ export function safeAtomicWrite(filePath, content, options = {}) {
48
+ const mode = options.mode || 0o644;
49
+ const encoding = options.encoding || "utf-8";
50
+
51
+ if (existsSync(filePath)) {
52
+ const stat = lstatSync(filePath);
53
+ if (stat.isSymbolicLink()) {
54
+ const realPath = realpathSync(filePath);
55
+ if (options.rejectSymlinks) {
56
+ throw new Error(`TOCTOU Guard: Refusing to write to symlink ${filePath} -> ${realPath}`);
57
+ }
58
+ }
59
+ }
60
+
61
+ const dir = dirname(filePath);
62
+ const tempFile = join(dir, `.tmp-${basename(filePath)}-${randomBytes(6).toString("hex")}`);
63
+
64
+ const fd = openSync(tempFile, "wx", mode);
65
+ try {
66
+ writeSync(fd, content, null, encoding);
67
+ fsyncSync(fd);
68
+ } finally {
69
+ closeSync(fd);
70
+ }
71
+
72
+ renameSync(tempFile, filePath);
73
+ return true;
74
+ }
75
+
37
76
  export function shannonEntropy(str) {
38
77
  if (!str || typeof str !== "string") return 0;
39
78
  const len = str.length;
package/src/state.mjs CHANGED
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  readFileSync,
3
- appendFileSync,
4
3
  existsSync,
5
4
  mkdirSync,
5
+ rmdirSync,
6
6
  readdirSync,
7
7
  openSync,
8
8
  writeSync,
9
9
  closeSync,
10
+ fsyncSync,
10
11
  unlinkSync,
11
12
  } from "node:fs";
12
13
  import { join } from "node:path";
14
+ import { createHash } from "node:crypto";
15
+ import { hostname } from "node:os";
13
16
  import { resolveRoot } from "./config.mjs";
14
17
 
15
18
  export function ensureDir(dirPath) {
@@ -43,17 +46,111 @@ export function getDailyLedgerPath(rootOrOpts = resolveRoot()) {
43
46
  }
44
47
 
45
48
  /**
46
- * Appends a structured audit event to the daily session ledger.
49
+ * Executes a function inside a kernel-level VFS directory mutex for strict linearizability.
50
+ */
51
+ export function withVfsMutex(mutexDir, fn, opts = {}) {
52
+ const maxRetries = opts.maxRetries || 50;
53
+ const retryDelayMs = opts.retryDelayMs || 10;
54
+
55
+ for (let i = 0; i < maxRetries; i++) {
56
+ try {
57
+ mkdirSync(mutexDir);
58
+ try {
59
+ return fn();
60
+ } finally {
61
+ try {
62
+ rmdirSync(mutexDir);
63
+ } catch (_) {}
64
+ }
65
+ } catch (err) {
66
+ if (err.code === "EEXIST") {
67
+ const deadline = Date.now() + retryDelayMs;
68
+ while (Date.now() < deadline) {}
69
+ continue;
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+ return fn();
75
+ }
76
+
77
+ /**
78
+ * Appends a structured audit event to the daily session ledger with SHA-256 hash-chain (VFS Mutex Linearized).
47
79
  */
48
80
  export function appendLedger(entry, rootOrOpts = resolveRoot()) {
49
81
  const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
50
82
  const filePath = getDailyLedgerPath(root);
51
- const payload = {
52
- timestamp: new Date().toISOString(),
53
- ...entry,
54
- };
55
- appendFileSync(filePath, JSON.stringify(payload) + "\n", "utf-8");
56
- return payload;
83
+ const mutexDir = join(getStateDir(root), ".ledger.mutex");
84
+
85
+ return withVfsMutex(mutexDir, () => {
86
+ let prevHash = "0".repeat(64);
87
+ if (existsSync(filePath)) {
88
+ try {
89
+ const raw = readFileSync(filePath, "utf-8");
90
+ const lines = raw.split("\n").filter(Boolean);
91
+ for (let i = lines.length - 1; i >= 0; i--) {
92
+ try {
93
+ const lastObj = JSON.parse(lines[i]);
94
+ if (lastObj.hash) {
95
+ prevHash = lastObj.hash;
96
+ break;
97
+ }
98
+ } catch (_) {}
99
+ }
100
+ } catch (_) {}
101
+ }
102
+
103
+ const timestamp = new Date().toISOString();
104
+ const rawPayload = { timestamp, ...entry, prevHash };
105
+ const hash = createHash("sha256").update(JSON.stringify(rawPayload)).digest("hex");
106
+ const payload = { ...rawPayload, hash };
107
+
108
+ const fd = openSync(filePath, "a");
109
+ try {
110
+ writeSync(fd, JSON.stringify(payload) + "\n", "utf-8");
111
+ fsyncSync(fd);
112
+ } finally {
113
+ closeSync(fd);
114
+ }
115
+
116
+ return payload;
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Verifies the SHA-256 cryptographic hash-chain integrity of a ledger file.
122
+ */
123
+ export function verifyLedgerIntegrity(filePath) {
124
+ if (!filePath || !existsSync(filePath)) return { ok: false, count: 0, error: "FILE_NOT_FOUND" };
125
+ try {
126
+ const raw = readFileSync(filePath, "utf-8");
127
+ const lines = raw.split("\n").filter(Boolean);
128
+ let expectedPrevHash = "0".repeat(64);
129
+
130
+ for (let i = 0; i < lines.length; i++) {
131
+ let obj;
132
+ try {
133
+ obj = JSON.parse(lines[i]);
134
+ } catch (err) {
135
+ return { ok: false, line: i + 1, error: "TORN_WRITE_CORRUPTION", detail: err.message };
136
+ }
137
+ if (!obj.hash || !obj.prevHash) {
138
+ return { ok: false, line: i + 1, error: "MISSING_HASH_FIELDS" };
139
+ }
140
+ if (obj.prevHash !== expectedPrevHash) {
141
+ return { ok: false, line: i + 1, error: "BROKEN_PREV_HASH", expected: expectedPrevHash, actual: obj.prevHash };
142
+ }
143
+ const { hash, ...rest } = obj;
144
+ const recomputed = createHash("sha256").update(JSON.stringify(rest)).digest("hex");
145
+ if (recomputed !== hash) {
146
+ return { ok: false, line: i + 1, error: "CORRUPTED_ENTRY_HASH", expected: recomputed, actual: hash };
147
+ }
148
+ expectedPrevHash = hash;
149
+ }
150
+ return { ok: true, count: lines.length, lastHash: expectedPrevHash };
151
+ } catch (err) {
152
+ return { ok: false, count: 0, error: err.message };
153
+ }
57
154
  }
58
155
 
59
156
  export function readLedger(filePath) {
@@ -72,9 +169,6 @@ export function readLedger(filePath) {
72
169
  }
73
170
  }
74
171
 
75
- /**
76
- * Enforces a daily token budget.
77
- */
78
172
  export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
79
173
  let root = typeof arg1 === "string" ? arg1 : resolveRoot();
80
174
  let limit = typeof arg1 === "number" ? arg1 : typeof arg2 === "number" ? arg2 : 300;
@@ -106,17 +200,35 @@ export class BudgetError extends Error {
106
200
  }
107
201
  }
108
202
 
109
- export async function withBudget(fn, root = resolveRoot(), limit = 300) {
203
+ export function reserveBudget(rootOrOpts = resolveRoot(), limit = 300) {
204
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
110
205
  const budget = checkDailyBudget(root, limit);
111
206
  if (!budget.ok) {
112
207
  throw new BudgetError(`Daily budget exhausted (${budget.used}/${budget.budget} tasks executed)`);
113
208
  }
114
- return fn();
209
+
210
+ const reservationId = `res-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
211
+ appendLedger({ event: "budget_reserved", reservationId, budget: limit }, root);
212
+ return { ok: true, reservationId, remaining: Math.max(0, budget.remaining - 1) };
213
+ }
214
+
215
+ export function commitBudgetReservation(rootOrOpts = resolveRoot(), reservationId = "") {
216
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
217
+ return appendLedger({ event: "budget_committed", reservationId }, root);
218
+ }
219
+
220
+ export async function withBudget(fn, root = resolveRoot(), limit = 300) {
221
+ const reservation = reserveBudget(root, limit);
222
+ try {
223
+ const result = await fn();
224
+ commitBudgetReservation(root, reservation.reservationId);
225
+ return result;
226
+ } catch (err) {
227
+ appendLedger({ event: "budget_reservation_failed", reservationId: reservation.reservationId, error: err.message }, root);
228
+ throw err;
229
+ }
115
230
  }
116
231
 
117
- /**
118
- * Atomic Advisory Lock File Manager with PID Vitality Check & Stale Lock Reaper (CWE-367 Fix).
119
- */
120
232
  export function getLockDir(rootOrOpts = resolveRoot()) {
121
233
  const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
122
234
  const dir = join(getStateDir(root), "locks");
@@ -124,14 +236,39 @@ export function getLockDir(rootOrOpts = resolveRoot()) {
124
236
  return dir;
125
237
  }
126
238
 
127
- export function isPidAlive(pid) {
239
+ /**
240
+ * Checks if a PID is alive with optional PID-recycling start time validation on Linux.
241
+ */
242
+ export function isPidAlive(pid, expectedStartTime = null) {
128
243
  if (!pid || typeof pid !== "number") return false;
129
244
  try {
130
245
  process.kill(pid, 0);
131
- return true;
132
246
  } catch (_) {
133
247
  return false;
134
248
  }
249
+
250
+ if (expectedStartTime && process.platform === "linux") {
251
+ try {
252
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
253
+ const parts = stat.split(" ");
254
+ const startTime = parts[21];
255
+ if (startTime && String(startTime) !== String(expectedStartTime)) {
256
+ return false;
257
+ }
258
+ } catch (_) {}
259
+ }
260
+
261
+ return true;
262
+ }
263
+
264
+ function getProcessStartTime(pid) {
265
+ if (process.platform === "linux") {
266
+ try {
267
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
268
+ return stat.split(" ")[21] || null;
269
+ } catch (_) {}
270
+ }
271
+ return null;
135
272
  }
136
273
 
137
274
  export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
@@ -139,15 +276,13 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
139
276
  const lockDir = getLockDir(root);
140
277
  const lockFile = join(lockDir, `${taskId}.json`);
141
278
 
142
- // Stale Lock Reaper Check
143
279
  if (existsSync(lockFile)) {
144
280
  try {
145
281
  const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
146
- const isAlive = isPidAlive(existing.pid);
282
+ const isAlive = isPidAlive(existing.pid, existing.processStartTime);
147
283
  const isExpired = existing.acquiredAt && Date.now() - new Date(existing.acquiredAt).getTime() > 7200000;
148
284
 
149
285
  if (!isAlive || isExpired) {
150
- // Dead or expired holder; safely reap lock
151
286
  try { unlinkSync(lockFile); } catch (_) {}
152
287
  } else {
153
288
  return { ok: false, holder: existing.agent, taskId, pid: existing.pid };
@@ -162,13 +297,15 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
162
297
  taskId,
163
298
  files,
164
299
  pid: process.pid,
300
+ processStartTime: getProcessStartTime(process.pid),
301
+ hostname: hostname(),
165
302
  acquiredAt: new Date().toISOString(),
166
303
  };
167
304
 
168
- // Atomic file creation via 'wx' flag (Fixes TOCTOU Race Condition)
169
305
  try {
170
306
  const fd = openSync(lockFile, "wx");
171
307
  writeSync(fd, JSON.stringify(payload, null, 2), "utf-8");
308
+ fsyncSync(fd);
172
309
  closeSync(fd);
173
310
  return { ok: true, lockFile };
174
311
  } catch (err) {
@@ -191,7 +328,7 @@ export function releaseLock(taskId, rootOrOpts = resolveRoot()) {
191
328
 
192
329
  if (existsSync(lockFile)) {
193
330
  try {
194
- unlinkSync(lockFile); // CRITICAL FIX: Fixed ESM unlinkSync (removed legacy require("node:fs"))
331
+ unlinkSync(lockFile);
195
332
  return true;
196
333
  } catch (_) {}
197
334
  }
package/src/webhook.mjs CHANGED
@@ -9,7 +9,7 @@ import { createServer } from "node:http";
9
9
  * @returns {boolean} True if signature matches
10
10
  */
11
11
  export function verifySignature(payload, signatureHeader, secret) {
12
- if (!secret) return true; // If no secret is configured, skip verification (development mode)
12
+ if (!secret) return false; // Fail closed if secret is not configured
13
13
  if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;
14
14
 
15
15
  const hmac = createHmac("sha256", secret);
@@ -104,15 +104,29 @@ export function createWebhookServer({ _port = 8787, secret = process.env.JULES_W
104
104
  return;
105
105
  }
106
106
 
107
+ const MAX_BODY = 2 * 1024 * 1024; // 2MB payload cap
107
108
  const chunks = [];
108
- req.on("data", (chunk) => chunks.push(chunk));
109
+ let totalSize = 0;
110
+
111
+ req.on("data", (chunk) => {
112
+ totalSize += chunk.length;
113
+ if (totalSize > MAX_BODY) {
114
+ res.writeHead(413, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ error: "Payload Too Large" }));
116
+ req.destroy();
117
+ return;
118
+ }
119
+ chunks.push(chunk);
120
+ });
121
+
109
122
  req.on("end", () => {
123
+ if (totalSize > MAX_BODY) return;
110
124
  const bodyBuf = Buffer.concat(chunks);
111
125
  const sigHeader = req.headers["x-hub-signature-256"];
112
126
  const eventType = req.headers["x-github-event"];
113
127
  const contentType = req.headers["content-type"] || "application/json";
114
128
 
115
- if (secret && !verifySignature(bodyBuf, sigHeader, secret)) {
129
+ if (!verifySignature(bodyBuf, sigHeader, secret)) {
116
130
  log(`[Webhook] Invalid signature received from ${req.socket.remoteAddress}`);
117
131
  res.writeHead(401, { "Content-Type": "application/json" });
118
132
  res.end(JSON.stringify({ error: "Invalid signature" }));