From 1343e91ef055406871ae103c1ac66c56b72ec362 Mon Sep 17 00:00:00 2001 From: KB Date: Wed, 22 Jul 2026 11:50:40 +0530 Subject: [PATCH 1/7] fix(ui): keep MathML annotation tags so KaTeX TeX source stops leaking (#194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOMPurify's mathMl profile disallows /, and its default KEEP_CONTENT strips the tags but keeps their text — so the raw TeX source inside KaTeX's MathML annotation survived as a bare text node and rendered visibly next to the typeset formula. Add ADD_TAGS/ ADD_ATTR to the sanitize config to allowlist the annotation wrapper tags, and export sanitize so it's directly testable. Adds a jsdom-based DOM test harness for frontend/ui (bunfig preload + jsdom.ts), which currently has no DOM test environment. Uses jsdom rather than the @happy-dom/global-registrator pattern used in frontend/workspace: happy-dom's NodeIterator doesn't implement the DOM spec's live-mutation adjustment, so once DOMPurify removes the first disallowed node mid-walk, iteration silently stops and everything after goes unsanitized — verified directly, and confirmed jsdom does not have this problem. --- bun.lock | 105 ++++++++++++++++++-- frontend/ui/bunfig.toml | 2 + frontend/ui/jsdom.ts | 14 +++ frontend/ui/package.json | 3 + frontend/ui/src/components/markdown.test.ts | 38 +++++++ frontend/ui/src/components/markdown.tsx | 4 +- 6 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 frontend/ui/bunfig.toml create mode 100644 frontend/ui/jsdom.ts create mode 100644 frontend/ui/src/components/markdown.test.ts diff --git a/bun.lock b/bun.lock index 2529b34f..4fcde3af 100644 --- a/bun.lock +++ b/bun.lock @@ -172,8 +172,11 @@ "@tailwindcss/vite": "catalog:", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", + "@types/jsdom": "28.0.3", "@types/katex": "0.16.7", "@types/luxon": "catalog:", + "global-jsdom": "29.0.0", + "jsdom": "29.1.1", "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", @@ -402,6 +405,14 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], @@ -450,12 +461,26 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/prompts": ["@clack/prompts@1.0.0-alpha.1", "", { "dependencies": { "@clack/core": "1.0.0-alpha.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-07MNT0OsxjKOcyVfX8KhXBhJiyUbDP1vuIAcHc+nx5v93MJO23pX3X/k3bWz6T3rpM9dgWPq90i4Jq7gZAyMbw=="], "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.9", "", { "dependencies": { "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.6", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -508,6 +533,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], @@ -928,6 +955,8 @@ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/jsdom": ["@types/jsdom@28.0.3", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], @@ -954,6 +983,8 @@ "@types/swagger-ui-dist": ["@types/swagger-ui-dist@3.30.6", "", {}, "sha512-FVxN7wjLYRtJsZBscOcOcf8oR++m38vbUFjT33Mr9HBuasX9bRDrJsp7iwixcOtKSHEEa2B7o2+4wEiXqC+Ebw=="], + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], @@ -1028,6 +1059,8 @@ "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -1126,12 +1159,16 @@ "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], @@ -1192,7 +1229,7 @@ "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -1280,6 +1317,8 @@ "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "global-jsdom": ["global-jsdom@29.0.0", "", { "peerDependencies": { "jsdom": ">=29 <30" } }, "sha512-S9Wl+EHDfkO8DYleqMYzf14hKfwIysEN/FvoVRdPif3uUGUlmiHs/gj1PVKXhmJ20DwUVACtdxxhNYYvvn9K7A=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -1314,6 +1353,8 @@ "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1376,6 +1417,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], @@ -1404,6 +1447,8 @@ "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -1458,7 +1503,7 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -1510,6 +1555,8 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], @@ -1662,7 +1709,7 @@ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -1704,6 +1751,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -1758,6 +1807,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], @@ -1832,6 +1883,8 @@ "swagger-ui-dist": ["swagger-ui-dist@5.32.8", "", { "dependencies": { "@scarf/scarf": "=1.4.0" } }, "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "synsci": ["synsci@workspace:tooling/launcher"], "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], @@ -1848,11 +1901,17 @@ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tldts": ["tldts@7.4.9", "", { "dependencies": { "tldts-core": "^7.4.9" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA=="], + + "tldts-core": ["tldts-core@7.4.9", "", {}, "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], @@ -1934,15 +1993,17 @@ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1958,6 +2019,10 @@ "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1978,6 +2043,8 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@hey-api/openapi-ts/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], @@ -2066,10 +2133,14 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/jsdom/undici-types": ["undici-types@7.28.0", "", {}, "sha512-LJAfY+2w6HGeT8d8J1wNQsUGUEGio6NWWpwdwurQe4f6oojzCFuGLizl1KSve4irsTxyLly1QhEeE6iapdaIvQ=="], + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "babel-plugin-jsx-dom-expressions/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2082,6 +2153,14 @@ "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "jsdom/decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "jsdom/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -2100,10 +2179,6 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -2194,10 +2269,14 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "babel-plugin-jsx-dom-expressions/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "cross-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -2321,5 +2400,9 @@ "@synsci/workspace/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], "@synsci/workspace/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "cross-fetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "cross-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], } } diff --git a/frontend/ui/bunfig.toml b/frontend/ui/bunfig.toml new file mode 100644 index 00000000..1eecb457 --- /dev/null +++ b/frontend/ui/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./jsdom.ts"] diff --git a/frontend/ui/jsdom.ts b/frontend/ui/jsdom.ts new file mode 100644 index 00000000..80131193 --- /dev/null +++ b/frontend/ui/jsdom.ts @@ -0,0 +1,14 @@ +import globalJsdom from "global-jsdom" + +// frontend/ui has no DOM. This registers jsdom globals (window, document, +// DOMParser, Node, NodeFilter, ...) so DOMPurify can run under `bun test`. +// +// Deliberately jsdom, not @happy-dom/global-registrator (the pattern used in +// frontend/workspace/happydom.ts): happy-dom's NodeIterator does not +// implement the DOM spec's live-mutation ("pre-remove steps") adjustment, so +// once DOMPurify removes the first disallowed node mid-walk, iteration stops +// silently and everything after goes unsanitized. That breaks any test that +// needs a removal to happen partway through the tree (e.g. verifying a +// disallowed MathML tag downstream of other content is actually stripped). +// jsdom implements this correctly; verified by direct comparison. +globalJsdom(undefined, { url: "http://localhost/" }) diff --git a/frontend/ui/package.json b/frontend/ui/package.json index fbfd9f09..cfe79011 100644 --- a/frontend/ui/package.json +++ b/frontend/ui/package.json @@ -30,8 +30,11 @@ "@tailwindcss/vite": "catalog:", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", + "@types/jsdom": "28.0.3", "@types/katex": "0.16.7", "@types/luxon": "catalog:", + "global-jsdom": "29.0.0", + "jsdom": "29.1.1", "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", diff --git a/frontend/ui/src/components/markdown.test.ts b/frontend/ui/src/components/markdown.test.ts new file mode 100644 index 00000000..1e625fae --- /dev/null +++ b/frontend/ui/src/components/markdown.test.ts @@ -0,0 +1,38 @@ +import { describe, test, expect } from "bun:test" +import katex from "katex" +import { sanitize } from "./markdown" + +const tex = "\\delta\\omega/\\omega < 10^{-6}" + +describe("sanitize (KaTeX MathML annotation)", () => { + test("keeps the wrapper so raw TeX doesn't leak as visible text", () => { + const katexHtml = katex.renderToString(tex, { throwOnError: false }) + const safe = sanitize(katexHtml) + expect(safe).toContain(", not as a bare child of ", () => { + const katexHtml = katex.renderToString(tex, { throwOnError: false }) + const safe = sanitize(katexHtml) + + const doc = new DOMParser().parseFromString(safe, "text/html") + const math = doc.querySelector("math") + expect(math).not.toBeNull() + + // No direct text-node child of should carry the raw TeX source. + const bareLeak = Array.from(math?.childNodes ?? []).some( + (node) => node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").includes(tex), + ) + expect(bareLeak).toBe(false) + + // The TeX source must actually be present, but only inside . + const annotation = doc.querySelector("annotation") + expect(annotation).not.toBeNull() + expect(annotation?.textContent).toContain(tex) + }) + + test("regression guard: still strips script-injection attributes (sanitizer stays active)", () => { + const safe = sanitize('') + expect(safe).not.toContain("onerror") + }) +}) diff --git a/frontend/ui/src/components/markdown.tsx b/frontend/ui/src/components/markdown.tsx index 7778847a..dd04d5f3 100644 --- a/frontend/ui/src/components/markdown.tsx +++ b/frontend/ui/src/components/markdown.tsx @@ -32,6 +32,8 @@ const config = { SANITIZE_NAMED_PROPS: true, FORBID_TAGS: ["style"], FORBID_CONTENTS: ["style", "script"], + ADD_TAGS: ["semantics", "annotation", "annotation-xml"], + ADD_ATTR: ["encoding"], } const iconPaths = { @@ -39,7 +41,7 @@ const iconPaths = { check: '', } -function sanitize(html: string) { +export function sanitize(html: string) { if (!DOMPurify.isSupported) return "" return DOMPurify.sanitize(html, config) } From 36260d3dd1b3aa2f4ca088724f23a7733038106b Mon Sep 17 00:00:00 2001 From: KB Date: Wed, 22 Jul 2026 11:58:50 +0530 Subject: [PATCH 2/7] test(ui): harden KaTeX sanitizer XSS guard with a multi-node payload --- frontend/ui/src/components/markdown.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/ui/src/components/markdown.test.ts b/frontend/ui/src/components/markdown.test.ts index 1e625fae..4ebd5617 100644 --- a/frontend/ui/src/components/markdown.test.ts +++ b/frontend/ui/src/components/markdown.test.ts @@ -31,8 +31,11 @@ describe("sanitize (KaTeX MathML annotation)", () => { expect(annotation?.textContent).toContain(tex) }) - test("regression guard: still strips script-injection attributes (sanitizer stays active)", () => { - const safe = sanitize('') + test("regression guard: still strips script-injection attributes across every node (sanitizer stays active)", () => { + const safe = sanitize( + "", + ) expect(safe).not.toContain("onerror") + expect(safe).not.toContain(" Date: Wed, 22 Jul 2026 12:26:08 +0530 Subject: [PATCH 3/7] fix(provider): stop dropping images for models with attachment capability (#192) unsupportedParts() gated image/pdf parts solely on the fine-grained capabilities.input.image/pdf flag, which is absent or wrong for models outside the local models.dev catalog. The synthetic OpenRouter model in particular hardcoded attachment:false + input.image:false for any whitelisted-but-uncataloged model, so vision-capable models silently had their images swapped for an ERROR text part that the model then paraphrased as "this model doesn't support image input". Fall back to the coarse attachment capability for image/pdf only when the finer modality flag is missing (audio/video untouched), and make the synthetic model's placeholder capabilities permissive for image/pdf so a genuinely-unsupported attachment surfaces a real provider error instead of a fabricated one. --- backend/cli/src/provider/provider.ts | 9 +- backend/cli/src/provider/transform.ts | 11 +- backend/cli/test/provider/transform.test.ts | 179 ++++++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) diff --git a/backend/cli/src/provider/provider.ts b/backend/cli/src/provider/provider.ts index 7a9a9bc9..af89c5f4 100644 --- a/backend/cli/src/provider/provider.ts +++ b/backend/cli/src/provider/provider.ts @@ -811,9 +811,14 @@ export namespace Provider { capabilities: { temperature: true, reasoning: true, - attachment: false, + // #192: this is a placeholder for a whitelisted model NOT in the + // local catalog — guessing `false` here silently drops images/PDFs + // for what may well be a vision-capable model. Guess permissive + // instead: a genuinely-unsupported attachment surfaces a real + // provider error rather than a fabricated "unsupported" one. + attachment: true, toolcall: true, - input: { text: true, audio: false, image: false, video: false, pdf: false }, + input: { text: true, audio: false, image: true, video: false, pdf: true }, output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false, }, diff --git a/backend/cli/src/provider/transform.ts b/backend/cli/src/provider/transform.ts index 07cd1ded..795032fb 100644 --- a/backend/cli/src/provider/transform.ts +++ b/backend/cli/src/provider/transform.ts @@ -290,7 +290,16 @@ export namespace ProviderTransform { if (part.type === "file") return fileToText(part) return part } - if (model.capabilities.input[modality]) return part + // #192: fine-grained input.image/input.pdf can be missing/wrong in the + // catalog (e.g. the synthetic OpenRouter model) even though the model + // is flagged attachment-capable. Fall back to the coarse `attachment` + // capability for image/pdf only — audio/video stay gated on their own + // modality flag. + if ( + model.capabilities.input[modality] || + (model.capabilities.attachment && (modality === "image" || modality === "pdf")) + ) + return part const name = filename ? `"${filename}"` : modality return { diff --git a/backend/cli/test/provider/transform.test.ts b/backend/cli/test/provider/transform.test.ts index 189f16e7..73e4c59a 100644 --- a/backend/cli/test/provider/transform.test.ts +++ b/backend/cli/test/provider/transform.test.ts @@ -1018,6 +1018,185 @@ describe("ProviderTransform.message - unsupported file attachments", () => { }) }) +describe("ProviderTransform.message - image/pdf attachment fallback (#192)", () => { + // #192: images were falsely rejected as "this model doesn't support image + // input" for vision-capable models whose fine-grained input.image/input.pdf + // flag was missing/wrong in the catalog (notably the synthetic OpenRouter + // model, which hardcoded these false). The gate must fall back to the + // coarse `attachment` capability for image/pdf so it isn't blocked purely + // on a missing modality flag — but a model with attachment:false is + // genuinely incapable and must still get the ERROR text. + const b64 = (s: string) => Buffer.from(s).toString("base64") + const validImageBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + const createModel = (capabilityOverrides: Record) => + ({ + id: "openrouter/some-vision-model", + providerID: "openrouter", + api: { id: "some-vision-model", url: "https://openrouter.ai/api/v1", npm: "@openrouter/ai-sdk-provider" }, + name: "Some Vision Model", + capabilities: { + temperature: true, + reasoning: false, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + ...capabilityOverrides, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 128000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + release_date: "", + }) as any + + test("image part survives when input.image=false but attachment=true (fallback)", () => { + const model = createModel({ + attachment: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image", image: `data:image/png;base64,${validImageBase64}` }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validImageBase64}` }) + }) + + test("image part still replaced with ERROR when input.image=false and attachment=false (genuinely incapable model)", () => { + const model = createModel({ + attachment: false, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image", image: `data:image/png;base64,${validImageBase64}` }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].content[1]).toEqual({ + type: "text", + text: "ERROR: Cannot read image (this model does not support image input). Inform the user.", + }) + }) + + test("image part survives when input.image=true regardless of attachment (regression)", () => { + const model = createModel({ + attachment: false, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [{ type: "image", image: `data:image/png;base64,${validImageBase64}` }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].content[0]).toEqual({ type: "image", image: `data:image/png;base64,${validImageBase64}` }) + }) + + test("pdf file part survives when input.pdf=false but attachment=true (fallback)", () => { + const model = createModel({ + attachment: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [ + { + type: "file", + mediaType: "application/pdf", + filename: "doc.pdf", + data: `data:application/pdf;base64,${b64("%PDF-1.4 fake")}`, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect((result[0].content as any[]).some((p: any) => p.type === "file")).toBe(true) + }) + + test("pdf file part replaced with ERROR when input.pdf=false and attachment=false", () => { + const model = createModel({ + attachment: false, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [ + { + type: "file", + mediaType: "application/pdf", + filename: "doc.pdf", + data: `data:application/pdf;base64,${b64("%PDF-1.4 fake")}`, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + const content = result[0].content as any[] + expect(content.some((p: any) => p.type === "file")).toBe(false) + expect(content[0]).toEqual({ + type: "text", + text: 'ERROR: Cannot read "doc.pdf" (this model does not support pdf input). Inform the user.', + }) + }) + + test("audio file part is NOT broadened by the attachment fallback (image/pdf only)", () => { + const model = createModel({ + attachment: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + }) + const msgs = [ + { + role: "user", + content: [ + { + type: "file", + mediaType: "audio/mpeg", + filename: "clip.mp3", + data: `data:audio/mpeg;base64,${b64("fake audio bytes")}`, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + const content = result[0].content as any[] + expect(content.some((p: any) => p.type === "file")).toBe(false) + expect(content[0]).toEqual({ + type: "text", + text: 'ERROR: Cannot read "clip.mp3" (this model does not support audio input). Inform the user.', + }) + }) +}) + describe("ProviderTransform.message - providerOptions key remapping", () => { const createModel = (providerID: string, npm: string) => ({ From 60cb776ee0e58ede0158f8aeec7a3262a65453cd Mon Sep 17 00:00:00 2001 From: KB Date: Wed, 22 Jul 2026 12:39:14 +0530 Subject: [PATCH 4/7] fix(cli): surface signal-terminated binary instead of exiting 0 silently (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On some ARM64 hosts (e.g. 64KB page-size kernels) the prebuilt native binary is killed by a signal (SIGSEGV/SIGILL) rather than exiting with a status code. spawnSync then returns status: null, signal: "SIG...", and the launcher's tail (`code = status ?? 0`) silently exited 0 with no output — the reporter's "exits silently." Add a signal branch in run() that prints an actionable diagnostic (signal name, host platform/arch, remediation steps) and exits 1. The existing result.error branch and numeric-status exit path are unchanged. --- backend/cli/bin/openscience | 10 ++++ .../test/installation/launcher-signal.test.ts | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 backend/cli/test/installation/launcher-signal.test.ts diff --git a/backend/cli/bin/openscience b/backend/cli/bin/openscience index d5341022..00b9a6e7 100755 --- a/backend/cli/bin/openscience +++ b/backend/cli/bin/openscience @@ -31,6 +31,16 @@ function run(target) { console.error(result.error.message) process.exit(1) } + if (result.signal) { + console.error( + `openscience was terminated by ${result.signal}. The prebuilt native binary may be ` + + `incompatible with this host (platform ${os.platform()}, arch ${os.arch()}). ` + + `Some ARM64 hosts (e.g. 64KB page-size kernels) cannot run the prebuilt binary. ` + + `Try reinstalling, set OPENSCIENCE_BIN_PATH to a compatible binary, or report at ` + + `https://github.com/synthetic-sciences/openscience/issues`, + ) + process.exit(1) + } const code = typeof result.status === "number" ? result.status : 0 process.exit(code) } diff --git a/backend/cli/test/installation/launcher-signal.test.ts b/backend/cli/test/installation/launcher-signal.test.ts new file mode 100644 index 00000000..5ea2a291 --- /dev/null +++ b/backend/cli/test/installation/launcher-signal.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test" +import os from "node:os" +import path from "node:path" +import { chmod, copyFile, mkdtemp, rm, writeFile } from "node:fs/promises" + +// The launcher (backend/cli/bin/openscience) is a plain CJS Node script. When +// the resolved binary is killed by a signal (SIGSEGV/SIGILL on some ARM64 +// hosts, per #190), spawnSync returns status: null, signal: "SIG...", and the +// launcher must not silently exit 0 — see the task brief for the confirmed +// root cause. +const launcherSource = path.join(__dirname, "../../bin/openscience") + +describe("launcher signal handling (#190)", () => { + test("exits non-zero with an actionable diagnostic when the binary is killed by a signal", async () => { + // CRITICAL SAFETY: run() destructively cleans ~/.openscience (unlinks + // files, rmSync's node_modules). HOME must point at a throwaway temp dir + // so this test never touches the real home directory. + const tmpHome = await mkdtemp(path.join(os.tmpdir(), "openscience-signal-home-")) + const tmpBin = await mkdtemp(path.join(os.tmpdir(), "openscience-signal-bin-")) + const crashScript = path.join(tmpBin, "crash.sh") + // Run the launcher from a copy outside this repo's tree: this repo's own + // package.json declares "type": "module", which would make a bare `node + // ` load the extension-less script as ESM and blow up on `require` + // before any launcher logic runs. The published wrapper package (see + // script/publish.ts) ships a package.json with no "type" field, so real + // installs default to CommonJS — copying to a bare temp dir (no ambient + // package.json) reproduces that real-world resolution instead. + const launcherCopy = path.join(tmpBin, "openscience") + + try { + // A shell script that signals itself SEGV. isBinary() in the launcher + // accepts any existing non-.js file that isn't the wrapper itself, so + // this stands in for a Bun binary crashing on an incompatible host. + await writeFile(crashScript, "#!/bin/sh\nkill -s SEGV $$\n") + await chmod(crashScript, 0o755) + await copyFile(launcherSource, launcherCopy) + + const proc = Bun.spawn(["node", launcherCopy, "some-arg"], { + env: { ...process.env, HOME: tmpHome, OPENSCIENCE_BIN_PATH: crashScript }, + stdout: "pipe", + stderr: "pipe", + }) + + const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]) + + expect(exitCode).not.toBe(0) + expect(stderr).toContain("SIGSEGV") + expect(stderr).toContain("incompatible") + } finally { + await rm(tmpHome, { recursive: true, force: true }) + await rm(tmpBin, { recursive: true, force: true }) + } + }) +}) From 1c755b3ee89b7b684043b494f1487c3f8c35444c Mon Sep 17 00:00:00 2001 From: KB Date: Wed, 22 Jul 2026 14:07:50 +0530 Subject: [PATCH 5/7] docs(skills): list the 1000 Genomes community skill (#191) Make the dnaerys/onekgpd-skill (1000 Genomes individual-genotype queries, MIT) discoverable via the existing `skill add` flow rather than bundling a third-party skill that depends on an external endpoint the maintainers don't control. --- frontend/docs/src/content/openscience/skills.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/docs/src/content/openscience/skills.mdx b/frontend/docs/src/content/openscience/skills.mdx index 2612ef08..63058587 100644 --- a/frontend/docs/src/content/openscience/skills.mdx +++ b/frontend/docs/src/content/openscience/skills.mdx @@ -47,6 +47,16 @@ openscience skill list Each skill in the repo passes a static safety check and an LLM safety review before it installs; rejected skills are skipped and reported. Installed skills are namespaced by repo, so `remove ` uninstalls the whole set. +### Community skills + +Skills contributed by the community that install with the command above: + +- **1000 Genomes genotypes** — [`dnaerys/onekgpd-skill`](https://github.com/dnaerys/onekgpd-skill) (MIT). Individual-level queries over the 1000 Genomes cohort (3,202 WGS samples, GRCh38): which individuals carry variants matching given criteria and zygosity, kinship between individuals, and cohort population/pedigree metadata. Queries a public, keyless [Dnaerys](https://dnaerys.org) endpoint (an external service the maintainers don't control), with an offline metadata tier. + + ```bash + openscience skill add gh:dnaerys/onekgpd-skill + ``` + ## Write your own ```bash From 8940041730c6609d247fd7a11c64023be91c233a Mon Sep 17 00:00:00 2001 From: KB Date: Wed, 22 Jul 2026 14:14:52 +0530 Subject: [PATCH 6/7] test(ui): guard annotation-xml integration-point mXSS surface (#194) --- frontend/ui/src/components/markdown.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/ui/src/components/markdown.test.ts b/frontend/ui/src/components/markdown.test.ts index 4ebd5617..65a33757 100644 --- a/frontend/ui/src/components/markdown.test.ts +++ b/frontend/ui/src/components/markdown.test.ts @@ -38,4 +38,12 @@ describe("sanitize (KaTeX MathML annotation)", () => { expect(safe).not.toContain("onerror") expect(safe).not.toContain(" { + const safe = sanitize( + '', + ) + expect(safe).not.toContain("onerror") + expect(safe).not.toContain(" Date: Wed, 22 Jul 2026 14:26:53 +0530 Subject: [PATCH 7/7] docs(test): warn happy-dom is unsound for DOMPurify/NodeIterator-mutation tests (#202) Audit for #202 found no DOMPurify-based tests in frontend/workspace (the only DOMPurify test, frontend/ui markdown.test.ts, is already on jsdom). Add a guard comment on the happy-dom registrator so future sanitizer tests aren't written against it and silently pass with a broken sanitizer. --- frontend/workspace/happydom.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/workspace/happydom.ts b/frontend/workspace/happydom.ts index de726718..e211d955 100644 --- a/frontend/workspace/happydom.ts +++ b/frontend/workspace/happydom.ts @@ -1,3 +1,10 @@ +// WARNING: happy-dom is NOT sound for tests that mutate the DOM while iterating +// it — most importantly DOMPurify. happy-dom's NodeIterator does not perform the +// DOM spec's live-mutation adjustment, so when a node is removed mid-walk, +// iteration stops early and later nodes go unvisited. DOMPurify sanitizes via +// exactly that pattern, so under happy-dom it silently under-sanitizes and a +// broken sanitizer can false-pass (verified — see #202). For sanitizer / +// NodeIterator-mutation tests use jsdom instead (see frontend/ui/jsdom.ts, #194). import { GlobalRegistrator } from "@happy-dom/global-registrator" GlobalRegistrator.register()