radiant-docs-validator 0.1.3 → 0.1.4

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.
package/dist/index.d.ts CHANGED
@@ -163,11 +163,11 @@ type Footer = {
163
163
  socials?: Partial<Record<SocialPlatform, string>>;
164
164
  links?: FooterLink[];
165
165
  };
166
- type DocsPageHrefResolution = {
166
+ type DocsHrefResolution = {
167
167
  kind: "ignored";
168
168
  } | {
169
169
  kind: "invalid";
170
- reason: "not-root-absolute" | "generated-route" | "empty-target";
170
+ reason: "not-root-absolute" | "generated-route" | "empty-target" | "path-traversal";
171
171
  href: string;
172
172
  pathname: string;
173
173
  suffix: string;
@@ -179,10 +179,21 @@ type DocsPageHrefResolution = {
179
179
  pathname: string;
180
180
  suffix: string;
181
181
  filePath: string;
182
+ } | {
183
+ kind: "local-asset";
184
+ href: string;
185
+ pathname: string;
186
+ suffix: string;
187
+ filePath: string;
188
+ extension: string;
189
+ publishable: boolean;
182
190
  };
183
191
  declare function loadOpenApiSpec(filePathOrUrl: string): Promise<any>;
184
192
  declare function getConfig(): Promise<DocsConfig>;
185
- declare function resolveDocsPageHref(href: string): DocsPageHrefResolution;
193
+ declare const PUBLISHABLE_STATIC_ASSET_EXTENSIONS: readonly [".avif", ".csv", ".doc", ".docx", ".gif", ".ico", ".jpeg", ".jpg", ".mp3", ".mp4", ".ogg", ".pdf", ".png", ".ppt", ".pptx", ".svg", ".tsv", ".txt", ".wav", ".webm", ".webp", ".xls", ".xlsx", ".zip"];
194
+ declare function isPublishableStaticAssetPath(filePath: string): boolean;
195
+ declare function resolveDocsHref(href: string): DocsHrefResolution;
196
+ declare const resolveDocsPageHref: typeof resolveDocsHref;
186
197
  declare function validateMdxContent(): Promise<void>;
187
198
 
188
- export { type AssistantButtonConfig, type AssistantButtonSize, type AssistantConfig, type AssistantIcon, type AssistantNavbarButtonConfig, BASE_COLOR_OPTIONS, type BaseColorByMode, type BaseColorOption, type CardButtonTheme, type CardCoverTheme, type CardTheme, type CodeSyntaxThemeConfig, type CodeTheme, DEFAULT_THEME_COLOR_DARK, DEFAULT_THEME_COLOR_LIGHT, type DocsConfig, type DocsPageHrefResolution, type DocsTheme, type DocsValidatorOptions, type Footer, type FooterLink, type HiddenPageRoute, type Logo, type LogoVariant, type NavGroup, type NavMenu, type NavMenuItem, type NavOpenApi, type NavOpenApiPage, type NavOpenApiPageRef, type NavPage, type NavTag, type NavbarItem, type NavigationItem, type SocialPlatform, type TagTheme, type ThemeColorByMode, configureDocsValidator, getConfig, loadOpenApiSpec, resolveDocsPageHref, validateMdxContent };
199
+ export { type AssistantButtonConfig, type AssistantButtonSize, type AssistantConfig, type AssistantIcon, type AssistantNavbarButtonConfig, BASE_COLOR_OPTIONS, type BaseColorByMode, type BaseColorOption, type CardButtonTheme, type CardCoverTheme, type CardTheme, type CodeSyntaxThemeConfig, type CodeTheme, DEFAULT_THEME_COLOR_DARK, DEFAULT_THEME_COLOR_LIGHT, type DocsConfig, type DocsHrefResolution, type DocsTheme, type DocsValidatorOptions, type Footer, type FooterLink, type HiddenPageRoute, type Logo, type LogoVariant, type NavGroup, type NavMenu, type NavMenuItem, type NavOpenApi, type NavOpenApiPage, type NavOpenApiPageRef, type NavPage, type NavTag, type NavbarItem, type NavigationItem, PUBLISHABLE_STATIC_ASSET_EXTENSIONS, type SocialPlatform, type TagTheme, type ThemeColorByMode, configureDocsValidator, getConfig, isPublishableStaticAssetPath, loadOpenApiSpec, resolveDocsHref, resolveDocsPageHref, validateMdxContent };
package/dist/index.js CHANGED
@@ -289,14 +289,19 @@ function validateFileExistence(filePath, currentPath) {
289
289
  );
290
290
  }
291
291
  }
292
- function normalizeDocsFilePath(value) {
292
+ function normalizeDocsRootFilePath(value) {
293
293
  let normalized = value.replace(/\\/g, "/").trim();
294
294
  if (!normalized) return "";
295
295
  normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, "");
296
- normalized = normalized.replace(/\.(md|mdx)$/i, "");
297
296
  const posixNormalized = path.posix.normalize(`/${normalized}`).replace(/^\/+/, "");
298
297
  return posixNormalized === "." ? "" : posixNormalized;
299
298
  }
299
+ function normalizeDocsFilePath(value) {
300
+ return normalizeDocsRootFilePath(value).replace(/\.(md|mdx)$/i, "");
301
+ }
302
+ function containsPathTraversal(value) {
303
+ return value.replace(/\\/g, "/").split("/").includes("..");
304
+ }
300
305
  function normalizeDocsPagePath(value, currentPath, label = "Page path") {
301
306
  checkType(value, "string", currentPath, label);
302
307
  const trimmedPath = value.trim();
@@ -1986,6 +1991,35 @@ function validateComponentUsage(content) {
1986
1991
  var EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
1987
1992
  var DOCS_PAGE_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
1988
1993
  var DOCS_PAGE_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".htm", ".html"]);
1994
+ var PUBLISHABLE_STATIC_ASSET_EXTENSIONS = [
1995
+ ".avif",
1996
+ ".csv",
1997
+ ".doc",
1998
+ ".docx",
1999
+ ".gif",
2000
+ ".ico",
2001
+ ".jpeg",
2002
+ ".jpg",
2003
+ ".mp3",
2004
+ ".mp4",
2005
+ ".ogg",
2006
+ ".pdf",
2007
+ ".png",
2008
+ ".ppt",
2009
+ ".pptx",
2010
+ ".svg",
2011
+ ".tsv",
2012
+ ".txt",
2013
+ ".wav",
2014
+ ".webm",
2015
+ ".webp",
2016
+ ".xls",
2017
+ ".xlsx",
2018
+ ".zip"
2019
+ ];
2020
+ var PUBLISHABLE_STATIC_ASSET_EXTENSION_SET = new Set(
2021
+ PUBLISHABLE_STATIC_ASSET_EXTENSIONS
2022
+ );
1989
2023
  function isIgnoredHref(pathname) {
1990
2024
  if (!pathname) return true;
1991
2025
  if (pathname.startsWith("#")) return true;
@@ -1996,54 +2030,67 @@ function isIgnoredHref(pathname) {
1996
2030
  function getHrefExtension(pathname) {
1997
2031
  return path.posix.extname(pathname.replace(/\\/g, "/")).toLowerCase();
1998
2032
  }
1999
- function isDocsPageLikeHref(pathname) {
2000
- if (isIgnoredHref(pathname)) return false;
2001
- const extension = getHrefExtension(pathname);
2002
- if (!extension) return true;
2003
- if (DOCS_PAGE_SOURCE_EXTENSIONS.has(extension)) return true;
2004
- return DOCS_PAGE_ROUTE_EXTENSIONS.has(extension);
2033
+ function isPublishableStaticAssetPath(filePath) {
2034
+ return PUBLISHABLE_STATIC_ASSET_EXTENSION_SET.has(getHrefExtension(filePath));
2005
2035
  }
2006
- function resolveDocsPageHref(href) {
2036
+ function resolveDocsHref(href) {
2007
2037
  const { pathname, suffix } = splitHrefPathAndSuffix(href);
2008
2038
  const baseHref = pathname.trim();
2009
- if (!isDocsPageLikeHref(baseHref)) {
2039
+ if (isIgnoredHref(baseHref)) {
2010
2040
  return { kind: "ignored" };
2011
2041
  }
2012
- const filePath = normalizeDocsFilePath(baseHref);
2042
+ const normalizedRootFilePath = normalizeDocsRootFilePath(baseHref);
2013
2043
  const baseResult = {
2014
2044
  href,
2015
2045
  pathname: baseHref,
2016
2046
  suffix,
2017
- filePath
2047
+ filePath: normalizedRootFilePath
2018
2048
  };
2049
+ if (containsPathTraversal(baseHref)) {
2050
+ return {
2051
+ kind: "invalid",
2052
+ reason: "path-traversal",
2053
+ ...baseResult
2054
+ };
2055
+ }
2019
2056
  if (!baseHref.startsWith("/")) {
2020
2057
  return {
2021
2058
  kind: "invalid",
2022
2059
  reason: "not-root-absolute",
2023
2060
  ...baseResult,
2024
- suggestedHref: filePath ? `/${filePath}` : void 0
2061
+ suggestedHref: normalizedRootFilePath ? `/${normalizedRootFilePath}` : void 0
2025
2062
  };
2026
2063
  }
2027
- const extension = getHrefExtension(baseHref);
2028
- if (extension && !DOCS_PAGE_SOURCE_EXTENSIONS.has(extension)) {
2064
+ if (!normalizedRootFilePath) {
2029
2065
  return {
2030
2066
  kind: "invalid",
2031
- reason: "generated-route",
2067
+ reason: "empty-target",
2032
2068
  ...baseResult
2033
2069
  };
2034
2070
  }
2035
- if (!filePath) {
2071
+ const extension = getHrefExtension(baseHref);
2072
+ if (DOCS_PAGE_ROUTE_EXTENSIONS.has(extension)) {
2036
2073
  return {
2037
2074
  kind: "invalid",
2038
- reason: "empty-target",
2075
+ reason: "generated-route",
2039
2076
  ...baseResult
2040
2077
  };
2041
2078
  }
2079
+ if (!extension || DOCS_PAGE_SOURCE_EXTENSIONS.has(extension)) {
2080
+ return {
2081
+ kind: "docs-page",
2082
+ ...baseResult,
2083
+ filePath: normalizeDocsFilePath(baseHref)
2084
+ };
2085
+ }
2042
2086
  return {
2043
- kind: "docs-page",
2044
- ...baseResult
2087
+ kind: "local-asset",
2088
+ ...baseResult,
2089
+ extension,
2090
+ publishable: isPublishableStaticAssetPath(normalizedRootFilePath)
2045
2091
  };
2046
2092
  }
2093
+ var resolveDocsPageHref = resolveDocsHref;
2047
2094
  function addRoutableDocsFilePath(routableFilePaths, filePath) {
2048
2095
  if (typeof filePath !== "string") return;
2049
2096
  const normalizedFilePath = normalizeDocsFilePath(filePath);
@@ -2104,7 +2151,7 @@ function buildDocsLinkIndex(config, files) {
2104
2151
  };
2105
2152
  }
2106
2153
  function validateDocsRootAbsoluteHref(args) {
2107
- const resolvedHref = resolveDocsPageHref(args.href);
2154
+ const resolvedHref = resolveDocsHref(args.href);
2108
2155
  if (resolvedHref.kind === "ignored") return;
2109
2156
  if (resolvedHref.kind === "invalid") {
2110
2157
  if (resolvedHref.reason === "not-root-absolute") {
@@ -2118,10 +2165,34 @@ function validateDocsRootAbsoluteHref(args) {
2118
2165
  `Invalid internal link "${args.href}" in ${args.sourceFile}. Docs page links must point to MDX source paths from the docs root, not generated HTML routes. Use a path like "/guides/quickstart" or "/guides/quickstart.mdx".`
2119
2166
  );
2120
2167
  }
2168
+ if (resolvedHref.reason === "path-traversal") {
2169
+ throw new Error(
2170
+ `Invalid local URL "${args.href}" in ${args.sourceFile}. Local URLs cannot contain ".." path traversal segments.`
2171
+ );
2172
+ }
2121
2173
  throw new Error(
2122
2174
  `Invalid internal link "${args.href}" in ${args.sourceFile}. Docs page links must point to a docs page file from the docs root, such as "/guides/quickstart".`
2123
2175
  );
2124
2176
  }
2177
+ if (resolvedHref.kind === "local-asset") {
2178
+ const fullAssetPath = path.join(DOCS_DIR, resolvedHref.filePath);
2179
+ if (!fs.existsSync(fullAssetPath) || !fs.statSync(fullAssetPath).isFile()) {
2180
+ throw new Error(
2181
+ `Invalid local asset "${args.href}" in ${args.sourceFile}. No matching file was found. Expected "${resolvedHref.filePath}" under the docs root.`
2182
+ );
2183
+ }
2184
+ if (!resolvedHref.publishable) {
2185
+ throw new Error(
2186
+ `Invalid local asset "${args.href}" in ${args.sourceFile}. Files with extension "${resolvedHref.extension}" are not published as docs assets.`
2187
+ );
2188
+ }
2189
+ return;
2190
+ }
2191
+ if (args.expectedTarget === "asset") {
2192
+ throw new Error(
2193
+ `Invalid local asset "${args.href}" in ${args.sourceFile}. Asset references must include a supported file extension.`
2194
+ );
2195
+ }
2125
2196
  if (args.linkIndex.routableDocsFilePaths.has(resolvedHref.filePath)) {
2126
2197
  return;
2127
2198
  }
@@ -2150,7 +2221,16 @@ function createInternalLinkValidationPlugin(args) {
2150
2221
  validateDocsRootAbsoluteHref({
2151
2222
  href: node.url,
2152
2223
  sourceFile: args.sourceFile,
2153
- linkIndex: args.linkIndex
2224
+ linkIndex: args.linkIndex,
2225
+ expectedTarget: "link"
2226
+ });
2227
+ }
2228
+ if (node.type === "image" && typeof node.url === "string") {
2229
+ validateDocsRootAbsoluteHref({
2230
+ href: node.url,
2231
+ sourceFile: args.sourceFile,
2232
+ linkIndex: args.linkIndex,
2233
+ expectedTarget: "asset"
2154
2234
  });
2155
2235
  }
2156
2236
  if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") {
@@ -2161,12 +2241,15 @@ function createInternalLinkValidationPlugin(args) {
2161
2241
  for (const attribute of element.attributes) {
2162
2242
  const hrefAttribute = attribute;
2163
2243
  if (hrefAttribute.type !== "mdxJsxAttribute") continue;
2164
- if (hrefAttribute.name !== "href") continue;
2244
+ if (hrefAttribute.name !== "href" && hrefAttribute.name !== "src") {
2245
+ continue;
2246
+ }
2165
2247
  if (typeof hrefAttribute.value !== "string") continue;
2166
2248
  validateDocsRootAbsoluteHref({
2167
2249
  href: hrefAttribute.value,
2168
2250
  sourceFile: args.sourceFile,
2169
- linkIndex: args.linkIndex
2251
+ linkIndex: args.linkIndex,
2252
+ expectedTarget: hrefAttribute.name === "src" ? "asset" : "link"
2170
2253
  });
2171
2254
  }
2172
2255
  });
@@ -2232,12 +2315,15 @@ export {
2232
2315
  DEFAULT_SHIKI_LIGHT_THEME,
2233
2316
  DEFAULT_THEME_COLOR_DARK,
2234
2317
  DEFAULT_THEME_COLOR_LIGHT,
2318
+ PUBLISHABLE_STATIC_ASSET_EXTENSIONS,
2235
2319
  SHIKI_BUNDLED_THEME_NAMES,
2236
2320
  configureDocsValidator,
2237
2321
  docsSchema,
2238
2322
  getConfig,
2239
2323
  isBundledShikiThemeName,
2324
+ isPublishableStaticAssetPath,
2240
2325
  loadOpenApiSpec,
2326
+ resolveDocsHref,
2241
2327
  resolveDocsPageHref,
2242
2328
  validateMdxContent
2243
2329
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs-validator",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Shared validation for Radiant documentation repositories",
5
5
  "type": "module",
6
6
  "scripts": {