ccakashic 0.2.7 → 0.3.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.
@@ -573,19 +573,23 @@ body {
573
573
  function getAppJS() {
574
574
  return `
575
575
  (function() {
576
- // Simple markdown renderer (no external dependency)
576
+ function esc(s) {
577
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
578
+ }
579
+
580
+ // Simple markdown renderer (no external dependency).
581
+ // SECURITY: callers pass el.textContent, which DECODES the server's HTML
582
+ // entities back to raw characters, so session content (semi-untrusted: it can
583
+ // contain text from fetched pages/files/tool output) arrives unescaped. We
584
+ // re-escape it up front before applying markdown — markdown syntax chars
585
+ // ([ ] ( ) * \` # -) are untouched by escaping so markdown still renders,
586
+ // while <, >, " stay inert as &lt; &gt; &quot; and cannot inject HTML.
577
587
  function renderMarkdown(text) {
578
- // Escape HTML first (already escaped by server, but for client-side re-render)
579
- // Actually the text is already HTML-escaped, so we need to unescape first
580
- text = text
581
- .replace(/&amp;/g, '&')
582
- .replace(/&lt;/g, '<')
583
- .replace(/&gt;/g, '>')
584
- .replace(/&quot;/g, '"');
585
-
586
- // Code blocks (fenced)
588
+ text = esc(text);
589
+
590
+ // Code blocks (fenced) — content is already escaped, do not re-escape
587
591
  text = text.replace(/\`\`\`(\\w*)?\\n([\\s\\S]*?)\`\`\`/g, function(m, lang, code) {
588
- return '<pre><code class="language-' + (lang || '') + '">' + esc(code.trim()) + '</code></pre>';
592
+ return '<pre><code class="language-' + (lang || '') + '">' + code.trim() + '</code></pre>';
589
593
  });
590
594
 
591
595
  // Inline code
@@ -606,8 +610,15 @@ function getAppJS() {
606
610
  text = text.replace(/^[\\-*] (.+)$/gm, '<li>$1</li>');
607
611
  text = text.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
608
612
 
609
- // Links
610
- text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
613
+ // Links — href is already escaped (so " is &quot; and can't break out of
614
+ // the attribute), but still allowlist safe schemes to block javascript:.
615
+ // The leading-slash branch uses (?!\\/) so protocol-relative //evil.com
616
+ // (which navigates off-site) is rejected — only same-origin /path passes.
617
+ text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, function(m, label, href) {
618
+ return /^(https?:\\/\\/|\\/(?!\\/)|#)/i.test(href)
619
+ ? '<a href="' + href + '" target="_blank" rel="noopener">' + label + '</a>'
620
+ : label;
621
+ });
611
622
 
612
623
  // Paragraphs (simple: double newlines)
613
624
  text = text.replace(/\\n\\n/g, '</p><p>');
@@ -625,14 +636,15 @@ function getAppJS() {
625
636
  return text;
626
637
  }
627
638
 
628
- function esc(s) {
629
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
639
+ // Render markdown in all [data-markdown] elements. Exposed globally so the
640
+ // dashboard can re-apply it to pane content replaced after a poll.
641
+ function applyMarkdown(root) {
642
+ root.querySelectorAll('[data-markdown]').forEach(function(el) {
643
+ el.innerHTML = renderMarkdown(el.textContent);
644
+ });
630
645
  }
631
-
632
- // Render markdown in all [data-markdown] elements
633
- document.querySelectorAll('[data-markdown]').forEach(function(el) {
634
- el.innerHTML = renderMarkdown(el.textContent);
635
- });
646
+ window.ccakashicApplyMarkdown = applyMarkdown;
647
+ applyMarkdown(document);
636
648
 
637
649
  // Scroll to bottom when hash is #session-bottom. We re-scroll here (after
638
650
  // markdown rendering) because the DOM height grows when markdown expands,
package/dist/util.js ADDED
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // Shared helpers used across the HTML generators and the server.
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.ACTIVE_THRESHOLD_MS = void 0;
38
+ exports.getOrCreateToken = getOrCreateToken;
39
+ exports.escapeHtml = escapeHtml;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const crypto = __importStar(require("crypto"));
44
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccakashic');
45
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
46
+ // The /api/resume CSRF token, persisted so it survives a server restart and the
47
+ // buttons on already-open tabs keep working without a reload. Falls back to an
48
+ // in-memory token if the file can't be read/written. DNS rebinding is already
49
+ // blocked by the Host-header check, so this token is defense-in-depth.
50
+ function getOrCreateToken() {
51
+ try {
52
+ const existing = fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
53
+ if (/^[a-f0-9]{32,}$/.test(existing))
54
+ return existing;
55
+ }
56
+ catch {
57
+ // not created yet
58
+ }
59
+ const token = crypto.randomBytes(16).toString('hex');
60
+ try {
61
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
62
+ fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
63
+ }
64
+ catch {
65
+ // fall back to an ephemeral token (rotates on restart)
66
+ }
67
+ return token;
68
+ }
69
+ function escapeHtml(str) {
70
+ return String(str)
71
+ .replace(/&/g, '&amp;')
72
+ .replace(/</g, '&lt;')
73
+ .replace(/>/g, '&gt;')
74
+ .replace(/"/g, '&quot;')
75
+ .replace(/'/g, '&#39;');
76
+ }
77
+ // A session whose file was touched within this window is treated as still
78
+ // attached to a live `claude` process. Shared so the dashboard status dot and
79
+ // the resume fork-confirmation use the exact same definition of "active".
80
+ exports.ACTIVE_THRESHOLD_MS = 2 * 60_000;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ccakashic",
3
- "version": "0.2.7",
4
- "description": "Browse Claude Code session logs (~/.claude/projects/) as beautiful HTML in your browser an Akashic Record of your Claude Code sessions",
3
+ "version": "0.3.0",
4
+ "description": "A cross-project dashboard for your Claude Code sessions (~/.claude/projects/) — browse logs as beautiful HTML, see which sessions are waiting for you, and resume any of them in one click via cmux",
5
5
  "bin": {
6
6
  "ccakashic": "dist/bin/ccakashic.js"
7
7
  },