@favish/staffbase-utils 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/dist/html.cjs.js +2 -0
  2. package/dist/html.cjs.js.map +1 -0
  3. package/dist/html.es.mjs +49 -0
  4. package/dist/html.es.mjs.map +1 -0
  5. package/dist/links.cjs.js +2 -0
  6. package/dist/links.cjs.js.map +1 -0
  7. package/dist/links.es.mjs +98 -0
  8. package/dist/links.es.mjs.map +1 -0
  9. package/dist/src/html/cleanHTML.d.ts +9 -0
  10. package/dist/src/html/cleanHTML.d.ts.map +1 -0
  11. package/dist/src/html/index.d.ts +5 -0
  12. package/dist/src/html/index.d.ts.map +1 -0
  13. package/dist/src/html/sanitizeArticleHtml.d.ts +15 -0
  14. package/dist/src/html/sanitizeArticleHtml.d.ts.map +1 -0
  15. package/dist/src/html/sanitizeHtml.d.ts +13 -0
  16. package/dist/src/html/sanitizeHtml.d.ts.map +1 -0
  17. package/dist/src/html/stripHtmlTags.d.ts +10 -0
  18. package/dist/src/html/stripHtmlTags.d.ts.map +1 -0
  19. package/dist/src/links/getInAppOpenLinkTarget.d.ts +11 -0
  20. package/dist/src/links/getInAppOpenLinkTarget.d.ts.map +1 -0
  21. package/dist/src/links/index.d.ts +7 -0
  22. package/dist/src/links/index.d.ts.map +1 -0
  23. package/dist/src/links/isAllowedIframeSrc.d.ts +9 -0
  24. package/dist/src/links/isAllowedIframeSrc.d.ts.map +1 -0
  25. package/dist/src/links/isPromiseLike.d.ts +8 -0
  26. package/dist/src/links/isPromiseLike.d.ts.map +1 -0
  27. package/dist/src/links/isSafeNavigationHref.d.ts +10 -0
  28. package/dist/src/links/isSafeNavigationHref.d.ts.map +1 -0
  29. package/dist/src/links/normalizeInAppLinks.d.ts +14 -0
  30. package/dist/src/links/normalizeInAppLinks.d.ts.map +1 -0
  31. package/dist/src/links/openStaffbaseAware.d.ts +9 -0
  32. package/dist/src/links/openStaffbaseAware.d.ts.map +1 -0
  33. package/dist/src/links/stripStaffbaseLinkPrefix.d.ts +8 -0
  34. package/dist/src/links/stripStaffbaseLinkPrefix.d.ts.map +1 -0
  35. package/dist/src/links/tryOpenWithStaffbase.d.ts +10 -0
  36. package/dist/src/links/tryOpenWithStaffbase.d.ts.map +1 -0
  37. package/dist/src/types/html/SanitizeArticleHtmlOptions.d.ts +15 -0
  38. package/dist/src/types/html/SanitizeArticleHtmlOptions.d.ts.map +1 -0
  39. package/dist/src/types/links/TryOpenResult.d.ts +10 -0
  40. package/dist/src/types/links/TryOpenResult.d.ts.map +1 -0
  41. package/dist/src/types/links/WindowWithStaffbase.d.ts +14 -0
  42. package/dist/src/types/links/WindowWithStaffbase.d.ts.map +1 -0
  43. package/package.json +14 -1
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("dompurify");c=s(c);var l=e=>{if(!e)return``;try{return c.default.sanitize(e,{ALLOWED_TAGS:[],KEEP_CONTENT:!0})}catch{return e.replace(/<\/?[^>]+(>|$)/g,``)}},u=e=>l(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g,` `).replace(/\s+/g,` `).trim(),d=(0,c.default)(window),f=null;d.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&f)){let t=e.getAttribute(`src`)??``;f(t)||e.parentNode?.removeChild(e)}});var p=(e,t={})=>{f=t.isAllowedIframeSrc??null;try{return d.sanitize(e,{USE_PROFILES:{html:!0},ADD_TAGS:[`iframe`],ADD_ATTR:[`target`,`allow`,`allowfullscreen`,`frameborder`,`scrolling`,`loading`,`referrerpolicy`],FORBID_TAGS:[`script`,`style`],FORBID_ATTR:[`onerror`,`onload`,`onclick`]})}finally{f=null}},m=e=>c.default.sanitize(e);exports.cleanHTML=u,exports.sanitizeArticleHtml=p,exports.sanitizeHtml=m,exports.stripHtmlTags=l;
2
+ //# sourceMappingURL=html.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n return purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. From global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => DOMPurify.sanitize(html)\n"],"mappings":"mkBAUA,IAAa,EAAiB,GAA0B,CACtD,GAAI,CAAC,EAAM,MAAO,GAElB,GAAI,CACF,OAAO,EAAA,QAAU,SAAS,EAAM,CAAE,aAAc,CAAC,EAAG,aAAc,EAAK,CAAC,CAC1E,MAAQ,CACN,OAAO,EAAK,QAAQ,kBAAmB,EAAE,CAC3C,CACF,ECTa,EAAa,GACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,yBAA0B,GAAG,EACrC,QAAQ,OAAQ,GAAG,EACnB,KAAK,ECPJ,GAAA,EAAA,EAAA,SAA2B,MAAM,EAInC,EAAuD,KAK3D,EAAS,QAAQ,0BAA4B,GAAS,CAC9C,gBAAgB,UAElB,EAAK,UAAY,KAAO,EAAK,aAAa,QAAQ,IAAM,UAC1D,EAAK,aAAa,MAAO,qBAAqB,EAG5C,EAAK,UAAY,UAAY,GAAmB,CAClD,IAAM,EAAM,EAAK,aAAa,KAAK,GAAK,GACnC,EAAkB,CAAG,GAAG,EAAK,YAAY,YAAY,CAAI,CAChE,CACF,CAAC,EAcD,IAAa,GACX,EACA,EAAsC,CAAC,IAC5B,CACX,EAAoB,EAAQ,oBAAsB,KAClD,GAAI,CACF,OAAO,EAAS,SAAS,EAAM,CAC7B,aAAc,CAAE,KAAM,EAAK,EAC3B,SAAU,CAAC,QAAQ,EACnB,SAAU,CACR,SACA,QACA,kBACA,cACA,YACA,UACA,gBACF,EACA,YAAa,CAAC,SAAU,OAAO,EAC/B,YAAa,CAAC,UAAW,SAAU,SAAS,CAC9C,CAAC,CACH,QAAU,CACR,EAAoB,IACtB,CACF,ECpDa,EAAgB,GAAyB,EAAA,QAAU,SAAS,CAAI"}
@@ -0,0 +1,49 @@
1
+ import e from "dompurify";
2
+ //#region src/html/stripHtmlTags.ts
3
+ var t = (t) => {
4
+ if (!t) return "";
5
+ try {
6
+ return e.sanitize(t, {
7
+ ALLOWED_TAGS: [],
8
+ KEEP_CONTENT: !0
9
+ });
10
+ } catch {
11
+ return t.replace(/<\/?[^>]+(>|$)/g, "");
12
+ }
13
+ }, n = (e) => t(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g, " ").replace(/\s+/g, " ").trim(), r = e(window), i = null;
14
+ r.addHook("afterSanitizeAttributes", (e) => {
15
+ if (e instanceof Element && (e.tagName === "A" && e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer"), e.tagName === "IFRAME" && i)) {
16
+ let t = e.getAttribute("src") ?? "";
17
+ i(t) || e.parentNode?.removeChild(e);
18
+ }
19
+ });
20
+ var a = (e, t = {}) => {
21
+ i = t.isAllowedIframeSrc ?? null;
22
+ try {
23
+ return r.sanitize(e, {
24
+ USE_PROFILES: { html: !0 },
25
+ ADD_TAGS: ["iframe"],
26
+ ADD_ATTR: [
27
+ "target",
28
+ "allow",
29
+ "allowfullscreen",
30
+ "frameborder",
31
+ "scrolling",
32
+ "loading",
33
+ "referrerpolicy"
34
+ ],
35
+ FORBID_TAGS: ["script", "style"],
36
+ FORBID_ATTR: [
37
+ "onerror",
38
+ "onload",
39
+ "onclick"
40
+ ]
41
+ });
42
+ } finally {
43
+ i = null;
44
+ }
45
+ }, o = (t) => e.sanitize(t);
46
+ //#endregion
47
+ export { n as cleanHTML, a as sanitizeArticleHtml, o as sanitizeHtml, t as stripHtmlTags };
48
+
49
+ //# sourceMappingURL=html.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.es.mjs","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n return purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. From global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => DOMPurify.sanitize(html)\n"],"mappings":";;AAUA,IAAa,KAAiB,MAA0B;CACtD,IAAI,CAAC,GAAM,OAAO;CAElB,IAAI;EACF,OAAO,EAAU,SAAS,GAAM;GAAE,cAAc,CAAC;GAAG,cAAc;EAAK,CAAC;CAC1E,QAAQ;EACN,OAAO,EAAK,QAAQ,mBAAmB,EAAE;CAC3C;AACF,GCTa,KAAa,MACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,0BAA0B,GAAG,EACrC,QAAQ,QAAQ,GAAG,EACnB,KAAK,GCPJ,IAAW,EAAgB,MAAM,GAInC,IAAuD;AAK3D,EAAS,QAAQ,4BAA4B,MAAS;CAC9C,iBAAgB,YAElB,EAAK,YAAY,OAAO,EAAK,aAAa,QAAQ,MAAM,YAC1D,EAAK,aAAa,OAAO,qBAAqB,GAG5C,EAAK,YAAY,YAAY,IAAmB;EAClD,IAAM,IAAM,EAAK,aAAa,KAAK,KAAK;EACxC,AAAK,EAAkB,CAAG,KAAG,EAAK,YAAY,YAAY,CAAI;CAChE;AACF,CAAC;AAcD,IAAa,KACX,GACA,IAAsC,CAAC,MAC5B;CACX,IAAoB,EAAQ,sBAAsB;CAClD,IAAI;EACF,OAAO,EAAS,SAAS,GAAM;GAC7B,cAAc,EAAE,MAAM,GAAK;GAC3B,UAAU,CAAC,QAAQ;GACnB,UAAU;IACR;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GACA,aAAa,CAAC,UAAU,OAAO;GAC/B,aAAa;IAAC;IAAW;IAAU;GAAS;EAC9C,CAAC;CACH,UAAU;EACR,IAAoB;CACtB;AACF,GCpDa,KAAgB,MAAyB,EAAU,SAAS,CAAI"}
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=e=>e.startsWith(`/deeplink/`)||e.startsWith(`/openlink/`)?`/${e.slice(10)}`:e,t=(t,n)=>{let r=t?.trim();if(!r||r.startsWith(`#`))return null;let i=r.toLowerCase();if(i.startsWith(`mailto:`)||i.startsWith(`tel:`)||i.startsWith(`sms:`)||i.startsWith(`javascript:`)||i.startsWith(`data:`))return null;try{let t=new URL(n),i=new URL(r,t.toString());if(i.origin===t.origin){let n=e(i.pathname);return new URL(`${n}${i.search}${i.hash}`,t.origin).toString()}return i.toString()}catch{return null}},n=[`youtube.com`,`youtube-nocookie.com`,`youtu.be`,`vimeo.com`,`player.vimeo.com`,`staffbase.com`,`staffbase.rocks`],r=e=>{let t=e.trim();if(!t)return!1;try{let e=typeof window<`u`?window.location.origin:`https://localhost`,r=new URL(t,e);if(r.protocol!==`https:`&&r.protocol!==`http:`)return!1;if(r.origin===e)return!0;let i=r.hostname.toLowerCase();return n.some(e=>i===e||i.endsWith(`.${e}`))}catch{return!1}},i=e=>{let t=e.trim().toLowerCase();return t?!(t.startsWith(`javascript:`)||t.startsWith(`data:`)||t.startsWith(`vbscript:`)):!1},a=(e,t)=>{let n=t?.trim()||``;if(!n)return;let r=Array.from(e.querySelectorAll(`a[href]`));for(let e of r){let t=e.getAttribute(`href`);if(!t)continue;let r=t.trim().toLowerCase();if(!(r.startsWith(`#`)||r.startsWith(`mailto:`)||r.startsWith(`tel:`)||r.startsWith(`sms:`)||r.startsWith(`javascript:`)||r.startsWith(`data:`)))try{let r=new URL(t,n);if(r.origin!==n){e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`);continue}let i=`${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//,`/`).replace(/^\/openlink\//,`/`);e.setAttribute(`href`,i),e.classList.add(`internal-link`),e.removeAttribute(`target`),e.removeAttribute(`rel`)}catch{}}},o=e=>typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`,s=e=>{try{if(typeof window>`u`)return{handled:!1};let t=window?.staffbase?.plugin?.util?.openLink;if(typeof t!=`function`)return{handled:!1};let n=t(e,{}),r;return n&&o(n)&&(r=n),{handled:!0,promise:r}}catch{return{handled:!1}}},c=e=>{if(!e||!i(e))return!1;let t=s(e);if(t.handled){try{let n=typeof window<`u`?window.location.href:void 0,r=setTimeout(()=>{try{let t=typeof window<`u`?window.location.href:void 0,r=typeof document<`u`?document.visibilityState:void 0;n&&t&&n===t&&r===`visible`&&typeof window<`u`&&window.location.assign(e)}catch{}},400);try{t.promise?.then(()=>{clearTimeout(r)}).catch(()=>{})}catch{}}catch{}return!0}try{typeof window<`u`&&window.location.assign(e)}catch{}return!1};exports.getInAppOpenLinkTarget=t,exports.isAllowedIframeSrc=r,exports.isSafeNavigationHref=i,exports.normalizeInAppLinks=a,exports.openStaffbaseAware=c,exports.tryOpenWithStaffbase=s;
2
+ //# sourceMappingURL=links.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.cjs.js","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ //#region src/links/stripStaffbaseLinkPrefix.ts
2
+ var e = (e) => e.startsWith("/deeplink/") || e.startsWith("/openlink/") ? `/${e.slice(10)}` : e, t = (t, n) => {
3
+ let r = t?.trim();
4
+ if (!r || r.startsWith("#")) return null;
5
+ let i = r.toLowerCase();
6
+ if (i.startsWith("mailto:") || i.startsWith("tel:") || i.startsWith("sms:") || i.startsWith("javascript:") || i.startsWith("data:")) return null;
7
+ try {
8
+ let t = new URL(n), i = new URL(r, t.toString());
9
+ if (i.origin === t.origin) {
10
+ let n = e(i.pathname);
11
+ return new URL(`${n}${i.search}${i.hash}`, t.origin).toString();
12
+ }
13
+ return i.toString();
14
+ } catch {
15
+ return null;
16
+ }
17
+ }, n = [
18
+ "youtube.com",
19
+ "youtube-nocookie.com",
20
+ "youtu.be",
21
+ "vimeo.com",
22
+ "player.vimeo.com",
23
+ "staffbase.com",
24
+ "staffbase.rocks"
25
+ ], r = (e) => {
26
+ let t = e.trim();
27
+ if (!t) return !1;
28
+ try {
29
+ let e = typeof window < "u" ? window.location.origin : "https://localhost", r = new URL(t, e);
30
+ if (r.protocol !== "https:" && r.protocol !== "http:") return !1;
31
+ if (r.origin === e) return !0;
32
+ let i = r.hostname.toLowerCase();
33
+ return n.some((e) => i === e || i.endsWith(`.${e}`));
34
+ } catch {
35
+ return !1;
36
+ }
37
+ }, i = (e) => {
38
+ let t = e.trim().toLowerCase();
39
+ return t ? !(t.startsWith("javascript:") || t.startsWith("data:") || t.startsWith("vbscript:")) : !1;
40
+ }, a = (e, t) => {
41
+ let n = t?.trim() || "";
42
+ if (!n) return;
43
+ let r = Array.from(e.querySelectorAll("a[href]"));
44
+ for (let e of r) {
45
+ let t = e.getAttribute("href");
46
+ if (!t) continue;
47
+ let r = t.trim().toLowerCase();
48
+ if (!(r.startsWith("#") || r.startsWith("mailto:") || r.startsWith("tel:") || r.startsWith("sms:") || r.startsWith("javascript:") || r.startsWith("data:"))) try {
49
+ let r = new URL(t, n);
50
+ if (r.origin !== n) {
51
+ e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer");
52
+ continue;
53
+ }
54
+ let i = `${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//, "/").replace(/^\/openlink\//, "/");
55
+ e.setAttribute("href", i), e.classList.add("internal-link"), e.removeAttribute("target"), e.removeAttribute("rel");
56
+ } catch {}
57
+ }
58
+ }, o = (e) => typeof e == "object" && !!e && "then" in e && typeof e.then == "function", s = (e) => {
59
+ try {
60
+ if (typeof window > "u") return { handled: !1 };
61
+ let t = window?.staffbase?.plugin?.util?.openLink;
62
+ if (typeof t != "function") return { handled: !1 };
63
+ let n = t(e, {}), r;
64
+ return n && o(n) && (r = n), {
65
+ handled: !0,
66
+ promise: r
67
+ };
68
+ } catch {
69
+ return { handled: !1 };
70
+ }
71
+ }, c = (e) => {
72
+ if (!e || !i(e)) return !1;
73
+ let t = s(e);
74
+ if (t.handled) {
75
+ try {
76
+ let n = typeof window < "u" ? window.location.href : void 0, r = setTimeout(() => {
77
+ try {
78
+ let t = typeof window < "u" ? window.location.href : void 0, r = typeof document < "u" ? document.visibilityState : void 0;
79
+ n && t && n === t && r === "visible" && typeof window < "u" && window.location.assign(e);
80
+ } catch {}
81
+ }, 400);
82
+ try {
83
+ t.promise?.then(() => {
84
+ clearTimeout(r);
85
+ }).catch(() => {});
86
+ } catch {}
87
+ } catch {}
88
+ return !0;
89
+ }
90
+ try {
91
+ typeof window < "u" && window.location.assign(e);
92
+ } catch {}
93
+ return !1;
94
+ };
95
+ //#endregion
96
+ export { t as getInAppOpenLinkTarget, r as isAllowedIframeSrc, i as isSafeNavigationHref, a as normalizeInAppLinks, c as openStaffbaseAware, s as tryOpenWithStaffbase };
97
+
98
+ //# sourceMappingURL=links.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.es.mjs","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string
3
+ * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags
4
+ * so tag removal stays single-sourced.
5
+ * @param {string} html - The HTML string to clean.
6
+ * @returns {string} The cleaned, normalized text.
7
+ */
8
+ export declare const cleanHTML: (html: string) => string;
9
+ //# sourceMappingURL=cleanHTML.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanHTML.d.ts","sourceRoot":"","sources":["../../../src/html/cleanHTML.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MAK9B,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { cleanHTML } from './cleanHTML';
2
+ export { sanitizeArticleHtml } from './sanitizeArticleHtml';
3
+ export { sanitizeHtml } from './sanitizeHtml';
4
+ export { stripHtmlTags } from './stripHtmlTags';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,15 @@
1
+ import { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions';
2
+ /**
3
+ * Canonical sanitizer for rich article HTML rendered into a session-bearing
4
+ * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps
5
+ * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via
6
+ * data-*), strips scripts/inline handlers/javascript:, and forces
7
+ * rel="noopener noreferrer" on target="_blank" anchors. When options.isAllowedIframeSrc
8
+ * is provided, iframes whose src fails it are dropped (injected so this module
9
+ * does not depend on /links, which owns isAllowedIframeSrc).
10
+ * @param {string} html - Raw article HTML from the Staffbase API.
11
+ * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.
12
+ * @returns {string} Sanitized HTML safe to inject/parse.
13
+ */
14
+ export declare const sanitizeArticleHtml: (html: string, options?: SanitizeArticleHtmlOptions) => string;
15
+ //# sourceMappingURL=sanitizeArticleHtml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizeArticleHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeArticleHtml.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAA;AA2B1F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EACZ,UAAS,0BAA+B,KACvC,MAqBF,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Strict sanitizer for untrusted snippet/teaser HTML rendered through a
3
+ * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile
4
+ * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping
5
+ * only basic rich-text markup. From global-content's sanitizeHtml.
6
+ *
7
+ * Use sanitizeArticleHtml instead when rendering full article bodies that must
8
+ * keep iframes / data-* embeds.
9
+ * @param {string} html - Raw HTML string from the API.
10
+ * @returns {string} Sanitized HTML.
11
+ */
12
+ export declare const sanitizeHtml: (html: string) => string;
13
+ //# sourceMappingURL=sanitizeHtml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeHtml.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Strips all HTML tags, keeping the text content. From smart-search's
3
+ * stripHtmlTags; its html-react-parser fallback is intentionally dropped to
4
+ * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is
5
+ * sufficient and never throws in practice.
6
+ * @param {string | undefined} html - String that may contain HTML tags.
7
+ * @returns {string} The text content with tags removed.
8
+ */
9
+ export declare const stripHtmlTags: (html?: string) => string;
10
+ //# sourceMappingURL=stripHtmlTags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripHtmlTags.d.ts","sourceRoot":"","sources":["../../../src/html/stripHtmlTags.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAQ7C,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Returns an absolute URL string that is safe to pass into Staffbase's
3
+ * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform
4
+ * strips it, links can still open in-app by delegating to `openLink()`. Ported
5
+ * from staffbase-alerts (identical to unack).
6
+ * @param {string | null | undefined} href - Raw href as found on an <a> element.
7
+ * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).
8
+ * @returns {string | null} An absolute URL string, or null if it should not be handled.
9
+ */
10
+ export declare const getInAppOpenLinkTarget: (href: string | null | undefined, baseUrl: string) => string | null;
11
+ //# sourceMappingURL=getInAppOpenLinkTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getInAppOpenLinkTarget.d.ts","sourceRoot":"","sources":["../../../src/links/getInAppOpenLinkTarget.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,SAAS,MAAM,KACd,MAAM,GAAG,IAsCX,CAAA"}
@@ -0,0 +1,7 @@
1
+ export { getInAppOpenLinkTarget } from './getInAppOpenLinkTarget';
2
+ export { isAllowedIframeSrc } from './isAllowedIframeSrc';
3
+ export { isSafeNavigationHref } from './isSafeNavigationHref';
4
+ export { normalizeInAppLinks } from './normalizeInAppLinks';
5
+ export { openStaffbaseAware } from './openStaffbaseAware';
6
+ export { tryOpenWithStaffbase } from './tryOpenWithStaffbase';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/links/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns true when an iframe src may be rendered. Same-origin sources are
3
+ * always allowed; cross-origin sources must match the embed allowlist. Pair with
4
+ * sanitizeArticleHtml's `isAllowedIframeSrc` option.
5
+ * @param {string} src - The iframe src attribute value.
6
+ * @returns {boolean} True when the iframe may be kept.
7
+ */
8
+ export declare const isAllowedIframeSrc: (src: string) => boolean;
9
+ //# sourceMappingURL=isAllowedIframeSrc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isAllowedIframeSrc.d.ts","sourceRoot":"","sources":["../../../src/links/isAllowedIframeSrc.ts"],"names":[],"mappings":"AAeA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,KAAG,OAqBhD,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type guard for Promise-like (thenable) values, used to normalize whatever
3
+ * Staffbase's openLink returns.
4
+ * @param {unknown} value - The value to test.
5
+ * @returns {boolean} True when value has a callable then method.
6
+ */
7
+ export declare const isPromiseLike: (value: unknown) => value is PromiseLike<unknown>;
8
+ //# sourceMappingURL=isPromiseLike.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isPromiseLike.d.ts","sourceRoot":"","sources":["../../../src/links/isPromiseLike.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAAW,CAAC,OAAO,CAIf,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Returns true when an href is safe to pass to a real browser navigation
3
+ * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could
4
+ * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel
5
+ * are allowed. Promoted from staffbase-alerts.
6
+ * @param {string} href - The href to validate.
7
+ * @returns {boolean} True when the href is safe to navigate to.
8
+ */
9
+ export declare const isSafeNavigationHref: (href: string) => boolean;
10
+ //# sourceMappingURL=isSafeNavigationHref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isSafeNavigationHref.d.ts","sourceRoot":"","sources":["../../../src/links/isSafeNavigationHref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OASnD,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile
3
+ * apps. On iOS, absolute same-origin links and/or `target="_blank"` can trigger
4
+ * Safari instead of in-app navigation; for same-origin links we rewrite to
5
+ * relative paths and remove target/rel. Ported from staffbase-alerts (the
6
+ * superset): cross-origin `target="_blank"` links are hardened with
7
+ * `rel="noopener noreferrer"` instead of left untouched. The Staffbase origin is
8
+ * passed in (the lib never reads env).
9
+ * @param {HTMLElement} root - The container whose anchors are normalized.
10
+ * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.
11
+ * @returns {void}
12
+ */
13
+ export declare const normalizeInAppLinks: (root: HTMLElement, staffbaseOrigin: string) => void;
14
+ //# sourceMappingURL=normalizeInAppLinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeInAppLinks.d.ts","sourceRoot":"","sources":["../../../src/links/normalizeInAppLinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,WAAW,EACjB,iBAAiB,MAAM,KACtB,IAsDF,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Try to open with Staffbase; if not available, navigate normally in the same
3
+ * tab. Ported from staffbase-alerts (the superset): it guards with
4
+ * isSafeNavigationHref first, so scripted schemes never reach navigation.
5
+ * @param {string} href - The target URL to open.
6
+ * @returns {boolean} True if Staffbase handled the navigation, otherwise false.
7
+ */
8
+ export declare const openStaffbaseAware: (href: string) => boolean;
9
+ //# sourceMappingURL=openStaffbaseAware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openStaffbaseAware.d.ts","sourceRoot":"","sources":["../../../src/links/openStaffbaseAware.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAuDjD,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a
3
+ * URL pathname, leaving the canonical in-app path.
4
+ * @param {string} path - The URL pathname.
5
+ * @returns {string} The path without the Staffbase prefix.
6
+ */
7
+ export declare const stripStaffbaseLinkPrefix: (path: string) => string;
8
+ //# sourceMappingURL=stripStaffbaseLinkPrefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripStaffbaseLinkPrefix.d.ts","sourceRoot":"","sources":["../../../src/links/stripStaffbaseLinkPrefix.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAMvD,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { TryOpenResult } from '../types/links/TryOpenResult';
2
+ /**
3
+ * Attempt to open a link using Staffbase's plugin util.openLink if available.
4
+ * Ported from staffbase-alerts (typed Promise-like guard; smart-search used
5
+ * `as any`).
6
+ * @param {string} href - The target URL to open.
7
+ * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.
8
+ */
9
+ export declare const tryOpenWithStaffbase: (href: string) => TryOpenResult;
10
+ //# sourceMappingURL=tryOpenWithStaffbase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tryOpenWithStaffbase.d.ts","sourceRoot":"","sources":["../../../src/links/tryOpenWithStaffbase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAKjE;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,aAoBnD,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Options for sanitizeArticleHtml.
3
+ */
4
+ export interface SanitizeArticleHtmlOptions {
5
+ /**
6
+ * Predicate deciding whether an iframe `src` may be kept. When provided,
7
+ * iframes whose src fails the predicate are dropped. When omitted, iframes are
8
+ * kept (DOMPurify still blocks `src="javascript:"`). Injected so the /html
9
+ * module does not depend on /links, which owns isAllowedIframeSrc.
10
+ * @param {string} src - The iframe src attribute value.
11
+ * @returns {boolean} True when the iframe may be kept.
12
+ */
13
+ isAllowedIframeSrc?: (src: string) => boolean;
14
+ }
15
+ //# sourceMappingURL=SanitizeArticleHtmlOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SanitizeArticleHtmlOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/html/SanitizeArticleHtmlOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CAC9C"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Result of attempting to open a link through Staffbase's plugin util.openLink.
3
+ */
4
+ export interface TryOpenResult {
5
+ /** Whether Staffbase handled the open call. */
6
+ handled: boolean;
7
+ /** The promise openLink returned, when it returned one. */
8
+ promise?: Promise<void>;
9
+ }
10
+ //# sourceMappingURL=TryOpenResult.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TryOpenResult.d.ts","sourceRoot":"","sources":["../../../../src/types/links/TryOpenResult.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Window augmented with the Staffbase plugin util surface used for in-app link
3
+ * opening. The whole chain is optional because the host API may be absent.
4
+ */
5
+ export interface WindowWithStaffbase extends Window {
6
+ staffbase?: {
7
+ plugin?: {
8
+ util?: {
9
+ openLink?: (link: string, opts?: unknown) => unknown;
10
+ };
11
+ };
12
+ };
13
+ }
14
+ //# sourceMappingURL=WindowWithStaffbase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WindowWithStaffbase.d.ts","sourceRoot":"","sources":["../../../../src/types/links/WindowWithStaffbase.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,mBAAoB,SAAQ,MAAM;IACjD,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE;YACP,IAAI,CAAC,EAAE;gBACL,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAA;aACrD,CAAA;SACF,CAAA;KACF,CAAA;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@favish/staffbase-utils",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared internal/host utilities for Staffbase widgets",
5
5
  "author": "Favish <dev@favish.com>",
6
6
  "license": "UNLICENSED",
@@ -17,6 +17,16 @@
17
17
  "types": "./dist/src/device/index.d.ts",
18
18
  "import": "./dist/device.es.mjs",
19
19
  "require": "./dist/device.cjs.js"
20
+ },
21
+ "./html": {
22
+ "types": "./dist/src/html/index.d.ts",
23
+ "import": "./dist/html.es.mjs",
24
+ "require": "./dist/html.cjs.js"
25
+ },
26
+ "./links": {
27
+ "types": "./dist/src/links/index.d.ts",
28
+ "import": "./dist/links.es.mjs",
29
+ "require": "./dist/links.cjs.js"
20
30
  }
21
31
  },
22
32
  "repository": {
@@ -63,6 +73,9 @@
63
73
  "vite": "^8.0.14",
64
74
  "vite-plugin-dts": "^5.0.1"
65
75
  },
76
+ "dependencies": {
77
+ "dompurify": "3.4.7"
78
+ },
66
79
  "scripts": {
67
80
  "build": "vite build",
68
81
  "check:all": "pnpm run format && pnpm run lint:fix && pnpm run type-check && pnpm run test",