jamdesk 1.1.6 → 1.1.8

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 (79) hide show
  1. package/README.md +46 -1
  2. package/dist/__tests__/unit/docs-json-writer.test.js +59 -1
  3. package/dist/__tests__/unit/docs-json-writer.test.js.map +1 -1
  4. package/dist/__tests__/unit/spellcheck-fix.test.d.ts +2 -0
  5. package/dist/__tests__/unit/spellcheck-fix.test.d.ts.map +1 -0
  6. package/dist/__tests__/unit/spellcheck-fix.test.js +82 -0
  7. package/dist/__tests__/unit/spellcheck-fix.test.js.map +1 -0
  8. package/dist/__tests__/unit/spellcheck-utils.test.d.ts +2 -0
  9. package/dist/__tests__/unit/spellcheck-utils.test.d.ts.map +1 -0
  10. package/dist/__tests__/unit/spellcheck-utils.test.js +234 -0
  11. package/dist/__tests__/unit/spellcheck-utils.test.js.map +1 -0
  12. package/dist/__tests__/unit/tech-words.test.d.ts +2 -0
  13. package/dist/__tests__/unit/tech-words.test.d.ts.map +1 -0
  14. package/dist/__tests__/unit/tech-words.test.js +31 -0
  15. package/dist/__tests__/unit/tech-words.test.js.map +1 -0
  16. package/dist/commands/spellcheck.d.ts +13 -0
  17. package/dist/commands/spellcheck.d.ts.map +1 -0
  18. package/dist/commands/spellcheck.js +144 -0
  19. package/dist/commands/spellcheck.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +28 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/deps.js +2 -2
  24. package/dist/lib/docs-json-writer.d.ts +6 -0
  25. package/dist/lib/docs-json-writer.d.ts.map +1 -1
  26. package/dist/lib/docs-json-writer.js +29 -0
  27. package/dist/lib/docs-json-writer.js.map +1 -1
  28. package/dist/lib/openapi/types.d.ts +11 -6
  29. package/dist/lib/openapi/types.d.ts.map +1 -1
  30. package/dist/lib/path-security.d.ts +3 -0
  31. package/dist/lib/path-security.d.ts.map +1 -1
  32. package/dist/lib/path-security.js +14 -1
  33. package/dist/lib/path-security.js.map +1 -1
  34. package/dist/lib/spellcheck-fix.d.ts +37 -0
  35. package/dist/lib/spellcheck-fix.d.ts.map +1 -0
  36. package/dist/lib/spellcheck-fix.js +292 -0
  37. package/dist/lib/spellcheck-fix.js.map +1 -0
  38. package/dist/lib/spellcheck-utils.d.ts +36 -0
  39. package/dist/lib/spellcheck-utils.d.ts.map +1 -0
  40. package/dist/lib/spellcheck-utils.js +138 -0
  41. package/dist/lib/spellcheck-utils.js.map +1 -0
  42. package/dist/lib/tech-words.d.ts +9 -0
  43. package/dist/lib/tech-words.d.ts.map +1 -0
  44. package/dist/lib/tech-words.js +118 -0
  45. package/dist/lib/tech-words.js.map +1 -0
  46. package/package.json +15 -10
  47. package/vendored/app/[[...slug]]/page.tsx +48 -13
  48. package/vendored/app/api/assets/[...path]/route.ts +2 -0
  49. package/vendored/components/layout/LayoutWrapper.tsx +3 -4
  50. package/vendored/components/mdx/ApiEndpoint.tsx +13 -2
  51. package/vendored/components/mdx/MDXComponents.tsx +16 -0
  52. package/vendored/components/mdx/OpenApiEndpoint.tsx +76 -36
  53. package/vendored/components/mdx/Tabs.tsx +1 -1
  54. package/vendored/components/mdx/Video.tsx +82 -0
  55. package/vendored/components/navigation/Header.tsx +3 -3
  56. package/vendored/components/navigation/Sidebar.tsx +3 -3
  57. package/vendored/components/ui/CodePanel.tsx +5 -5
  58. package/vendored/components/ui/CodePanelModal.tsx +3 -3
  59. package/vendored/components/ui/DevOnlyNotice.tsx +78 -0
  60. package/vendored/hooks/useChatPanel.tsx +21 -2
  61. package/vendored/hooks/useMediaQuery.ts +27 -0
  62. package/vendored/lib/build-endpoint-from-mdx.ts +66 -0
  63. package/vendored/lib/isr-build-executor.ts +1 -1
  64. package/vendored/lib/middleware-helpers.ts +6 -1
  65. package/vendored/lib/openapi/code-examples.ts +479 -99
  66. package/vendored/lib/openapi/index.ts +9 -1
  67. package/vendored/lib/openapi/types.ts +29 -5
  68. package/vendored/lib/preprocess-mdx.ts +103 -36
  69. package/vendored/lib/process-mdx-with-exports.ts +22 -14
  70. package/vendored/lib/remark-extract-param-fields.ts +134 -0
  71. package/vendored/lib/shiki-client.ts +12 -0
  72. package/vendored/lib/static-artifacts.ts +2 -0
  73. package/vendored/lib/static-file-route.ts +1 -1
  74. package/vendored/lib/url-safety.ts +122 -0
  75. package/vendored/next.config.js +7 -0
  76. package/vendored/schema/docs-schema.json +35 -4
  77. package/vendored/scripts/copy-files.cjs +60 -54
  78. package/vendored/scripts/validate-links.cjs +1 -1
  79. package/vendored/shared/path-security.ts +17 -1
@@ -22,7 +22,7 @@ export type {
22
22
  OpenApiErrorType,
23
23
  OpenApiValidationError,
24
24
  ValidationResult,
25
- CodeExamples,
25
+ CodeExample,
26
26
  GeneratedPage,
27
27
  CachedSpec,
28
28
  } from './types';
@@ -58,9 +58,17 @@ export {
58
58
  // Code Examples
59
59
  export type { AuthMethod, CodeExampleConfig } from './code-examples';
60
60
  export {
61
+ DEFAULT_LANGUAGES,
62
+ SUPPORTED_LANGUAGES,
61
63
  generateCurlExample,
62
64
  generatePythonExample,
63
65
  generateJsExample,
66
+ generateGoExample,
67
+ generateRubyExample,
68
+ generateCsharpExample,
69
+ generateJavaExample,
70
+ generateRustExample,
71
+ generatePhpExample,
64
72
  generateCodeExamples,
65
73
  } from './code-examples';
66
74
 
@@ -6,9 +6,11 @@
6
6
  */
7
7
 
8
8
  import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
9
+ import type { AuthMethod } from './code-examples';
9
10
 
10
11
  // Re-export for convenience
11
12
  export type { OpenAPI, OpenAPIV3, OpenAPIV3_1 };
13
+ export type { AuthMethod };
12
14
 
13
15
  /**
14
16
  * JSON Schema type (simplified for our use case)
@@ -201,12 +203,17 @@ export interface ValidationResult {
201
203
  }
202
204
 
203
205
  /**
204
- * Generated code examples
206
+ * Single code example for a specific language
205
207
  */
206
- export interface CodeExamples {
207
- curl: string;
208
- python: string;
209
- javascript: string;
208
+ export interface CodeExample {
209
+ /** Language identifier (e.g., 'curl', 'python', 'go') */
210
+ id: string;
211
+ /** Display label for the tab (e.g., 'cURL', 'Python', 'Go') */
212
+ label: string;
213
+ /** Shiki language ID for syntax highlighting (e.g., 'bash', 'python', 'go') */
214
+ language: string;
215
+ /** Generated code string */
216
+ code: string;
210
217
  }
211
218
 
212
219
  /**
@@ -230,3 +237,20 @@ export interface CachedSpec {
230
237
  version: '2.0' | '3.0' | '3.1';
231
238
  timestamp: number;
232
239
  }
240
+
241
+ /**
242
+ * User-supplied values from the API playground form.
243
+ * Passed to generateCodeExamplesFromValues to produce live code previews.
244
+ */
245
+ export interface PlaygroundValues {
246
+ /** Raw credential value (token, key, base64 credentials) */
247
+ auth?: string;
248
+ /** Auth scheme from docs.json api.mdx.auth.method */
249
+ authMethod?: AuthMethod;
250
+ /** Custom header name for 'key' auth method, from docs.json api.mdx.auth.name */
251
+ authHeaderName?: string;
252
+ pathParams: Record<string, string>;
253
+ queryParams: Record<string, string>;
254
+ headerParams: Record<string, string>;
255
+ body: Record<string, unknown>;
256
+ }
@@ -317,8 +317,9 @@ export function ensureBlankLinesInJsx(content: string): string {
317
317
  // Check it's not an HTML element or self-closing tag
318
318
  if (/^<[a-z]/.test(trimmed)) return false;
319
319
 
320
- // Check it's not a code fence
321
- if (/^```/.test(trimmed)) return false;
320
+ // Code fences need blank line separation from JSX tags too — without it,
321
+ // MDX doesn't transition to markdown mode and the fence can be misinterpreted
322
+ if (/^```/.test(trimmed)) return true;
322
323
 
323
324
  // Check it's not frontmatter
324
325
  if (/^---/.test(trimmed)) return false;
@@ -409,6 +410,8 @@ export function normalizeJsxIndentation(content: string): string {
409
410
  let baseIndent = 0; // The indentation of the outermost JSX opening tag
410
411
  // Track if we're inside a fenced code block (``` ... ```)
411
412
  let insideCodeFence = false;
413
+ // How many spaces to strip from code fence content (computed from fence marker normalization)
414
+ let fenceIndentShift = 0;
412
415
 
413
416
  for (let i = 0; i < lines.length; i++) {
414
417
  let line = lines[i];
@@ -418,12 +421,38 @@ export function normalizeJsxIndentation(content: string): string {
418
421
  // Must check BEFORE any other processing to preserve code block content
419
422
  if (trimmed.startsWith('```')) {
420
423
  insideCodeFence = !insideCodeFence;
424
+ if (!insideCodeFence) {
425
+ // Closing fence: normalize its indentation, then reset shift
426
+ if (jsxDepth > 0 && fenceIndentShift > 0) {
427
+ const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
428
+ const newIndent = Math.max(0, currentIndent - fenceIndentShift);
429
+ line = ' '.repeat(newIndent) + trimmed;
430
+ }
431
+ fenceIndentShift = 0;
432
+ } else if (jsxDepth > 0) {
433
+ // Opening fence: normalize indentation and compute shift for content
434
+ const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
435
+ const excessIndent = currentIndent - baseIndent;
436
+ if (excessIndent >= 4) {
437
+ const newIndent = baseIndent + 2;
438
+ fenceIndentShift = currentIndent - newIndent;
439
+ line = ' '.repeat(newIndent) + trimmed;
440
+ } else {
441
+ fenceIndentShift = 0;
442
+ }
443
+ }
421
444
  result.push(line);
422
445
  continue;
423
446
  }
424
447
 
425
- // If inside a code fence, preserve the line exactly as-is
448
+ // If inside a code fence, apply the same indent shift as the fence marker
449
+ // to preserve relative indentation within the code block
426
450
  if (insideCodeFence) {
451
+ if (fenceIndentShift > 0) {
452
+ const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
453
+ const newIndent = Math.max(0, currentIndent - fenceIndentShift);
454
+ line = ' '.repeat(newIndent) + line.trimStart();
455
+ }
427
456
  result.push(line);
428
457
  continue;
429
458
  }
@@ -700,6 +729,48 @@ export function transformImageDimensions(content: string): string {
700
729
  return transformOutsideCodeBlocks(content, transformImageDimensionsInText);
701
730
  }
702
731
 
732
+ // Keep in sync with VIDEO_EXTENSIONS in components/mdx/Video.tsx
733
+ const VIDEO_EXTENSIONS = ['.mp4', '.webm'];
734
+
735
+ /** Asset directory prefixes handled by path transforms */
736
+ const ASSET_DIRECTORIES = ['images', 'videos'] as const;
737
+
738
+ /**
739
+ * Convert markdown video syntax to <Video> JSX component.
740
+ *
741
+ * Converts:
742
+ * - ![alt](/videos/demo.mp4) -> <Video src="/videos/demo.mp4" title="alt" />
743
+ * - ![](/videos/demo.webm) -> <Video src="/videos/demo.webm" />
744
+ *
745
+ * This must run BEFORE path transforms so the <Video> src attribute gets
746
+ * rewritten by transformRelativeImagePaths. Converting to JSX also avoids
747
+ * the <p><video> hydration error — MDX wraps markdown images in <p>, but
748
+ * JSX components get their own block context.
749
+ */
750
+ function transformMarkdownVideos(content: string): string {
751
+ return transformOutsideCodeBlocks(content, transformMarkdownVideosInText);
752
+ }
753
+
754
+ function transformMarkdownVideosInText(text: string): string {
755
+ // Same pattern as markdown images: ![alt](url)
756
+ const pattern = /!\[([^\]]*)\]\(([^)\s]+)(\s+["'][^"']*["'])?\)/g;
757
+
758
+ return text.replace(pattern, (match, alt, url) => {
759
+ const urlStr = url as string;
760
+ // Skip external URLs and data URIs — only convert local video paths
761
+ if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('data:')) {
762
+ return match;
763
+ }
764
+ const pathOnly = urlStr.split('?')[0].toLowerCase();
765
+ if (!VIDEO_EXTENSIONS.some(ext => pathOnly.endsWith(ext))) {
766
+ return match; // Not a video — leave as markdown image
767
+ }
768
+ const escapedAlt = (alt as string).replace(/"/g, '&quot;');
769
+ const titleAttr = escapedAlt ? ` title="${escapedAlt}"` : '';
770
+ return `<Video src="${urlStr}"${titleAttr} />`;
771
+ });
772
+ }
773
+
703
774
  /**
704
775
  * Transform Markdown image paths to use the Jamdesk asset prefix.
705
776
  *
@@ -735,20 +806,11 @@ function normalizeMarkdownImageUrl(url: string, assetVersion?: string): string |
735
806
  return null;
736
807
  }
737
808
 
738
- if (url.startsWith(`${ASSET_PREFIX}/images/`)) {
739
- return null;
740
- }
741
-
742
- if (url.startsWith('/images/')) {
743
- return appendAssetVersion(`${ASSET_PREFIX}${url}`, assetVersion);
744
- }
745
-
746
- if (url.startsWith('./images/')) {
747
- return appendAssetVersion(`${ASSET_PREFIX}/${url.slice(2)}`, assetVersion);
748
- }
749
-
750
- if (url.startsWith('images/')) {
751
- return appendAssetVersion(`${ASSET_PREFIX}/${url}`, assetVersion);
809
+ for (const dir of ASSET_DIRECTORIES) {
810
+ if (url.startsWith(`${ASSET_PREFIX}/${dir}/`)) return null;
811
+ if (url.startsWith(`/${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}${url}`, assetVersion);
812
+ if (url.startsWith(`./${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}/${url.slice(2)}`, assetVersion);
813
+ if (url.startsWith(`${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}/${url}`, assetVersion);
752
814
  }
753
815
 
754
816
  return null;
@@ -800,24 +862,23 @@ export function transformRelativeImagePaths(content: string, assetVersion?: stri
800
862
  let result = content;
801
863
 
802
864
  for (const prop of IMAGE_PATH_PROPS) {
803
- // Shared replacement: rewrites the captured image path under ASSET_PREFIX
804
- const rewrite = (_match: string, quote: string, imgPath: string): string =>
805
- `${prop}=${quote}${appendAssetVersion(`${ASSET_PREFIX}/${imgPath}`, assetVersion)}${quote}`;
865
+ for (const dir of ASSET_DIRECTORIES) {
866
+ const rewrite = (_match: string, quote: string, assetPath: string): string =>
867
+ `${prop}=${quote}${appendAssetVersion(`${ASSET_PREFIX}/${assetPath}`, assetVersion)}${quote}`;
806
868
 
807
- // ./images/... -> /_jd/images/...?v=xxx
808
- result = result.replace(
809
- new RegExp(`${prop}=(["'])\\./(images/[^"']+)\\1`, 'g'), rewrite,
810
- );
811
-
812
- // images/... -> /_jd/images/...?v=xxx (skip URLs, data URIs, and paths starting with /)
813
- result = result.replace(
814
- new RegExp(`${prop}=(["'])(?!/|https?://|data:)(images/[^"']+)\\1`, 'g'), rewrite,
815
- );
816
-
817
- // /images/... -> /_jd/images/...?v=xxx
818
- result = result.replace(
819
- new RegExp(`${prop}=(["'])/(images/[^"']+)\\1`, 'g'), rewrite,
820
- );
869
+ // ./{dir}/... -> /_jd/{dir}/...
870
+ result = result.replace(
871
+ new RegExp(`${prop}=(["'])\\./(${dir}/[^"']+)\\1`, 'g'), rewrite,
872
+ );
873
+ // {dir}/... -> /_jd/{dir}/... (skip URLs, data URIs, paths starting with /)
874
+ result = result.replace(
875
+ new RegExp(`${prop}=(["'])(?!/|https?://|data:)(${dir}/[^"']+)\\1`, 'g'), rewrite,
876
+ );
877
+ // /{dir}/... -> /_jd/{dir}/...
878
+ result = result.replace(
879
+ new RegExp(`${prop}=(["'])/(${dir}/[^"']+)\\1`, 'g'), rewrite,
880
+ );
881
+ }
821
882
  }
822
883
 
823
884
  return result;
@@ -863,8 +924,9 @@ export function containsView(content: string): boolean {
863
924
  * 6. Un-indents JSX blocks that follow list items (prevents list context errors)
864
925
  * 7. Ensures blank lines around markdown in JSX components
865
926
  * 8. Normalizes indentation inside JSX components (prevents code block interpretation)
866
- * 9. Transforms Markdown image dimension syntax to HTML img tags
867
- * 10. Transforms relative image paths to absolute paths
927
+ * 9. Converts markdown video syntax ![alt](video.mp4) to <Video> JSX
928
+ * 10. Transforms Markdown image dimension syntax to HTML img tags
929
+ * 11. Transforms relative image/video paths to absolute paths
868
930
  *
869
931
  * @param content - Raw MDX content
870
932
  * @returns Preprocessed MDX content
@@ -901,6 +963,11 @@ export function preprocessMdx(content: string, options?: { assetVersion?: string
901
963
  // 4+ space indentation from being interpreted as code blocks
902
964
  processed = normalizeJsxIndentation(processed);
903
965
 
966
+ // Convert markdown video syntax ![alt](/videos/x.mp4) to <Video> JSX.
967
+ // Must run BEFORE transformImageDimensions and path transforms so the
968
+ // <Video> src attribute gets rewritten correctly.
969
+ processed = transformMarkdownVideos(processed);
970
+
904
971
  // Transform Markdown images with dimension syntax to HTML img tags
905
972
  // Must run BEFORE transformRelativeImagePaths (which works on src= attributes)
906
973
  processed = transformImageDimensions(processed);
@@ -13,6 +13,8 @@ import remarkMdx from 'remark-mdx';
13
13
  import { VFile } from 'vfile';
14
14
  import { remarkExtractExports } from './remark-extract-exports';
15
15
  import { compileInlineComponents } from './mdx-inline-components';
16
+ import { remarkExtractParamFields } from './remark-extract-param-fields';
17
+ import type { ExtractedParam } from './remark-extract-param-fields';
16
18
 
17
19
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
20
  type AnyComponent = React.ComponentType<any>;
@@ -20,8 +22,17 @@ type AnyComponent = React.ComponentType<any>;
20
22
  interface ProcessResult {
21
23
  inlineComponents: Record<string, AnyComponent>;
22
24
  hasInlineExports: boolean;
25
+ paramFields: ExtractedParam[];
23
26
  }
24
27
 
28
+ // Build the unified processor once at module level (frozen + reusable).
29
+ // This avoids re-creating the pipeline on every ISR page request.
30
+ const mdxProcessor = unified()
31
+ .use(remarkParse)
32
+ .use(remarkMdx)
33
+ .use(remarkExtractExports)
34
+ .use(remarkExtractParamFields);
35
+
25
36
  /**
26
37
  * Extract and compile inline components from MDX source.
27
38
  *
@@ -35,29 +46,25 @@ export async function extractInlineComponents(
35
46
  ): Promise<ProcessResult> {
36
47
  // Defensive: return empty if source is not a valid string
37
48
  if (!source || typeof source !== 'string') {
38
- return { inlineComponents: {}, hasInlineExports: false };
49
+ return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
39
50
  }
40
51
 
41
- // Quick check: if no "export const" in source, skip processing
42
- if (!source.includes('export const')) {
43
- return { inlineComponents: {}, hasInlineExports: false };
52
+ // Fast-path: skip remark pipeline when neither inline exports nor ParamFields exist
53
+ if (!source.includes('export const') && !source.includes('ParamField')) {
54
+ return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
44
55
  }
45
56
 
46
57
  try {
47
- const processor = unified().use(remarkParse).use(remarkMdx).use(remarkExtractExports);
48
-
49
- // Create VFile to hold the content and collect metadata
50
58
  const inputFile = new VFile({ value: source });
59
+ const tree = mdxProcessor.parse(inputFile);
60
+ await mdxProcessor.run(tree, inputFile);
51
61
 
52
- // Parse and run the transforms
53
- const tree = processor.parse(inputFile);
54
- await processor.run(tree, inputFile);
55
-
62
+ const paramFields: ExtractedParam[] = inputFile.data.paramFields ?? [];
56
63
  const inlineExports = inputFile.data.inlineExports || {};
57
64
 
58
- // No exports found
65
+ // No exports found — still return paramFields
59
66
  if (Object.keys(inlineExports).length === 0) {
60
- return { inlineComponents: {}, hasInlineExports: false };
67
+ return { inlineComponents: {}, hasInlineExports: false, paramFields };
61
68
  }
62
69
 
63
70
  // Compile with access to built-in MDX components
@@ -66,10 +73,11 @@ export async function extractInlineComponents(
66
73
  return {
67
74
  inlineComponents,
68
75
  hasInlineExports: Object.keys(inlineExports).length > 0,
76
+ paramFields,
69
77
  };
70
78
  } catch (error) {
71
79
  // Log but don't fail the build - return empty components
72
80
  console.warn('[MDX] Failed to extract inline components:', error instanceof Error ? error.message : error);
73
- return { inlineComponents: {}, hasInlineExports: false };
81
+ return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
74
82
  }
75
83
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Remark plugin to extract <ParamField> data from MDX AST.
3
+ *
4
+ * Visits mdxJsxFlowElement nodes named 'ParamField' and extracts
5
+ * the param metadata (name, location, type, required, default) into
6
+ * file.data.paramFields for use by the API playground.
7
+ *
8
+ * ParamField syntax mirrors Mintlify's convention:
9
+ * <ParamField body="name" type="string" required>...</ParamField>
10
+ * The attribute name (body/query/path/header) determines the param location,
11
+ * and its string value is the param name.
12
+ */
13
+
14
+ import type { Root } from 'mdast';
15
+ import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
16
+ import { visit } from 'unist-util-visit';
17
+ import type { VFile } from 'vfile';
18
+
19
+ export interface ExtractedParam {
20
+ name: string;
21
+ in: 'body' | 'query' | 'path' | 'header';
22
+ type?: string;
23
+ required: boolean;
24
+ default?: unknown;
25
+ }
26
+
27
+ declare module 'vfile' {
28
+ interface DataMap {
29
+ paramFields?: ExtractedParam[];
30
+ }
31
+ }
32
+
33
+ const LOCATION_ATTRS = ['body', 'query', 'path', 'header'] as const;
34
+ type ParamLocation = (typeof LOCATION_ATTRS)[number];
35
+
36
+ /**
37
+ * Parse the raw value of a default attribute.
38
+ *
39
+ * Expression defaults like `default={1}` have their inner text as the raw
40
+ * value. We try JSON.parse first (handles numbers, booleans, quoted strings)
41
+ * and fall back to returning the raw string.
42
+ */
43
+ function parseDefaultValue(raw: string): unknown {
44
+ try {
45
+ return JSON.parse(raw);
46
+ } catch {
47
+ return raw;
48
+ }
49
+ }
50
+
51
+ export function remarkExtractParamFields() {
52
+ return (tree: Root, file: VFile) => {
53
+ const params: ExtractedParam[] = [];
54
+
55
+ visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
56
+ if (node.name !== 'ParamField') return;
57
+
58
+ let paramName: string | undefined;
59
+ let paramIn: ParamLocation | undefined;
60
+ let paramType: string | undefined;
61
+ let paramRequired = false;
62
+ let paramDefault: unknown = undefined;
63
+
64
+ for (const attr of node.attributes) {
65
+ // Only process standard JSX attributes (not spread attributes)
66
+ if (attr.type !== 'mdxJsxAttribute') continue;
67
+
68
+ const attrName = attr.name as string;
69
+ const attrValue = attr.value;
70
+
71
+ // Location attribute: name is the location, value is the param name
72
+ if ((LOCATION_ATTRS as ReadonlyArray<string>).includes(attrName)) {
73
+ paramIn = attrName as ParamLocation;
74
+ // Value must be a plain string for the param name
75
+ if (typeof attrValue === 'string') {
76
+ paramName = attrValue;
77
+ }
78
+ continue;
79
+ }
80
+
81
+ if (attrName === 'type') {
82
+ if (typeof attrValue === 'string') {
83
+ paramType = attrValue;
84
+ }
85
+ continue;
86
+ }
87
+
88
+ if (attrName === 'required') {
89
+ if (attrValue === null) {
90
+ // Boolean attribute without value: <ParamField required>
91
+ paramRequired = true;
92
+ } else if (
93
+ typeof attrValue === 'object' &&
94
+ attrValue.type === 'mdxJsxAttributeValueExpression'
95
+ ) {
96
+ // Expression syntax: required={true} or required={false}
97
+ paramRequired = parseDefaultValue(attrValue.value) === true;
98
+ }
99
+ continue;
100
+ }
101
+
102
+ if (attrName === 'default') {
103
+ if (typeof attrValue === 'string') {
104
+ // Plain string default: default="created_at"
105
+ paramDefault = attrValue;
106
+ } else if (
107
+ attrValue != null &&
108
+ typeof attrValue === 'object' &&
109
+ attrValue.type === 'mdxJsxAttributeValueExpression'
110
+ ) {
111
+ // Expression default: default={1}
112
+ paramDefault = parseDefaultValue(attrValue.value);
113
+ }
114
+ continue;
115
+ }
116
+ }
117
+
118
+ // Only emit a param if we found a valid location + name pair
119
+ if (paramName !== undefined && paramIn !== undefined) {
120
+ params.push({
121
+ name: paramName,
122
+ in: paramIn,
123
+ type: paramType,
124
+ required: paramRequired,
125
+ default: paramDefault,
126
+ });
127
+ }
128
+ });
129
+
130
+ if (params.length > 0) {
131
+ file.data.paramFields = params;
132
+ }
133
+ };
134
+ }
@@ -27,6 +27,12 @@ const CLIENT_LANGUAGES: BundledLanguage[] = [
27
27
  'bash',
28
28
  'json',
29
29
  'shell',
30
+ 'go',
31
+ 'ruby',
32
+ 'csharp',
33
+ 'java',
34
+ 'rust',
35
+ 'php',
30
36
  ];
31
37
 
32
38
  // Dual themes for light/dark mode support
@@ -63,6 +69,12 @@ async function getHighlighter(): Promise<HighlighterCore> {
63
69
  import('shiki/langs/bash.mjs'),
64
70
  import('shiki/langs/json.mjs'),
65
71
  import('shiki/langs/shell.mjs'),
72
+ import('shiki/langs/go.mjs'),
73
+ import('shiki/langs/ruby.mjs'),
74
+ import('shiki/langs/csharp.mjs'),
75
+ import('shiki/langs/java.mjs'),
76
+ import('shiki/langs/rust.mjs'),
77
+ import('shiki/langs/php.mjs'),
66
78
  ],
67
79
  engine: createJavaScriptRegexEngine(),
68
80
  });
@@ -164,6 +164,8 @@ Disallow: /
164
164
  return `User-agent: *
165
165
  Allow: /
166
166
 
167
+ Disallow: /_next/
168
+
167
169
  Sitemap: ${sitemapUrl || defaultSitemapUrl}
168
170
  `;
169
171
  }
@@ -25,7 +25,7 @@ export const STATIC_REVALIDATION_PATHS = STATIC_FILE_NAMES.flatMap(
25
25
  const CONTENT_TYPES: Record<string, string> = {
26
26
  '.xml': 'application/xml',
27
27
  '.json': 'application/json',
28
- '.txt': 'text/plain',
28
+ '.txt': 'text/plain; charset=utf-8',
29
29
  };
30
30
 
31
31
  /**
@@ -0,0 +1,122 @@
1
+ // DO NOT EDIT — this file is auto-synced from shared/. Edit the source in shared/ and run ./scripts/sync-shared.sh
2
+
3
+ /**
4
+ * SSRF protection: check if a URL is safe to proxy/fetch.
5
+ * Blocks private IPs, loopback, link-local, and metadata endpoints.
6
+ * Supports decimal, hex, octal, and mixed IP encodings (SSRF bypass vectors).
7
+ *
8
+ * Unlike sanitize-url.ts (proxy), this allows both HTTP and HTTPS
9
+ * since API playground requests may target staging/dev APIs over HTTP.
10
+ */
11
+
12
+ /**
13
+ * Parse a hostname as an IPv4 address in any encoding.
14
+ * Handles dotted decimal, decimal integer, hex, octal, and mixed formats.
15
+ * Returns the 32-bit unsigned long, or null if not an IP.
16
+ */
17
+ function parseIPv4ToLong(hostname: string): number | null {
18
+ // Pure decimal integer (e.g., 2130706433 = 127.0.0.1)
19
+ if (/^\d+$/.test(hostname)) {
20
+ const val = Number(hostname);
21
+ if (val >= 0 && val <= 0xFFFFFFFF) return val;
22
+ }
23
+
24
+ // Hex integer (e.g., 0x7f000001 = 127.0.0.1)
25
+ if (/^0x[0-9a-f]+$/i.test(hostname)) {
26
+ const val = parseInt(hostname, 16);
27
+ if (val >= 0 && val <= 0xFFFFFFFF) return val;
28
+ }
29
+
30
+ // Dotted notation (decimal, octal, or hex parts)
31
+ const parts = hostname.split('.');
32
+ if (parts.length < 1 || parts.length > 4) return null;
33
+
34
+ const parsed: number[] = [];
35
+ for (const part of parts) {
36
+ if (!part) return null;
37
+ let val: number;
38
+ if (/^0x[0-9a-f]+$/i.test(part)) {
39
+ val = parseInt(part, 16);
40
+ } else if (/^0\d+$/.test(part)) {
41
+ // Octal (e.g., 0177 = 127)
42
+ val = parseInt(part, 8);
43
+ } else if (/^\d+$/.test(part)) {
44
+ val = parseInt(part, 10);
45
+ } else {
46
+ return null; // Not a numeric IP
47
+ }
48
+ if (isNaN(val) || val < 0) return null;
49
+ parsed.push(val);
50
+ }
51
+
52
+ // Convert to 32-bit long based on part count (matches BSD socket behavior)
53
+ if (parsed.length === 1) return parsed[0];
54
+ if (parsed.length === 2) {
55
+ if (parsed[0] > 0xFF || parsed[1] > 0xFFFFFF) return null;
56
+ return (parsed[0] << 24) | parsed[1];
57
+ }
58
+ if (parsed.length === 3) {
59
+ if (parsed[0] > 0xFF || parsed[1] > 0xFF || parsed[2] > 0xFFFF) return null;
60
+ return (parsed[0] << 24) | (parsed[1] << 16) | parsed[2];
61
+ }
62
+ if (parsed.length === 4) {
63
+ if (parsed.some(p => p > 0xFF)) return null;
64
+ return (parsed[0] << 24) | (parsed[1] << 16) | (parsed[2] << 8) | parsed[3];
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Check if an IPv4 long integer falls in a private/reserved range.
71
+ */
72
+ function isPrivateIPv4(ip: number): boolean {
73
+ // Use unsigned right-shift to ensure correct comparison for high addresses
74
+ const uip = ip >>> 0;
75
+ return (
76
+ (uip >>> 24) === 0 || // 0.0.0.0/8
77
+ (uip >>> 24) === 10 || // 10.0.0.0/8
78
+ (uip >>> 24) === 127 || // 127.0.0.0/8 (loopback)
79
+ (uip >>> 20) === 0xAC1 || // 172.16.0.0/12
80
+ (uip >>> 16) === 0xC0A8 || // 192.168.0.0/16
81
+ (uip >>> 16) === 0xA9FE || // 169.254.0.0/16 (link-local/metadata)
82
+ uip === 0xFFFFFFFF // 255.255.255.255 (broadcast)
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Returns true if the URL is safe to fetch (not an internal/private address).
88
+ * Both HTTP and HTTPS are allowed — API playground may target dev APIs over HTTP.
89
+ * Returns false for invalid URLs, private IPs, localhost, and link-local addresses.
90
+ */
91
+ export function isUrlSafe(rawUrl: string): boolean {
92
+ if (!rawUrl) return false;
93
+ try {
94
+ const url = new URL(rawUrl);
95
+
96
+ // Only allow HTTP and HTTPS schemes
97
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
98
+
99
+ const hostname = url.hostname.toLowerCase();
100
+
101
+ // Block localhost by name
102
+ if (hostname === 'localhost') return false;
103
+
104
+ // Block ALL IPv6 addresses (bracketed hostnames).
105
+ // IPv6-embedded IPv4 (e.g. ::127.0.0.1 → ::7f00:1) bypasses prefix-based
106
+ // checks. Legitimate API endpoints virtually never use raw IPv6 literals,
107
+ // so blocking all bracketed hostnames is the safest approach.
108
+ if (hostname.startsWith('[')) {
109
+ return false;
110
+ }
111
+
112
+ // Parse as IPv4 (handles dotted, decimal, octal, hex, and mixed encodings)
113
+ const ipLong = parseIPv4ToLong(hostname);
114
+ if (ipLong !== null) {
115
+ if (isPrivateIPv4(ipLong)) return false;
116
+ }
117
+
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
@@ -14,6 +14,13 @@
14
14
  const nextConfig = {
15
15
  // Hide dev indicator (floating icon in bottom-left)
16
16
  devIndicators: false,
17
+ // Rewrite /_jd/ asset paths to public/ in dev (ISR middleware handles this in production)
18
+ async rewrites() {
19
+ return [
20
+ { source: '/_jd/images/:path*', destination: '/images/:path*' },
21
+ { source: '/_jd/videos/:path*', destination: '/videos/:path*' },
22
+ ];
23
+ },
17
24
  // Allow /_jd/ image paths with ?v= cache-busting query strings in <Image>
18
25
  images: {
19
26
  localPatterns: [