Skip to content

Commit d2a4189

Browse files
authored
Allow safe connector MDX formatting (#80)
**Why** Connector docs validation was flagging valid docs syntax, including comment-only MDX expressions, table `<br/>` line breaks, `CardGroup`, and indented code placeholders. This keeps the PR check aligned with docs syntax we expect while preserving rejects for executable MDX and unsafe URLs. **What this changes** - Allows comment-only MDX expressions, safe `<br/>` elements, and `CardGroup`. - Adds a shared fixture file for cases that should match registry validation. - Adds reject fixtures for reviewed escape/comment bypass cases so the workflow validator pins the same contract as the registry validator. - Runs the fixture contract from the Node linter tests. Validation: - `npm test` in `tools/mdx-lint`: 30 passed. - Live connector-doc compatibility scan: 233 valid, 1 real doc issue, 71 missing docs. Rollout: - After merge, connector repos need to consume this workflow ref/tag before PR checks see the updated policy.
1 parent 5c02f9e commit d2a4189

3 files changed

Lines changed: 158 additions & 1 deletion

File tree

tools/mdx-lint/mdx-lint.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const ALLOWED_COMPONENTS = new Set([
3333
"Info",
3434
"Icon",
3535
"Frame",
36+
"CardGroup",
3637
"Card",
3738
"Check",
3839
"Tabs",
@@ -41,6 +42,7 @@ const ALLOWED_COMPONENTS = new Set([
4142
"Step",
4243
]);
4344

45+
const ALLOWED_INTRINSIC_ELEMENTS = new Set(["br"]);
4446
const URL_ATTRIBUTE_NAMES = new Set(["href", "src", "action", "formaction"]);
4547

4648
function decodeHtmlEntities(input) {
@@ -81,6 +83,26 @@ function fail(node, message) {
8183
throw new Error(`${at(node)} ${message}`);
8284
}
8385

86+
function isMdxCommentExpression(node) {
87+
const value = String(node.value ?? "").trim();
88+
if (!value) {
89+
return false;
90+
}
91+
92+
let rest = value;
93+
while (rest) {
94+
if (!rest.startsWith("/*")) {
95+
return false;
96+
}
97+
const end = rest.indexOf("*/", 2);
98+
if (end < 0) {
99+
return false;
100+
}
101+
rest = rest.slice(end + 2).trim();
102+
}
103+
return true;
104+
}
105+
84106
function validateUrlNode(node) {
85107
if (node.url && containsDangerousUrl(node.url)) {
86108
fail(node, "contains a dangerous URL scheme");
@@ -121,7 +143,10 @@ function validateJsxElement(node) {
121143
if (node.name.includes(".")) {
122144
fail(node, `contains disallowed JSX component "${node.name}"`);
123145
}
124-
if (!ALLOWED_COMPONENTS.has(node.name)) {
146+
if (
147+
!ALLOWED_COMPONENTS.has(node.name) &&
148+
!ALLOWED_INTRINSIC_ELEMENTS.has(node.name)
149+
) {
125150
fail(node, `contains disallowed JSX component "${node.name}"`);
126151
}
127152

@@ -138,6 +163,9 @@ function validateTree(tree) {
138163
break;
139164
case "mdxFlowExpression":
140165
case "mdxTextExpression":
166+
if (isMdxCommentExpression(node)) {
167+
break;
168+
}
141169
fail(node, "contains an MDX expression");
142170
break;
143171
case "mdxJsxFlowElement":

tools/mdx-lint/mdx-lint.test.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from "node:assert/strict";
2+
import { readFile } from "node:fs/promises";
23
import { describe, it } from "node:test";
34

45
import { lintMdxContent } from "./mdx-lint.mjs";
@@ -11,6 +12,13 @@ async function expectInvalid(content, pattern) {
1112
await assert.rejects(() => lintMdxContent(content), pattern);
1213
}
1314

15+
const sharedFixtures = JSON.parse(
16+
await readFile(
17+
new URL("./testdata/shared-mdx-validation.json", import.meta.url),
18+
"utf8",
19+
),
20+
);
21+
1422
describe("mdx-lint policy", () => {
1523
it("allows supported connector docs content", async () => {
1624
await expectValid(`---
@@ -70,6 +78,12 @@ Read the public setup guide at https://example.com/docs.
7078
await expectInvalid("{process.env.SECRET}\n", /MDX expression/);
7179
});
7280

81+
it("allows MDX comments", async () => {
82+
await expectValid(`{/* AUTO-GENERATED:START - capabilities
83+
Generated from baton_capabilities.json. Do not edit manually. */}
84+
`);
85+
});
86+
7387
it("rejects raw HTML elements", async () => {
7488
await expectInvalid("<div>raw html</div>\n", /disallowed JSX component "div"/);
7589
});
@@ -96,4 +110,19 @@ Read the public setup guide at https://example.com/docs.
96110
await expectInvalid("\ufeff# Title\n", /byte order marks/);
97111
await expectInvalid("hello\0world\n", /NUL bytes/);
98112
});
113+
114+
describe("shared validation fixtures", () => {
115+
for (const fixture of sharedFixtures) {
116+
it(fixture.name, async () => {
117+
if (fixture.valid) {
118+
await expectValid(fixture.content);
119+
return;
120+
}
121+
await expectInvalid(
122+
fixture.content,
123+
new RegExp(fixture.errorContains),
124+
);
125+
});
126+
}
127+
});
99128
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
[
2+
{
3+
"name": "allow dangerous scheme word in prose",
4+
"valid": true,
5+
"content": "<Steps>\n <Step>\n In the permissions section, choose the relevant permissions:\n\n To sync (read) data:\n\n - Project: Read\n </Step>\n</Steps>\n"
6+
},
7+
{
8+
"name": "allow angle bracket placeholder in inline code",
9+
"valid": true,
10+
"content": "Give the app a globally unique name, such as \"c1-integration-`<org name>`\".\n"
11+
},
12+
{
13+
"name": "allow data scheme literal in inline code",
14+
"valid": true,
15+
"content": "Use `data:` as the literal scheme name in examples.\n"
16+
},
17+
{
18+
"name": "allow data URL literal in fenced code",
19+
"valid": true,
20+
"content": "```json\n{\"tenant\":\"{tenant}\",\"url\":\"data:image/png;base64,abc\"}\n```\n"
21+
},
22+
{
23+
"name": "allow MDX comment",
24+
"valid": true,
25+
"content": "{/* AUTO-GENERATED:START - capabilities\n Generated from baton_capabilities.json. Do not edit manually. */}\n"
26+
},
27+
{
28+
"name": "allow nested indented fence placeholders",
29+
"valid": true,
30+
"content": " ```yaml expandable\n BATON_CLIENT_ID: <ConductorOne client ID>\n ```\n"
31+
},
32+
{
33+
"name": "allow tab-indented fenced content",
34+
"valid": true,
35+
"content": " ```json expandable\n\t{\n\t \"ok\": true\n\t}\n ```\n"
36+
},
37+
{
38+
"name": "allow escaped angle bracket placeholder",
39+
"valid": true,
40+
"content": "Use \\<CATALOG ITEM ID> as the placeholder.\n"
41+
},
42+
{
43+
"name": "reject double escaped JSX tag",
44+
"valid": false,
45+
"errorContains": "disallowed JSX component",
46+
"content": "This is not escaped JSX: \\\\<script>alert(1)</script>\n"
47+
},
48+
{
49+
"name": "reject escaped fake MDX comment hiding JSX",
50+
"valid": false,
51+
"errorContains": "dangerous URL scheme",
52+
"content": "Note: write the literal sequence \\{/* in your markdown when you need it.\n\n<Frame src=\"javascript:alert(1)\">bad</Frame>\n\nEnd of note */}.\n"
53+
},
54+
{
55+
"name": "reject inline code fake MDX comment hiding JSX",
56+
"valid": false,
57+
"errorContains": "dangerous URL scheme",
58+
"content": "Use `{/*` as literal syntax.\n\n<Frame src=\"javascript:alert(1)\">bad</Frame>\n\nUse `*/}` as literal syntax.\n"
59+
},
60+
{
61+
"name": "reject mixed MDX comment expression",
62+
"valid": false,
63+
"errorContains": "MDX expression",
64+
"content": "{/* a */ x /* b */}\n"
65+
},
66+
{
67+
"name": "allow table line break element",
68+
"valid": true,
69+
"content": "| Field | Description |\n| --- | --- |\n| Values | `one`<br/>`two` |\n"
70+
},
71+
{
72+
"name": "allow CardGroup layout wrapper",
73+
"valid": true,
74+
"content": "<CardGroup>\n<Card title=\"Learn more\" icon=\"book\" horizontal href=\"https://example.com/docs\" />\n</CardGroup>\n"
75+
},
76+
{
77+
"name": "reject backtick fence info string with backtick",
78+
"valid": false,
79+
"errorContains": "MDX expression",
80+
"content": "``` `\n{process.env.REGISTRY_TOKEN}\n```\n"
81+
},
82+
{
83+
"name": "reject markdown data URL",
84+
"valid": false,
85+
"errorContains": "dangerous URL scheme",
86+
"content": "[image](data:image/png;base64,abc)\n"
87+
},
88+
{
89+
"name": "reject JSX data URL attribute",
90+
"valid": false,
91+
"errorContains": "dangerous URL scheme",
92+
"content": "<Frame src=\"data:image/svg+xml,<svg onload=alert(1)>\">bad</Frame>\n"
93+
},
94+
{
95+
"name": "reject encoded javascript URL",
96+
"valid": false,
97+
"errorContains": "dangerous URL scheme",
98+
"content": "[link](java&#x73;cript:alert(1))\n"
99+
}
100+
]

0 commit comments

Comments
 (0)