git-watchtower 2.3.12 → 2.3.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.12",
3
+ "version": "2.3.14",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const { ansi, box } = require('../ui/ansi');
10
+ const sounds = require('./sounds');
10
11
 
11
12
  // ============================================================================
12
13
  // Casino Mode State
@@ -110,6 +111,18 @@ function disable() {
110
111
  // up to ~15 frames × 120 ms = 1.8 s after disable, and lossMessage stayed
111
112
  // set so isLossAnimating() reported true into the next session.
112
113
  resetLossState();
114
+ // Drop the marquee render callback so a stale closure to the previous
115
+ // session's render() doesn't survive across enable/disable cycles. In
116
+ // production this is mostly hygiene (bin/git-watchtower.js wires the
117
+ // callback exactly once at startup against a singleton render fn), but
118
+ // tests that re-use the casino module saw the previous test's callback
119
+ // persist into the next setRenderCallback assignment.
120
+ marqueeCallback = null;
121
+ // Cancel any pending sound timeouts (jackpot bell chains, multi-play
122
+ // mega-jackpot files) so audio doesn't continue after the user
123
+ // toggled casino mode off — and so child processes from the file-play
124
+ // path don't get spawned by a setTimeout that fires after shutdown.
125
+ sounds.cancelAll();
113
126
  }
114
127
 
115
128
  /**
@@ -9,6 +9,44 @@ const { execFile } = require('child_process');
9
9
  const path = require('path');
10
10
  const fs = require('fs');
11
11
 
12
+ /**
13
+ * Pending setTimeout handles for the multi-bell / multi-play chains used
14
+ * by playJackpot and playMegaJackpot. Tracked so casino.disable() (and
15
+ * shutdown) can cancel any in-flight chain instead of letting up to ~600
16
+ * ms of post-disable audio leak through.
17
+ * @type {Set<NodeJS.Timeout>}
18
+ */
19
+ const _pendingTimeouts = new Set();
20
+
21
+ /**
22
+ * Schedule a callback like setTimeout, but auto-track the handle so it
23
+ * can be cancelled by cancelAll() and auto-removes itself on fire.
24
+ * @param {Function} fn
25
+ * @param {number} delay
26
+ * @returns {NodeJS.Timeout}
27
+ * @private
28
+ */
29
+ function _scheduleTracked(fn, delay) {
30
+ const handle = setTimeout(() => {
31
+ _pendingTimeouts.delete(handle);
32
+ try { fn(); } catch (e) { /* sounds are optional */ }
33
+ }, delay);
34
+ _pendingTimeouts.add(handle);
35
+ return handle;
36
+ }
37
+
38
+ /**
39
+ * Cancel every pending sound timeout. Idempotent. Called from
40
+ * casino.disable() so a jackpot fired moments before the user toggled
41
+ * casino mode off doesn't keep playing afterward.
42
+ */
43
+ function cancelAll() {
44
+ for (const handle of _pendingTimeouts) {
45
+ clearTimeout(handle);
46
+ }
47
+ _pendingTimeouts.clear();
48
+ }
49
+
12
50
  // ============================================================================
13
51
  // Sound Configuration
14
52
  // ============================================================================
@@ -133,10 +171,11 @@ function playJackpot() {
133
171
  if (soundPath) {
134
172
  playFile(soundPath, VOLUME.jackpot);
135
173
  } else {
136
- // Multiple bells for jackpot!
174
+ // Multiple bells for jackpot! Tracked so casino.disable() can
175
+ // cancel mid-chain — see _scheduleTracked / cancelAll.
137
176
  playBell();
138
- setTimeout(playBell, 200);
139
- setTimeout(playBell, 400);
177
+ _scheduleTracked(playBell, 200);
178
+ _scheduleTracked(playBell, 400);
140
179
  }
141
180
  }
142
181
 
@@ -146,14 +185,14 @@ function playJackpot() {
146
185
  function playMegaJackpot() {
147
186
  const soundPath = getSoundPath('jackpot');
148
187
  if (soundPath) {
149
- // Play jackpot sound multiple times
188
+ // Play jackpot sound multiple times. Tracked for cancellation.
150
189
  playFile(soundPath, VOLUME.jackpot);
151
- setTimeout(() => playFile(soundPath, VOLUME.jackpot), 300);
152
- setTimeout(() => playFile(soundPath, VOLUME.jackpot), 600);
190
+ _scheduleTracked(() => playFile(soundPath, VOLUME.jackpot), 300);
191
+ _scheduleTracked(() => playFile(soundPath, VOLUME.jackpot), 600);
153
192
  } else {
154
193
  // Lots of bells!
155
194
  for (let i = 0; i < 5; i++) {
156
- setTimeout(playBell, i * 150);
195
+ _scheduleTracked(playBell, i * 150);
157
196
  }
158
197
  }
159
198
  }
@@ -240,6 +279,7 @@ module.exports = {
240
279
  playSpin,
241
280
  playLoss,
242
281
  playForWinLevel,
282
+ cancelAll,
243
283
  getSoundPath,
244
284
  SOUNDS_DIR,
245
285
  };
package/src/server/web.js CHANGED
@@ -46,6 +46,36 @@ const SSE_KEEPALIVE_INTERVAL = 15000;
46
46
  */
47
47
  const MAX_STALLED_PUSHES = 60;
48
48
 
49
+ /**
50
+ * Content-Security-Policy header for the dashboard HTML.
51
+ *
52
+ * The dashboard uses inline <script> and inline <style> blocks (the whole
53
+ * UI is bundled at build time), and makes XHR / EventSource calls to
54
+ * /api/... on the same origin. No external resources are loaded.
55
+ *
56
+ * `default-src 'none'` denies everything not explicitly allowed.
57
+ * `script-src` / `style-src` need 'unsafe-inline' for the bundled blocks.
58
+ * `connect-src 'self'` lets XHR + SSE hit /api/... .
59
+ * `base-uri 'none'` blocks <base> injection from rewriting URLs.
60
+ * `form-action 'none'` blocks any rogue form posts.
61
+ * `frame-ancestors 'none'` blocks embedding (defense vs. clickjacking).
62
+ *
63
+ * Defense-in-depth — the dashboard already escapes user-supplied content
64
+ * everywhere we render it, but a future regression that forgets escHtml
65
+ * is mitigated here.
66
+ */
67
+ const CONTENT_SECURITY_POLICY = [
68
+ "default-src 'none'",
69
+ "script-src 'self' 'unsafe-inline'",
70
+ "style-src 'self' 'unsafe-inline'",
71
+ "connect-src 'self'",
72
+ "img-src 'self' data:",
73
+ "font-src 'self'",
74
+ "base-uri 'none'",
75
+ "form-action 'none'",
76
+ "frame-ancestors 'none'",
77
+ ].join('; ');
78
+
49
79
  /**
50
80
  * Actions the web dashboard is allowed to POST to /api/action. Every entry
51
81
  * here MUST be matched by a `case` in `handleWebAction` in bin/git-watchtower.js
@@ -474,7 +504,12 @@ class WebDashboardServer {
474
504
 
475
505
  // Routes
476
506
  if (pathname === '/' && req.method === 'GET') {
477
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
507
+ res.writeHead(200, {
508
+ 'Content-Type': 'text/html; charset=utf-8',
509
+ 'Content-Security-Policy': CONTENT_SECURITY_POLICY,
510
+ 'X-Content-Type-Options': 'nosniff',
511
+ 'Referrer-Policy': 'no-referrer',
512
+ });
478
513
  res.end(this._cachedHtml);
479
514
  return;
480
515
  }
@@ -673,4 +708,5 @@ module.exports = {
673
708
  STATE_PUSH_INTERVAL,
674
709
  MAX_STALLED_PUSHES,
675
710
  ALLOWED_ACTIONS,
711
+ CONTENT_SECURITY_POLICY,
676
712
  };