fraim-hub 2.0.233 → 2.0.234

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": "fraim-hub",
3
- "version": "2.0.233",
3
+ "version": "2.0.234",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "bin": {
6
6
  "fraim-hub": "bin/fraim-hub.js"
@@ -158,7 +158,7 @@
158
158
  "electron": "^41.2.2",
159
159
  "electron-updater": "^6.8.9",
160
160
  "express": "^5.2.1",
161
- "fraim": "2.0.233",
161
+ "fraim": "2.0.234",
162
162
  "mongodb": "^7.0.0",
163
163
  "node-cron": "4.2.1",
164
164
  "node-edge-tts": "^1.2.10",
@@ -1311,6 +1311,19 @@ function stripHubInjectedPromptBlocks(text) {
1311
1311
  .trim();
1312
1312
  }
1313
1313
 
1314
+ // Returns a validated http/https URL string suitable for use in an href, or null if unsafe.
1315
+ function _safeHref(raw) {
1316
+ try {
1317
+ const url = new URL(raw);
1318
+ return (url.protocol === 'https:' || url.protocol === 'http:') ? url.href : null;
1319
+ } catch (_) { return null; }
1320
+ }
1321
+
1322
+ // Escape & and " in a URL for safe insertion into an href="..." attribute.
1323
+ function _escapeHref(href) {
1324
+ return href.replace(/&/g, '&').replace(/"/g, '"');
1325
+ }
1326
+
1314
1327
  // R8: render markdown subset safely. HTML is escaped first.
1315
1328
  function formatEmployeeText(text) {
1316
1329
  if (!text) return '';
@@ -1326,6 +1339,27 @@ function formatEmployeeText(text) {
1326
1339
  s = s.replace(/```[\w]*\n?([\s\S]*?)```/g, (_, code) => `<pre><code>${code.trimEnd()}</code></pre>`);
1327
1340
  // 3. Inline code (single backtick, non-greedy, no newlines).
1328
1341
  s = s.replace(/`([^`\n]+)`/g, '<code>$1</code>');
1342
+ // 3.5. Markdown links [text](url) — only http/https hrefs allowed; link text is already HTML-escaped.
1343
+ // Empty-text links and non-absolute URLs fall back to plain text.
1344
+ s = s.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (match, linkText, rawUrl) => {
1345
+ const trimmed = linkText.trim();
1346
+ // Empty link text: render as plain text; sentinel prevents step 3.6 auto-linkify.
1347
+ if (!trimmed) return `\x00NOLINK\x00${rawUrl.trim()}\x00ENDNOLINK\x00`;
1348
+ // Reverse HTML entities step 1 introduced inside the URL before structural validation.
1349
+ const decodedUrl = rawUrl.trim().replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
1350
+ const href = _safeHref(decodedUrl);
1351
+ if (!href) return trimmed;
1352
+ return `<a href="${_escapeHref(href)}" target="_blank" rel="noopener noreferrer">${trimmed}</a>`;
1353
+ });
1354
+ // 3.6. Auto-linkify bare https:// / http:// URLs not already inside href="...".
1355
+ // Sentinel prefix prevents double-linkifying empty-text [](url) patterns.
1356
+ s = s.replace(/(?<!href=")(?<!href=')(?<!\x00NOLINK\x00)(https?:\/\/[^\s<>"'\x00]+)/g, (url) => {
1357
+ const href = _safeHref(url);
1358
+ if (!href) return url;
1359
+ return `<a href="${_escapeHref(href)}" target="_blank" rel="noopener noreferrer">${_escapeHref(href)}</a>`;
1360
+ });
1361
+ // Strip sentinels: emit the URL as plain text (no anchor).
1362
+ s = s.replace(/\x00NOLINK\x00(.*?)\x00ENDNOLINK\x00/g, (_, url) => url);
1329
1363
  // 4. Bold (**text**).
1330
1364
  s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1331
1365
  // 5–7. Process line by line for lists and paragraphs.
@@ -1669,6 +1669,15 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
1669
1669
  border: 1px solid var(--line);
1670
1670
  color: var(--text);
1671
1671
  }
1672
+ .message.employee .bubble a {
1673
+ color: var(--accent);
1674
+ text-decoration: underline;
1675
+ text-underline-offset: 2px;
1676
+ word-break: break-all;
1677
+ }
1678
+ .message.employee .bubble a:hover {
1679
+ color: var(--accent-strong);
1680
+ }
1672
1681
  .typing-indicator {
1673
1682
  animation: none;
1674
1683
  justify-items: start;