@solongate/proxy 0.81.3 → 0.81.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/hooks/audit.mjs CHANGED
@@ -399,6 +399,10 @@ let input = '';
399
399
  // EOF avoids creating that handle, so the exits below tear down cleanly.
400
400
  try { input += readFileSync(0, 'utf-8'); } catch {}
401
401
  ;(async () => {
402
+ // Safety backstop ONLY: the normal path exits by natural drain. Force-exit if the
403
+ // loop fails to drain — 8s > the 5s fetch timeout, so the threadpool is idle and
404
+ // exit() can't hit the Windows UV_HANDLE_CLOSING abort.
405
+ try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
402
406
  try {
403
407
  const data = JSON.parse(input);
404
408
  let EMITTED_PAYLOAD = null;
@@ -531,12 +535,13 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
531
535
  // on the write's flush callback (and on the fire-and-forget audit POST).
532
536
  let flushed = (typeof EMITTED_PAYLOAD !== 'string');
533
537
  let fetchDone = false;
534
- // Defer the exit by one loop tick (setImmediate). Calling process.exit() in the
535
- // same tick the audit-log fetch settled aborts on Windows + Node 24 with
536
- // `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`
537
- // a threadpool worker is still mid-uv_async_send when exit() closes the loop's
538
- // async handle. One tick lets libuv drain that completion; we then still hard-exit.
539
- const maybeExit = () => { if (flushed && fetchDone) { process.exitCode = 0; setImmediate(() => { try { process.exit(0); } catch {} }); } };
538
+ // Set the exit code and let the event loop drain naturally do NOT call
539
+ // process.exit(). On Windows + Node 24, process.exit() in the same tick the
540
+ // audit-log fetch settled aborts with `Assertion failed: !(handle->flags &
541
+ // UV_HANDLE_CLOSING), file src\win\async.c` (a threadpool DNS worker is still
542
+ // mid-uv_async_send when exit() force-closes the loop's async handle). undici
543
+ // unrefs idle sockets, so Node exits on its own ~1ms after the fetch settles.
544
+ const maybeExit = () => { if (flushed && fetchDone) { process.exitCode = 0; } };
540
545
  if (typeof EMITTED_PAYLOAD === 'string') {
541
546
  try { process.stdout.write(EMITTED_PAYLOAD, () => { flushed = true; maybeExit(); }); }
542
547
  catch { flushed = true; }
@@ -586,11 +591,11 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
586
591
  signal: AbortSignal.timeout(5000),
587
592
  }).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
588
593
  }
589
- // Hard backstop: exit even if the write callback or fetch never settles. By 3s
590
- // the threadpool has long drained, so a direct exit here is safe.
591
- setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 3000);
594
+ // Safety backstop ONLY (unref'd, 8s > the 5s fetch timeout): the normal path
595
+ // exits by natural drain ~1ms after the fetch settles. This fires only if the
596
+ // loop somehow fails to drain by 8s the threadpool is idle, so exit() is safe.
597
+ try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
592
598
  } catch {
593
- // Defer one tick in case an error was thrown while a fetch was still settling.
594
- try { setImmediate(() => { try { process.exit(0); } catch {} }); } catch { try { process.exit(0); } catch {} }
599
+ process.exitCode = 0;
595
600
  }
596
601
  })();
@@ -6757,21 +6757,17 @@ async function refreshPolicyCache() {
6757
6757
  }
6758
6758
  }
6759
6759
  if (REFRESH_MODE) {
6760
- refreshPolicyCache().finally(() => {
6761
- process.exitCode = 0;
6762
- try {
6763
- setImmediate(() => {
6764
- try {
6765
- process.exit(0);
6766
- } catch {
6767
- }
6768
- });
6769
- } catch {
6760
+ try {
6761
+ setTimeout(() => {
6770
6762
  try {
6771
- process.exit(0);
6763
+ process.exit(process.exitCode || 0);
6772
6764
  } catch {
6773
6765
  }
6774
- }
6766
+ }, 8e3).unref();
6767
+ } catch {
6768
+ }
6769
+ refreshPolicyCache().finally(() => {
6770
+ process.exitCode = 0;
6775
6771
  });
6776
6772
  }
6777
6773
  var SG_DONE = Symbol("sg-done");
@@ -6780,19 +6776,6 @@ function sgFinish(code) {
6780
6776
  if (!_sgDone) {
6781
6777
  _sgDone = true;
6782
6778
  process.exitCode = code;
6783
- try {
6784
- setImmediate(() => {
6785
- try {
6786
- process.exit(process.exitCode || 0);
6787
- } catch {
6788
- }
6789
- });
6790
- } catch {
6791
- try {
6792
- process.exit(code);
6793
- } catch {
6794
- }
6795
- }
6796
6779
  }
6797
6780
  throw SG_DONE;
6798
6781
  }
@@ -7698,6 +7681,15 @@ if (!REFRESH_MODE) {
7698
7681
  }
7699
7682
  }
7700
7683
  (async () => {
7684
+ try {
7685
+ setTimeout(() => {
7686
+ try {
7687
+ process.exit(process.exitCode || 0);
7688
+ } catch {
7689
+ }
7690
+ }, 8e3).unref();
7691
+ } catch {
7692
+ }
7701
7693
  try {
7702
7694
  if (REFRESH_MODE)
7703
7695
  return;
package/hooks/guard.mjs CHANGED
@@ -281,7 +281,7 @@ async function refreshPolicyCache() {
281
281
  try { writeFileSync(cacheFile, JSON.stringify({ _ts: Date.now(), policy, selfProtect, security, hookVersions })); } catch {}
282
282
  } catch {}
283
283
  }
284
- if (REFRESH_MODE) { refreshPolicyCache().finally(() => { process.exitCode = 0; try { setImmediate(() => { try { process.exit(0); } catch {} }); } catch { try { process.exit(0); } catch {} } }); }
284
+ if (REFRESH_MODE) { try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {} refreshPolicyCache().finally(() => { process.exitCode = 0; }); }
285
285
 
286
286
  // ── Per-tool block/allow output ──
287
287
  // Response format depends on the agent:
@@ -301,18 +301,20 @@ if (REFRESH_MODE) { refreshPolicyCache().finally(() => { process.exitCode = 0; t
301
301
  // the threadpool has drained, so exit() is safe.
302
302
  const SG_DONE = Symbol('sg-done');
303
303
  let _sgDone = false;
304
+ // Terminate WITHOUT ever calling process.exit() on the hot path. On Windows + Node
305
+ // 24, process.exit() called in (or right after) the same tick a cloud fetch()
306
+ // settled aborts with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
307
+ // file src\win\async.c`: a libuv threadpool worker (DNS) is still mid-uv_async_send
308
+ // when exit() force-closes the loop's async handle. The abort replaces exit code 2,
309
+ // so Claude Code sees a non-blocking hook failure and runs the tool anyway (guard
310
+ // logs DENY, never blocks). Instead we just set process.exitCode and let the event
311
+ // loop drain — undici unrefs idle sockets, so Node exits on its own within ~1ms of
312
+ // the work finishing, cleanly (no forced teardown → no abort). A latch preserves
313
+ // the FIRST code so a later allowTool() on fall-through can't overwrite a block; an
314
+ // unref'd backstop armed at the top of the handler is the only place exit() may run,
315
+ // and only long after every fetch has settled.
304
316
  function sgFinish(code) {
305
- if (!_sgDone) {
306
- _sgDone = true;
307
- process.exitCode = code;
308
- // Defer the real exit by ONE loop tick. process.exit() called synchronously in
309
- // the same tick a fetch just settled aborts on Windows: a threadpool worker is
310
- // still mid-uv_async_send when exit() closes the loop's async handle. setImmediate
311
- // lets libuv drain that completion first, then we exit — ~0ms, and it still hard-
312
- // exits (killing undici keep-alive sockets) instead of waiting on them.
313
- try { setImmediate(() => { try { process.exit(process.exitCode || 0); } catch {} }); }
314
- catch { try { process.exit(code); } catch {} }
315
- }
317
+ if (!_sgDone) { _sgDone = true; process.exitCode = code; }
316
318
  throw SG_DONE;
317
319
  }
318
320
 
@@ -1440,6 +1442,10 @@ function readReferencedFiles(args, cwd) {
1440
1442
  // inside a stream callback, so the loop tears down cleanly.
1441
1443
  if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
1442
1444
  ;(async () => {
1445
+ // Safety backstop ONLY: the normal path exits by natural drain (~1ms after work
1446
+ // finishes). If the loop somehow fails to drain, force-exit — 8s is well past every
1447
+ // fetch timeout (≤3s) so the threadpool is idle and exit() can't hit the abort.
1448
+ try { setTimeout(() => { try { process.exit(process.exitCode || 0); } catch {} }, 8000).unref(); } catch {}
1443
1449
  try {
1444
1450
  // Background-refresh invocation has no tool call to evaluate — it already ran
1445
1451
  // refreshPolicyCache() at startup; do nothing on the (empty) stdin.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.3",
3
+ "version": "0.81.4",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {