fix(custom-widgets): parse fenced imports safely

This commit is contained in:
Thomas Camlong committed 2026-07-15 10:32:42 +02:00
1 parent e174e85c39
commit b573b4d46f
3 files changed
+63 -30

No files matched your search

+26 -2
View File
@@ -8,10 +8,34 @@ export interface ImportReview {
hasActions: boolean;
}
function extractFencedBlock(text: string, acceptedLanguages: readonly string[]): string | undefined {
let cursor = 0;
while (cursor < text.length) {
const fenceStart = text.indexOf("```", cursor);
if (fenceStart === -1) return undefined;
const headerStart = fenceStart + 3;
const headerEnd = text.indexOf("\n", headerStart);
if (headerEnd === -1) return undefined;
const contentStart = headerEnd + 1;
const fenceEnd = text.indexOf("```", contentStart);
if (fenceEnd === -1) return undefined;
const language = text.slice(headerStart, headerEnd).trim().toLowerCase();
if (acceptedLanguages.includes(language)) return text.slice(contentStart, fenceEnd);
cursor = fenceEnd + 3;
}
return undefined;
}
export function parseCustomWidgetClipboard(text: string): Record<string, unknown> | null {
try {
const jsonBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/iu)?.[1];
const jsxBlock = text.match(/```(?:jsx|tsx)\s*([\s\S]*?)```/iu)?.[1]?.trim();
const jsonBlock = extractFencedBlock(text, ["", "json"]);
const jsxBlock = extractFencedBlock(text, ["jsx", "tsx"])?.trim();
const value: unknown = JSON.parse(jsonBlock ?? text);
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const widget = value as Record<string, unknown>;
@@ -53,4 +53,16 @@ ${JSON.stringify(widget, null, 2)}
'<Stack gap="sm">\n <Text>{data.name}</Text>\n</Stack>',
);
});
it("accepts case-insensitive fenced languages and CRLF line endings", () => {
const result = parseCustomWidgetClipboard(
`\`\`\`JSON\r\n${JSON.stringify(widget)}\r\n\`\`\`\r\n\`\`\`TSX\r\n<Text>{data.name}</Text>\r\n\`\`\``,
);
expect((result?.displayConfig as Record<string, unknown> | undefined)?.template).toBe("<Text>{data.name}</Text>");
});
it("rejects an unterminated fence with a large whitespace payload", () => {
expect(parseCustomWidgetClipboard(`\`\`\`json\n${" ".repeat(250_000)}`)).toBeNull();
});
});
+25 -28
View File
@@ -1755,7 +1755,7 @@ importers:
version: link:../../tooling/typescript
'@types/node':
specifier: 'catalog:'
version: 24.13.2
version: 24.13.3
'@types/react':
specifier: 'catalog:'
version: 19.2.17
@@ -1773,7 +1773,7 @@ importers:
version: 6.0.3
vitest:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
packages/db:
dependencies:
@@ -18918,7 +18918,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/yargs': 17.0.35
chalk: 4.1.2
@@ -20501,7 +20501,7 @@ snapshots:
'@scalar/icons@0.7.3(typescript@6.0.3)':
dependencies:
'@phosphor-icons/core': 2.1.1
'@types/node': 24.13.2
'@types/node': 24.13.3
chalk: 5.6.2
vue: 3.5.35(typescript@6.0.3)
transitivePeerDependencies:
@@ -21476,11 +21476,11 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/bonjour@3.5.13':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/chai@5.2.2':
dependencies:
@@ -21491,11 +21491,11 @@ snapshots:
'@types/connect-history-api-fallback@1.5.4':
dependencies:
'@types/express-serve-static-core': 4.19.5
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/cookies@0.9.2':
dependencies:
@@ -21641,13 +21641,13 @@ snapshots:
'@types/docker-modem@3.0.6':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/ssh2': 1.15.1
'@types/dockerode@4.0.1':
dependencies:
'@types/docker-modem': 3.0.6
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/ssh2': 1.15.1
'@types/estree-jsx@1.0.5':
@@ -21658,7 +21658,7 @@ snapshots:
'@types/express-serve-static-core@4.19.5':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/qs': 6.9.16
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -21690,7 +21690,7 @@ snapshots:
'@types/http-proxy@1.17.17':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/istanbul-lib-coverage@2.0.6': {}
@@ -21720,7 +21720,7 @@ snapshots:
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
form-data: 4.0.6
'@types/node-unifi@2.5.1(patch_hash=5e6ae51e2a17a7f9729bfa30b0eb3d0842a5810ac6db47603ab4a6efa1ed84c5)':
@@ -21784,14 +21784,14 @@ snapshots:
'@types/sax@1.2.7':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/semver@7.5.8': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/serve-index@1.9.4':
dependencies:
@@ -21800,7 +21800,7 @@ snapshots:
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/send': 0.17.4
'@types/set-cookie-parser@2.4.10':
@@ -21810,15 +21810,15 @@ snapshots:
'@types/sockjs@0.3.36':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/ssh2-streams@0.1.12':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/ssh2@0.5.52':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/ssh2-streams': 0.1.12
'@types/ssh2@1.15.1':
@@ -21830,7 +21830,7 @@ snapshots:
'@types/stream-buffers@3.0.7':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/triple-beam@1.3.5': {}
@@ -21855,7 +21855,7 @@ snapshots:
'@types/xml2js@0.4.14':
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
'@types/yargs-parser@21.0.3': {}
@@ -21979,7 +21979,6 @@ snapshots:
optionalDependencies:
msw: 2.14.6(@types/node@24.13.3)(typescript@6.0.3)
vite: 8.1.3(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)
optional: true
'@vitest/pretty-format@4.1.9':
dependencies:
@@ -24144,7 +24143,7 @@ snapshots:
eval@0.1.8:
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
require-like: 0.1.2
event-target-shim@5.0.1: {}
@@ -25450,7 +25449,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 24.13.2
'@types/node': 24.13.3
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@@ -25464,7 +25463,7 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 24.13.2
'@types/node': 24.13.3
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -27867,7 +27866,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 24.13.2
'@types/node': 24.13.3
long: 5.3.2
proxmox-api@1.1.1:
@@ -29874,7 +29873,6 @@ snapshots:
terser: 5.46.1
tsx: 4.21.0
yaml: 2.9.0
optional: true
vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)):
dependencies:
@@ -29937,7 +29935,6 @@ snapshots:
jsdom: 29.1.1(@noble/hashes@2.0.1)
transitivePeerDependencies:
- msw
optional: true
vue-component-type-helpers@3.3.4: {}