@lionrapid/core 0.1.3-beta.0 → 0.1.4-beta.2

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.
Files changed (56) hide show
  1. package/dist/browser/editor/editor-api.d.ts +9 -0
  2. package/dist/browser/editor/editor-api.d.ts.map +1 -1
  3. package/dist/browser/editor/editor-api.js +29 -0
  4. package/dist/browser/editor/editor-api.js.map +1 -1
  5. package/dist/browser/editor/editor-init.js +1 -1
  6. package/dist/browser/editor/editor-init.js.map +1 -1
  7. package/dist/browser/editor/editor-styles.d.ts +1 -1
  8. package/dist/browser/editor/editor-styles.d.ts.map +1 -1
  9. package/dist/browser/editor/editor-styles.js +52 -0
  10. package/dist/browser/editor/editor-styles.js.map +1 -1
  11. package/dist/browser/editor/pii-redactor.d.ts +39 -0
  12. package/dist/browser/editor/pii-redactor.d.ts.map +1 -0
  13. package/dist/browser/editor/pii-redactor.js +149 -0
  14. package/dist/browser/editor/pii-redactor.js.map +1 -0
  15. package/dist/browser/editor/sidebar.d.ts +17 -1
  16. package/dist/browser/editor/sidebar.d.ts.map +1 -1
  17. package/dist/browser/editor/sidebar.js +77 -1
  18. package/dist/browser/editor/sidebar.js.map +1 -1
  19. package/dist/browser/editor/snapshot-capture.d.ts +4 -4
  20. package/dist/browser/editor/snapshot-capture.js +11 -25
  21. package/dist/browser/editor/snapshot-capture.js.map +1 -1
  22. package/dist/browser/lionrapid.auto.dev.js +34 -3
  23. package/dist/browser/lionrapid.auto.dev.js.map +3 -3
  24. package/dist/browser/lionrapid.auto.js +34 -3
  25. package/dist/browser/lionrapid.auto.js.map +3 -3
  26. package/dist/browser/lionrapid.auto.min.js +3 -3
  27. package/dist/browser/lionrapid.auto.min.js.map +4 -4
  28. package/dist/browser/lionrapid.editor.js +256 -24
  29. package/dist/browser/lionrapid.editor.js.map +4 -4
  30. package/dist/browser/lionrapid.editor.min.js +55 -3
  31. package/dist/browser/lionrapid.editor.min.js.map +4 -4
  32. package/dist/browser/lionrapid.esm.js +34 -3
  33. package/dist/browser/lionrapid.esm.js.map +3 -3
  34. package/dist/browser/lionrapid.esm.min.js +2 -2
  35. package/dist/browser/lionrapid.esm.min.js.map +4 -4
  36. package/dist/browser/lionrapid.iife.dev.js +34 -3
  37. package/dist/browser/lionrapid.iife.dev.js.map +3 -3
  38. package/dist/browser/lionrapid.iife.js +34 -3
  39. package/dist/browser/lionrapid.iife.js.map +3 -3
  40. package/dist/browser/lionrapid.iife.min.js +2 -2
  41. package/dist/browser/lionrapid.iife.min.js.map +4 -4
  42. package/dist/browser/lionrapid.umd.js +34 -3
  43. package/dist/browser/lionrapid.umd.js.map +3 -3
  44. package/dist/browser/lionrapid.umd.min.js +2 -2
  45. package/dist/browser/lionrapid.umd.min.js.map +4 -4
  46. package/dist/content/safe-href.d.ts +22 -0
  47. package/dist/content/safe-href.d.ts.map +1 -0
  48. package/dist/content/safe-href.js +48 -0
  49. package/dist/content/safe-href.js.map +1 -0
  50. package/dist/content/style-mapper/DefaultStyleMapper.d.ts.map +1 -1
  51. package/dist/content/style-mapper/DefaultStyleMapper.js +5 -1
  52. package/dist/content/style-mapper/DefaultStyleMapper.js.map +1 -1
  53. package/dist/utils/logger.d.ts.map +1 -1
  54. package/dist/utils/logger.js +29 -3
  55. package/dist/utils/logger.js.map +1 -1
  56. package/package.json +1 -1
@@ -174,6 +174,58 @@ var LionRapidEditor = (() => {
174
174
  cursor: not-allowed;
175
175
  }
176
176
 
177
+ /* Workflow action buttons (Phase 3) */
178
+ .lr-workflow-actions {
179
+ display: flex;
180
+ flex-wrap: wrap;
181
+ gap: 6px;
182
+ margin: 12px 0 4px;
183
+ padding: 8px 0 0;
184
+ border-top: 1px solid #e5e7eb;
185
+ }
186
+
187
+ .lr-wf-btn {
188
+ font-size: 12px;
189
+ font-weight: 500;
190
+ padding: 5px 10px;
191
+ border-radius: 4px;
192
+ border: 1px solid transparent;
193
+ cursor: pointer;
194
+ transition: background-color 0.1s;
195
+ }
196
+
197
+ .lr-wf-btn:disabled {
198
+ opacity: 0.5;
199
+ cursor: not-allowed;
200
+ }
201
+
202
+ .lr-wf-primary {
203
+ background: #2563eb;
204
+ color: white;
205
+ border-color: #2563eb;
206
+ }
207
+ .lr-wf-primary:hover:not(:disabled) {
208
+ background: #1d4ed8;
209
+ }
210
+
211
+ .lr-wf-secondary {
212
+ background: white;
213
+ color: #374151;
214
+ border-color: #d1d5db;
215
+ }
216
+ .lr-wf-secondary:hover:not(:disabled) {
217
+ background: #f3f4f6;
218
+ }
219
+
220
+ .lr-wf-danger {
221
+ background: white;
222
+ color: #b91c1c;
223
+ border-color: #fca5a5;
224
+ }
225
+ .lr-wf-danger:hover:not(:disabled) {
226
+ background: #fef2f2;
227
+ }
228
+
177
229
  /* Snapshot viewer sandbox wrapper */
178
230
  #__lr_snapshot_root {
179
231
  isolation: isolate;
@@ -449,6 +501,27 @@ var LionRapidEditor = (() => {
449
501
  }
450
502
  `;
451
503
 
504
+ // src/content/safe-href.ts
505
+ var SAFE_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
506
+ var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "sms:"]);
507
+ function safeHref(href) {
508
+ if (!href) return "#";
509
+ const trimmed = href.trim();
510
+ if (!trimmed) return "#";
511
+ if (trimmed.startsWith("//")) {
512
+ return "#";
513
+ }
514
+ if (trimmed.startsWith("/") || trimmed.startsWith("?") || trimmed.startsWith("#") || trimmed.startsWith("./") || trimmed.startsWith("../")) {
515
+ return trimmed;
516
+ }
517
+ const schemeMatch = SAFE_SCHEME.exec(trimmed);
518
+ if (schemeMatch) {
519
+ const scheme = schemeMatch[0].toLowerCase();
520
+ return ALLOWED_SCHEMES.has(scheme) ? trimmed : "#";
521
+ }
522
+ return trimmed;
523
+ }
524
+
452
525
  // src/content/style-mapper/DefaultStyleMapper.ts
453
526
  var DefaultStyleMapper = class {
454
527
  constructor() {
@@ -534,7 +607,7 @@ var LionRapidEditor = (() => {
534
607
  if (style.startsWith("hyperlink:")) {
535
608
  return {
536
609
  element: "a",
537
- attrs: { href: style.slice("hyperlink:".length) }
610
+ attrs: { href: safeHref(style.slice("hyperlink:".length)) }
538
611
  };
539
612
  }
540
613
  }
@@ -1274,7 +1347,7 @@ var LionRapidEditor = (() => {
1274
1347
  // src/browser/editor/sidebar.ts
1275
1348
  var htmlSerializer2 = new HTMLSerializer();
1276
1349
  var Sidebar = class {
1277
- constructor(shadowRoot, api, locale, namespace, defaultLocale, onBlockClick) {
1350
+ constructor(shadowRoot, api, locale, namespace, defaultLocale, onBlockClick, userRole = "") {
1278
1351
  __publicField(this, "el");
1279
1352
  __publicField(this, "bodyEl");
1280
1353
  __publicField(this, "statusEl");
@@ -1290,11 +1363,13 @@ var LionRapidEditor = (() => {
1290
1363
  __publicField(this, "onProgressUpdate", null);
1291
1364
  __publicField(this, "lastFetchTime", 0);
1292
1365
  __publicField(this, "lastFlushTime", 0);
1366
+ __publicField(this, "userRole");
1293
1367
  this.api = api;
1294
1368
  this.locale = locale;
1295
1369
  this.namespace = namespace;
1296
1370
  this.defaultLocale = defaultLocale;
1297
1371
  this.onBlockClick = onBlockClick;
1372
+ this.userRole = userRole;
1298
1373
  this.blockExtractor = new BlockExtractor();
1299
1374
  this.el = document.createElement("div");
1300
1375
  this.el.className = "lr-sidebar";
@@ -1448,6 +1523,9 @@ var LionRapidEditor = (() => {
1448
1523
  }
1449
1524
  return previewEl;
1450
1525
  });
1526
+ if (!isSourceLocale) {
1527
+ this.renderWorkflowActions(key, status);
1528
+ }
1451
1529
  if (!isLocked) {
1452
1530
  textarea.addEventListener("input", () => {
1453
1531
  const text = textarea.value;
@@ -1470,6 +1548,69 @@ var LionRapidEditor = (() => {
1470
1548
  setTimeout(() => textarea.focus(), 100);
1471
1549
  }
1472
1550
  }
1551
+ /**
1552
+ * Render Confirm/Reject/Mark-Ready/Unmark-Ready buttons under the preview.
1553
+ * Visibility:
1554
+ * - Confirm: shown when row is not locked and not already ready
1555
+ * - Reject: shown when row is not locked and not already ready (server
1556
+ * returns 400 if stage_index is already 0)
1557
+ * - Unconfirm: shown when isConfirmed
1558
+ * - Mark Ready: PM/admin only, hidden when already ready
1559
+ * - Unmark Ready: PM/admin only, only when ready
1560
+ * After any successful click we re-fetch status for this key so the UI
1561
+ * reflects the new state and the unaffected buttons hide/show themselves.
1562
+ */
1563
+ renderWorkflowActions(key, status) {
1564
+ const isPM = this.userRole === "project_manager" || this.userRole === "admin";
1565
+ const isReady = !!status?.isReady;
1566
+ const isLocked = !!status?.isLocked;
1567
+ const isConfirmed = !!status?.isConfirmed;
1568
+ const composite = `${this.locale}:${this.namespace}:${key}`;
1569
+ const wrap = document.createElement("div");
1570
+ wrap.className = "lr-workflow-actions";
1571
+ const addBtn = (label, action, variant = "secondary") => {
1572
+ const btn = document.createElement("button");
1573
+ btn.className = `lr-wf-btn lr-wf-${variant}`;
1574
+ btn.textContent = label;
1575
+ btn.addEventListener("click", async () => {
1576
+ btn.disabled = true;
1577
+ const result = await this.api.transitionTarget(composite, action);
1578
+ btn.disabled = false;
1579
+ if (result) {
1580
+ await this.refreshStatusForKey(key);
1581
+ }
1582
+ });
1583
+ wrap.appendChild(btn);
1584
+ };
1585
+ if (!isLocked && !isReady) {
1586
+ addBtn("Confirm", "confirm", "primary");
1587
+ addBtn("Reject", "reject", "danger");
1588
+ }
1589
+ if (isConfirmed && !isLocked && !isReady) {
1590
+ addBtn("Unconfirm", "unconfirm");
1591
+ }
1592
+ if (isPM) {
1593
+ if (!isReady) {
1594
+ addBtn("Mark Ready", "mark-ready", "primary");
1595
+ } else {
1596
+ addBtn("Unmark Ready", "unmark-ready", "danger");
1597
+ }
1598
+ }
1599
+ if (wrap.children.length > 0) {
1600
+ this.bodyEl.appendChild(wrap);
1601
+ }
1602
+ }
1603
+ /** Re-fetch status for one key and re-render the current view. */
1604
+ async refreshStatusForKey(key) {
1605
+ const fresh = await this.api.fetchStatus([key], this.locale, this.namespace);
1606
+ for (const [k, s] of Object.entries(fresh)) {
1607
+ this.statusMap.set(k, s);
1608
+ }
1609
+ if (this.current) {
1610
+ this.renderContent(this.current);
1611
+ }
1612
+ this.emitProgress();
1613
+ }
1473
1614
  addField(label, contentFn) {
1474
1615
  const field = document.createElement("div");
1475
1616
  field.className = "lr-field";
@@ -1930,10 +2071,118 @@ var LionRapidEditor = (() => {
1930
2071
  return false;
1931
2072
  }
1932
2073
  }
2074
+ /**
2075
+ * Workflow transitions (Phase 3). Per-key endpoints on the integrations API
2076
+ * that mirror the dashboard's row-level transitions but are addressable by
2077
+ * composite editor key (locale:namespace:auto_xxx).
2078
+ *
2079
+ * Returns the parsed JSON on success, or null on failure (caller usually
2080
+ * shows a toast / re-fetches status).
2081
+ */
2082
+ async transitionTarget(composite, action) {
2083
+ try {
2084
+ const response = await fetch(
2085
+ `${this.apiBase}/api/integrations/targets/${encodeURIComponent(composite)}/${action}`,
2086
+ {
2087
+ method: "POST",
2088
+ headers: { "Authorization": `Bearer ${this.sessionToken}` }
2089
+ }
2090
+ );
2091
+ if (response.status === 401) {
2092
+ this.handleSessionExpired();
2093
+ return null;
2094
+ }
2095
+ if (!response.ok) {
2096
+ this.onStatus("error", `${action} failed: HTTP ${response.status}`);
2097
+ return null;
2098
+ }
2099
+ return await response.json();
2100
+ } catch (err) {
2101
+ this.onStatus("error", `${action} failed: ${err.message}`);
2102
+ return null;
2103
+ }
2104
+ }
1933
2105
  };
1934
2106
 
1935
- // src/browser/editor/snapshot-capture.ts
2107
+ // src/browser/editor/pii-redactor.ts
1936
2108
  var REDACTED = "[REDACTED]";
2109
+ var EMAIL_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi;
2110
+ var PHONE_RE = /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}/g;
2111
+ var SSN_RE = /\b\d{3}[\s-]\d{2}[\s-]\d{4}\b/g;
2112
+ var IPV4_RE = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g;
2113
+ var URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"']+/gi;
2114
+ var CC_CANDIDATE_RE = /\b\d(?:[ -]?\d){12,18}\b/g;
2115
+ function luhnValid(digits) {
2116
+ if (digits.length < 13 || digits.length > 19) return false;
2117
+ let sum = 0;
2118
+ let alt = false;
2119
+ for (let i = digits.length - 1; i >= 0; i--) {
2120
+ let n = digits.charCodeAt(i) - 48;
2121
+ if (n < 0 || n > 9) return false;
2122
+ if (alt) {
2123
+ n *= 2;
2124
+ if (n > 9) n -= 9;
2125
+ }
2126
+ sum += n;
2127
+ alt = !alt;
2128
+ }
2129
+ return sum % 10 === 0;
2130
+ }
2131
+ function ipv4Valid(match) {
2132
+ const parts = match.split(".");
2133
+ if (parts.length !== 4) return false;
2134
+ for (const p of parts) {
2135
+ const n = Number(p);
2136
+ if (!Number.isInteger(n) || n < 0 || n > 255) return false;
2137
+ }
2138
+ return true;
2139
+ }
2140
+ function redactString(text) {
2141
+ if (!text) return text;
2142
+ let out = text;
2143
+ out = out.replace(URL_RE, REDACTED);
2144
+ out = out.replace(EMAIL_RE, REDACTED);
2145
+ out = out.replace(PHONE_RE, REDACTED);
2146
+ out = out.replace(SSN_RE, REDACTED);
2147
+ out = out.replace(IPV4_RE, (m) => ipv4Valid(m) ? REDACTED : m);
2148
+ out = out.replace(CC_CANDIDATE_RE, (m) => {
2149
+ const digits = m.replace(/[\s-]/g, "");
2150
+ return luhnValid(digits) ? REDACTED : m;
2151
+ });
2152
+ return out;
2153
+ }
2154
+ var SKIP_TAGS3 = /* @__PURE__ */ new Set(["CODE", "PRE", "STYLE", "TEXTAREA"]);
2155
+ function redactTextNodes(root) {
2156
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2157
+ const toUpdate = [];
2158
+ let node = walker.nextNode();
2159
+ while (node) {
2160
+ if (!isInSkippedSubtree(node)) {
2161
+ const original = node.nodeValue || "";
2162
+ const replaced = redactString(original);
2163
+ if (replaced !== original) {
2164
+ toUpdate.push({ node, value: replaced });
2165
+ }
2166
+ }
2167
+ node = walker.nextNode();
2168
+ }
2169
+ for (const { node: textNode, value } of toUpdate) {
2170
+ textNode.nodeValue = value;
2171
+ }
2172
+ }
2173
+ function isInSkippedSubtree(node) {
2174
+ let cur = node.parentNode;
2175
+ while (cur && cur.nodeType === Node.ELEMENT_NODE) {
2176
+ const el = cur;
2177
+ if (SKIP_TAGS3.has(el.tagName)) return true;
2178
+ if (el.hasAttribute("data-no-redact")) return true;
2179
+ cur = el.parentNode;
2180
+ }
2181
+ return false;
2182
+ }
2183
+
2184
+ // src/browser/editor/snapshot-capture.ts
2185
+ var REDACTED2 = "[REDACTED]";
1937
2186
  function captureSnapshot() {
1938
2187
  const clone = document.documentElement.cloneNode(true);
1939
2188
  const textEntries = extractTextEntries(clone);
@@ -2001,7 +2250,7 @@ var LionRapidEditor = (() => {
2001
2250
  const sensitive = root.querySelectorAll("[data-pii], [data-sensitive]");
2002
2251
  for (let i = 0; i < sensitive.length; i++) {
2003
2252
  const el = sensitive[i];
2004
- el.textContent = REDACTED;
2253
+ el.textContent = REDACTED2;
2005
2254
  const attrs = Array.from(el.attributes);
2006
2255
  for (const attr of attrs) {
2007
2256
  if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
@@ -2024,24 +2273,7 @@ var LionRapidEditor = (() => {
2024
2273
  }
2025
2274
  }
2026
2275
  function redactTextNodePII(root) {
2027
- const filter = new ExcludeFilter(
2028
- { emails: true, urls: true, numbers: true, phones: true },
2029
- 1
2030
- // minTextLength=1 so single-char filler still passes through
2031
- );
2032
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2033
- const nodesToRedact = [];
2034
- let node = walker.nextNode();
2035
- while (node) {
2036
- const trimmed = (node.nodeValue || "").trim();
2037
- if (trimmed.length >= 5 && filter.shouldExclude(trimmed)) {
2038
- nodesToRedact.push(node);
2039
- }
2040
- node = walker.nextNode();
2041
- }
2042
- for (const textNode of nodesToRedact) {
2043
- textNode.nodeValue = REDACTED;
2044
- }
2276
+ redactTextNodes(root);
2045
2277
  }
2046
2278
  function stripSensitiveMetaAndHeaders(root) {
2047
2279
  const sensitiveMetas = root.querySelectorAll(
@@ -2062,7 +2294,7 @@ var LionRapidEditor = (() => {
2062
2294
  let changed = false;
2063
2295
  for (const param of TOKEN_PARAMS) {
2064
2296
  if (url.searchParams.has(param)) {
2065
- url.searchParams.set(param, REDACTED);
2297
+ url.searchParams.set(param, REDACTED2);
2066
2298
  changed = true;
2067
2299
  }
2068
2300
  }
@@ -2306,7 +2538,7 @@ var LionRapidEditor = (() => {
2306
2538
  sidebar.open(resolved);
2307
2539
  inspector.clearHighlight();
2308
2540
  };
2309
- const sidebar = new Sidebar(shadow, api, currentLocale, namespace, defaultLocale, handleBlockSelect);
2541
+ const sidebar = new Sidebar(shadow, api, currentLocale, namespace, defaultLocale, handleBlockSelect, role);
2310
2542
  const inspector = new Inspector(shadow, handleBlockSelect, inspectorRoot, snapshotKeyMap);
2311
2543
  const toolbar = new Toolbar(
2312
2544
  shadow,