jamdesk 1.1.26 → 1.1.27

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.
@@ -139,6 +139,7 @@ export interface ParsedOpenApiFrontmatter {
139
139
  specPath: string;
140
140
  method: HttpMethod;
141
141
  path: string;
142
+ isShortFormat: boolean;
142
143
  }
143
144
  /**
144
145
  * Validation error types
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/openapi/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/openapi/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.26",
3
+ "version": "1.1.27",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -27,6 +27,7 @@ import { PageNavigation } from '@/components/navigation/PageNavigation';
27
27
  import { SocialFooter } from '@/components/navigation/SocialFooter';
28
28
  import { ApiPageWrapper } from '@/components/mdx/ApiPage';
29
29
  import { OpenApiEndpoint } from '@/components/mdx/OpenApiEndpoint';
30
+ import { OpenApiError } from '@/components/openapi/OpenApiError';
30
31
  import { getHighlighter } from '@/lib/shiki-highlighter';
31
32
  import { createShikiRehypePlugin } from '@/lib/shiki-config';
32
33
  import rehypeSlug from 'rehype-slug';
@@ -69,6 +70,7 @@ import {
69
70
  generateCodeExamples,
70
71
  formatOpenApiWarning,
71
72
  type OpenApiEndpointData,
73
+ type OpenApiValidationError,
72
74
  type CodeExample,
73
75
  type AuthMethod,
74
76
  } from '@/lib/openapi';
@@ -547,46 +549,76 @@ export default async function DocPage({ params }: PageProps) {
547
549
  // Parse OpenAPI endpoint data if openapi frontmatter is present
548
550
  let openApiEndpointData: OpenApiEndpointData | null = null;
549
551
  let openApiCodeExamples: CodeExample[] | null = null;
552
+ let openApiError: string | null = null;
550
553
 
551
554
  // OpenAPI spec parsing - supports both static and ISR modes
552
555
  if (data.openapi && typeof data.openapi === 'string') {
553
556
  try {
554
- // Get default spec path from config if configured
555
- const defaultSpecPath = typeof config.api?.openapi === 'string' ? config.api.openapi : undefined;
556
-
557
- // Parse the frontmatter to get spec path, method, and endpoint path
558
- const parsed = parseOpenApiFrontmatter(data.openapi, defaultSpecPath);
559
-
560
- if (isIsrMode() && projectSlug) {
561
- // ISR mode: fetch spec from R2 or URL
562
- const { resolveOpenApiSpec } = await import('@/lib/openapi-isr');
563
- const spec = await resolveOpenApiSpec(projectSlug, parsed.specPath);
564
- // Cast to expected type - the ISR fetcher validates the spec structure
565
- openApiEndpointData = parseEndpoint(
566
- spec as Parameters<typeof parseEndpoint>[0],
567
- parsed.method,
568
- parsed.path,
569
- parsed.specPath
570
- );
571
- } else {
572
- // Static mode: fetch spec from filesystem
573
- const contentDir = getContentDir();
574
- const { api } = await getCachedSpec(parsed.specPath, contentDir);
575
- openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, parsed.specPath);
557
+ // Normalize config to array (handles string, array, or undefined)
558
+ const openApiConfig = config.api?.openapi;
559
+ const allSpecPaths: string[] = typeof openApiConfig === 'string'
560
+ ? [openApiConfig]
561
+ : Array.isArray(openApiConfig)
562
+ ? openApiConfig
563
+ : [];
564
+
565
+ const parsed = parseOpenApiFrontmatter(
566
+ data.openapi,
567
+ allSpecPaths.length > 0 ? allSpecPaths : undefined
568
+ );
569
+
570
+ const specsToTry = parsed.isShortFormat && allSpecPaths.length > 1
571
+ ? allSpecPaths
572
+ : [parsed.specPath];
573
+
574
+ // Hoist mode-dependent values before the loop
575
+ const useIsr = isIsrMode() && !!projectSlug;
576
+ const resolveSpec = useIsr
577
+ ? (await import('@/lib/openapi-isr')).resolveOpenApiSpec
578
+ : null;
579
+ const contentDir = useIsr ? null : getContentDir();
580
+
581
+ let lastError: unknown = null;
582
+
583
+ for (const specPath of specsToTry) {
584
+ try {
585
+ if (resolveSpec && projectSlug) {
586
+ const spec = await resolveSpec(projectSlug, specPath);
587
+ openApiEndpointData = parseEndpoint(
588
+ spec as Parameters<typeof parseEndpoint>[0],
589
+ parsed.method,
590
+ parsed.path,
591
+ specPath
592
+ );
593
+ } else {
594
+ const { api } = await getCachedSpec(specPath, contentDir!);
595
+ openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, specPath);
596
+ }
597
+ lastError = null;
598
+ break;
599
+ } catch (err) {
600
+ lastError = err;
601
+ }
602
+ }
603
+
604
+ if (lastError) {
605
+ throw lastError;
576
606
  }
577
607
 
578
608
  // Generate code examples
579
- const authMethod = config.api?.mdx?.auth?.method as AuthMethod | undefined;
580
- const languages = config.api?.examples?.languages;
581
- openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, languages });
609
+ if (openApiEndpointData) {
610
+ const authMethod = config.api?.mdx?.auth?.method as AuthMethod | undefined;
611
+ const languages = config.api?.examples?.languages;
612
+ openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, languages });
613
+ }
582
614
  } catch (err) {
583
- // Log formatted warning to console (appears in CLI and build logs)
584
- // Check if it's an OpenAPI validation error with our format
585
- const error = err as { type?: string; specPath?: string; message?: string; suggestion?: string };
615
+ const error = err as Partial<OpenApiValidationError>;
586
616
  if (error.type && error.specPath && error.message) {
587
- console.warn(formatOpenApiWarning(error as Parameters<typeof formatOpenApiWarning>[0]));
617
+ console.warn(formatOpenApiWarning(error as OpenApiValidationError));
618
+ openApiError = error.message;
588
619
  } else {
589
620
  console.error(`Failed to parse OpenAPI for ${slug.join('/')}:`, err);
621
+ openApiError = 'Unexpected error loading OpenAPI specification';
590
622
  }
591
623
  }
592
624
  }
@@ -712,6 +744,11 @@ export default async function DocPage({ params }: PageProps) {
712
744
  />
713
745
  )}
714
746
 
747
+ {/* OpenAPI error — shown when spec parsing fails */}
748
+ {!openApiEndpointData && openApiError && (
749
+ <OpenApiError message={openApiError} slug={slug.join('/')} />
750
+ )}
751
+
715
752
  {/* Additional MDX content — strip <ResponseExample> on OpenAPI pages
716
753
  (auto-generated ResponseExamplePanel already handles responses) */}
717
754
  <ImagePriorityProvider>
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Docs Search API — Semantic Search Endpoint
3
+ *
4
+ * REST API for external integrations (Intercom, Zendesk, custom chatbots)
5
+ * to search project documentation via vector similarity.
6
+ *
7
+ * Security:
8
+ * - Opaque API key auth (SHA-256 hash lookup in Upstash Redis; revocation
9
+ * is a Redis DEL, so there is no separate blocklist)
10
+ * - Per-key rate limiting (60 req/min)
11
+ * - CORS enabled (cross-origin access by design)
12
+ *
13
+ * Usage:
14
+ * POST https://acme.jamdesk.app/_api/search
15
+ * Authorization: Bearer jd_live_<32 hex>
16
+ * {"query": "How do I authenticate?", "limit": 5}
17
+ */
18
+ import { NextRequest, NextResponse } from 'next/server';
19
+ import { querySimilarChunks } from '@/lib/vector-store';
20
+ import { verifyApiKey } from '@/lib/docs-search-auth';
21
+ import { getBaseUrl, trackServerAnalytics } from '@/lib/route-helpers';
22
+ import { redis } from '@/lib/redis';
23
+
24
+ export const runtime = 'nodejs';
25
+ export const maxDuration = 30;
26
+
27
+ const CORS_HEADERS = {
28
+ 'Access-Control-Allow-Origin': '*',
29
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
30
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
31
+ };
32
+
33
+ const MAX_LIMIT = 20;
34
+ const DEFAULT_LIMIT = 5;
35
+ const MAX_QUERY_LENGTH = 500;
36
+ const RATE_LIMIT_PER_MIN = 60;
37
+
38
+ export async function OPTIONS(_request: NextRequest) {
39
+ return new NextResponse(null, { status: 204, headers: CORS_HEADERS });
40
+ }
41
+
42
+ export async function POST(
43
+ request: NextRequest,
44
+ context: { params: Promise<{ project: string }> },
45
+ ): Promise<NextResponse> {
46
+ const { project } = await context.params;
47
+
48
+ // --- Auth: verify opaque key against Upstash ---
49
+ // A revoked key is a Redis DEL, so "not found" and "revoked" collapse
50
+ // into the same invalid_key reason — no separate blocklist check.
51
+ // RFC 7235: scheme is case-insensitive. Accept `Bearer`, `bearer`, etc.,
52
+ // and tolerate stray whitespace rather than 401-ing strict clients.
53
+ const token = (request.headers.get('Authorization') || '')
54
+ .replace(/^Bearer\s+/i, '')
55
+ .trim();
56
+ const verify = await verifyApiKey(token, project);
57
+
58
+ if (!verify.ok) {
59
+ const status =
60
+ verify.reason === 'wrong_project' ? 403 :
61
+ verify.reason === 'lookup_failed' ||
62
+ verify.reason === 'redis_unavailable' ? 503 :
63
+ 401;
64
+ return NextResponse.json(
65
+ { error: verify.reason },
66
+ { status, headers: CORS_HEADERS },
67
+ );
68
+ }
69
+
70
+ // --- Rate limiting: per key ID ---
71
+ // Always-call expire (not just on count === 1) — otherwise a transient
72
+ // failure between INCR and EXPIRE leaves the bucket immortal, slowly
73
+ // leaking Upstash keys. EXPIRE is idempotent.
74
+ if (redis) {
75
+ try {
76
+ const rlKey = `docs_search_rl:${verify.id}:${Math.floor(Date.now() / 60000)}`;
77
+ const count = await redis.incr(rlKey);
78
+ await redis.expire(rlKey, 120);
79
+
80
+ if (count > RATE_LIMIT_PER_MIN) {
81
+ return NextResponse.json(
82
+ { error: 'Rate limit exceeded' },
83
+ { status: 429, headers: { ...CORS_HEADERS, 'Retry-After': '60' } },
84
+ );
85
+ }
86
+ } catch {
87
+ // Redis down — allow through
88
+ }
89
+ }
90
+
91
+ // --- Parse & validate request body ---
92
+ let body: { query?: string; limit?: number };
93
+ try {
94
+ body = await request.json();
95
+ } catch {
96
+ return NextResponse.json(
97
+ { error: 'Invalid JSON body' },
98
+ { status: 400, headers: CORS_HEADERS },
99
+ );
100
+ }
101
+
102
+ const { query, limit: rawLimit } = body;
103
+
104
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
105
+ return NextResponse.json(
106
+ { error: 'Missing or empty "query" field' },
107
+ { status: 400, headers: CORS_HEADERS },
108
+ );
109
+ }
110
+
111
+ if (query.length > MAX_QUERY_LENGTH) {
112
+ return NextResponse.json(
113
+ { error: `Query exceeds ${MAX_QUERY_LENGTH} characters` },
114
+ { status: 400, headers: CORS_HEADERS },
115
+ );
116
+ }
117
+
118
+ // Use Number.isFinite so limit=0 clamps to 1 instead of falling to default
119
+ const parsedLimit = Number(rawLimit);
120
+ const effectiveLimit = Number.isFinite(parsedLimit)
121
+ ? parsedLimit
122
+ : DEFAULT_LIMIT;
123
+ const limit = Math.min(
124
+ Math.max(1, Math.floor(effectiveLimit)),
125
+ MAX_LIMIT,
126
+ );
127
+
128
+ // --- Semantic vector search ---
129
+ const startMs = Date.now();
130
+ let chunks;
131
+ try {
132
+ chunks = await querySimilarChunks(project, query.trim(), limit);
133
+ } catch (err) {
134
+ console.error('Vector search failed:', err);
135
+ return NextResponse.json(
136
+ { error: 'Search temporarily unavailable' },
137
+ { status: 502, headers: CORS_HEADERS },
138
+ );
139
+ }
140
+ const durationMs = Date.now() - startMs;
141
+
142
+ const resolvedHost = request.headers.get('x-jamdesk-forwarded-host')
143
+ || request.headers.get('x-original-host') || '';
144
+ const baseUrl = getBaseUrl(project, resolvedHost);
145
+
146
+ const results = chunks.map(chunk => ({
147
+ title: chunk.pageTitle,
148
+ section: chunk.sectionHeading || undefined,
149
+ slug: chunk.pageSlug,
150
+ content: chunk.content.slice(0, 500),
151
+ url: `${baseUrl}/${chunk.pageSlug}`,
152
+ score: Math.round(chunk.score * 1000) / 1000,
153
+ }));
154
+
155
+ // --- Analytics (fire-and-forget) ---
156
+ trackServerAnalytics({
157
+ projectSlug: project,
158
+ type: 'docs_search',
159
+ query: query.trim(),
160
+ resultsCount: results.length,
161
+ source: `key:${verify.id}`,
162
+ });
163
+
164
+ return NextResponse.json(
165
+ { results, query: query.trim(), total: results.length, durationMs },
166
+ { status: 200, headers: CORS_HEADERS },
167
+ );
168
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OpenApiError — visible warning when OpenAPI spec resolution fails.
3
+ * Renders in all environments (dev, ISR, CLI) so users see the problem
4
+ * instead of a silently blank page.
5
+ */
6
+
7
+ interface OpenApiErrorProps {
8
+ message: string;
9
+ slug: string;
10
+ }
11
+
12
+ export function OpenApiError({ message, slug }: OpenApiErrorProps) {
13
+ return (
14
+ <div
15
+ role="alert"
16
+ style={{
17
+ margin: '1.5rem 0',
18
+ padding: '1rem 1.25rem',
19
+ borderRadius: '8px',
20
+ border: '1px solid #f59e0b',
21
+ backgroundColor: 'rgba(245, 158, 11, 0.08)',
22
+ fontSize: '0.875rem',
23
+ lineHeight: 1.5,
24
+ color: 'var(--color-text-primary, #1e293b)',
25
+ }}
26
+ >
27
+ <div style={{ fontWeight: 600, marginBottom: '0.375rem' }}>
28
+ OpenAPI Error
29
+ </div>
30
+ <div style={{ color: 'var(--color-text-muted, #64748b)' }}>
31
+ Failed to load the OpenAPI specification for <code>{slug}</code>: {message}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Docs Search API key verification.
3
+ *
4
+ * Tokens are opaque: `jd_live_<32 hex chars>` (40 chars total, 128 bits
5
+ * of entropy from crypto.randomBytes). The `_live_` segment reserves
6
+ * namespace for a future `jd_test_` mode. The server hashes the token
7
+ * with SHA-256 and looks up `apikey:<hash>` in Upstash Redis — the
8
+ * dashboard function dual-writes this record on generate and deletes
9
+ * it on revoke. No JWT, no signing secret, no expiration.
10
+ */
11
+ import {createHash} from 'crypto';
12
+ import {redis} from './redis';
13
+ import {parseRedisConfig} from './domain-helpers';
14
+
15
+ const KEY_FORMAT = /^jd_live_[0-9a-f]{32}$/;
16
+
17
+ export type VerifyResult =
18
+ | {ok: true; id: string}
19
+ | {ok: false; reason: VerifyFailure};
20
+
21
+ export type VerifyFailure =
22
+ | 'invalid_key_format'
23
+ | 'invalid_key'
24
+ | 'wrong_project'
25
+ | 'lookup_failed'
26
+ | 'redis_unavailable';
27
+
28
+ interface StoredKey {
29
+ projectSlug: string;
30
+ id: string;
31
+ }
32
+
33
+ function hashApiKey(rawKey: string): string {
34
+ return createHash('sha256').update(rawKey).digest('hex');
35
+ }
36
+
37
+ function parseStoredKey(raw: unknown): StoredKey | null {
38
+ let parsed: Record<string, unknown> | null;
39
+ try {
40
+ parsed = parseRedisConfig(raw);
41
+ } catch {
42
+ return null;
43
+ }
44
+ if (
45
+ parsed &&
46
+ typeof parsed.projectSlug === 'string' &&
47
+ typeof parsed.id === 'string'
48
+ ) {
49
+ return parsed as unknown as StoredKey;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Verify a bearer token against the expected project slug.
56
+ *
57
+ * @param rawKey The bearer token exactly as received (no trimming — the
58
+ * caller is responsible for stripping the `Bearer ` prefix).
59
+ * @param projectSlug The slug from the URL path (`[project]` param).
60
+ * @returns `{ok: true, id}` on success, `{ok: false, reason}` otherwise.
61
+ * `id` is the short identifier suitable for audit logging;
62
+ * do NOT log the raw token or the hash.
63
+ */
64
+ export async function verifyApiKey(
65
+ rawKey: string,
66
+ projectSlug: string,
67
+ ): Promise<VerifyResult> {
68
+ if (typeof rawKey !== 'string' || !KEY_FORMAT.test(rawKey)) {
69
+ return {ok: false, reason: 'invalid_key_format'};
70
+ }
71
+
72
+ if (!redis) {
73
+ return {ok: false, reason: 'redis_unavailable'};
74
+ }
75
+
76
+ const hash = hashApiKey(rawKey);
77
+
78
+ let raw: unknown;
79
+ try {
80
+ raw = await redis.get(`apikey:${hash}`);
81
+ } catch {
82
+ return {ok: false, reason: 'lookup_failed'};
83
+ }
84
+
85
+ const stored = parseStoredKey(raw);
86
+ if (!stored) {
87
+ return {ok: false, reason: 'invalid_key'};
88
+ }
89
+
90
+ if (stored.projectSlug !== projectSlug) {
91
+ return {ok: false, reason: 'wrong_project'};
92
+ }
93
+
94
+ return {ok: true, id: stored.id};
95
+ }
@@ -7,6 +7,7 @@
7
7
  import { create, insertMultiple, search as oramaSearch, type Orama } from '@orama/orama';
8
8
  import { restore } from '@orama/plugin-data-persistence';
9
9
  import { getFileBufferFromR2 } from './r2';
10
+ import { querySimilarChunks } from './vector-store';
10
11
 
11
12
  // Types matching builder/build-service/lib/search-client.ts
12
13
  export interface SearchDocument {
@@ -28,6 +29,35 @@ export interface SearchResult {
28
29
  score: number;
29
30
  }
30
31
 
32
+ /**
33
+ * Vector search adapter for MCP. Wraps querySimilarChunks()
34
+ * and maps to SearchResult[] format. Returns null if vector
35
+ * store is not configured (Orama fallback kicks in).
36
+ */
37
+ export async function searchProjectWithVector(
38
+ project: string,
39
+ query: string,
40
+ limit: number,
41
+ ): Promise<SearchResult[] | null> {
42
+ if (!process.env.UPSTASH_VECTOR_REST_URL ||
43
+ !process.env.UPSTASH_VECTOR_REST_TOKEN) {
44
+ return null;
45
+ }
46
+
47
+ try {
48
+ const chunks = await querySimilarChunks(project, query, limit);
49
+ return chunks.map(chunk => ({
50
+ title: chunk.pageTitle,
51
+ url: chunk.pageSlug,
52
+ section: chunk.sectionHeading || undefined,
53
+ type: 'guide',
54
+ score: chunk.score,
55
+ }));
56
+ } catch {
57
+ return null; // Fall back to Orama on error
58
+ }
59
+ }
60
+
31
61
  // Orama schema type
32
62
  type SearchSchema = {
33
63
  id: 'string';
@@ -74,6 +104,15 @@ export async function searchProject(
74
104
  return [];
75
105
  }
76
106
 
107
+ // Try vector search first for unfiltered queries
108
+ if (type === 'all') {
109
+ const vectorResults = await searchProjectWithVector(project, query, limit);
110
+ if (vectorResults) {
111
+ return vectorResults.map(r => ({ ...r, url: `${docsPath}/${r.url}` }));
112
+ }
113
+ }
114
+
115
+ // Fall back to Orama text search
77
116
  const db = await getOrCreateIndex(project, docsPath);
78
117
 
79
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -7,7 +7,6 @@
7
7
 
8
8
  import { log } from './logger';
9
9
  import {
10
- resolveProject,
11
10
  resolveProjectFromHostname,
12
11
  resolveCustomDomain,
13
12
  getProjectConfig,
@@ -315,6 +314,7 @@ export const INTERNAL_API_ROUTES = [
315
314
  '/api/indexnow', // IndexNow key verification (app/api/indexnow/[key])
316
315
  '/api/isr-health', // Health check endpoint (app/api/isr-health)
317
316
  '/api/chat', // Chat endpoint (app/api/chat/[project])
317
+ '/api/docs-search', // Docs Search API (app/api/docs-search/[project]/search)
318
318
  '/api/mcp', // MCP endpoint (app/api/mcp/[project])
319
319
  '/api/og', // OG image generation (app/api/og)
320
320
  '/api/playground', // API playground (token, proxy, demo) — must skip hostAtDocs redirect
@@ -447,6 +447,26 @@ export function getChatApiPath(projectSlug: string): string {
447
447
  return `/api/chat/${projectSlug}`;
448
448
  }
449
449
 
450
+ /**
451
+ * Check if this is a docs search request that needs routing.
452
+ *
453
+ * @param pathname - Request pathname
454
+ * @returns true if this is a docs search request
455
+ */
456
+ export function isDocsSearchRequest(pathname: string): boolean {
457
+ return pathname === '/_api/search' || pathname === '/docs/_api/search';
458
+ }
459
+
460
+ /**
461
+ * Get the docs search API path for a project.
462
+ *
463
+ * @param projectSlug - Project identifier
464
+ * @returns Docs search API route path
465
+ */
466
+ export function getDocsSearchApiPath(projectSlug: string): string {
467
+ return `/api/docs-search/${projectSlug}/search`;
468
+ }
469
+
450
470
  const PLAYGROUND_PREFIX = '/_jd/playground/';
451
471
 
452
472
  /**
@@ -27,7 +27,7 @@ const VALID_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'H
27
27
  * Full format: "/path/to/spec.yaml METHOD /endpoint/path"
28
28
  * Short format (with defaultSpecPath): "METHOD /endpoint/path"
29
29
  */
30
- export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string): ParsedOpenApiFrontmatter {
30
+ export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string | string[]): ParsedOpenApiFrontmatter {
31
31
  const trimmed = value.trim();
32
32
  const parts = trimmed.split(/\s+/);
33
33
 
@@ -38,7 +38,7 @@ export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string)
38
38
 
39
39
  if (isMethodFirst) {
40
40
  // Short format: "METHOD /endpoint/path"
41
- if (!defaultSpecPath) {
41
+ if (!defaultSpecPath || (Array.isArray(defaultSpecPath) && defaultSpecPath.length === 0)) {
42
42
  throw createFrontmatterError(
43
43
  value,
44
44
  'Short format (e.g., "GET /users") requires api.openapi to be configured in docs.json.\n' +
@@ -46,6 +46,8 @@ export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string)
46
46
  );
47
47
  }
48
48
 
49
+ const resolvedDefault = Array.isArray(defaultSpecPath) ? defaultSpecPath[0] : defaultSpecPath;
50
+
49
51
  const [method, ...pathParts] = parts;
50
52
  const endpointPath = pathParts.join(' ');
51
53
 
@@ -58,9 +60,10 @@ export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string)
58
60
  }
59
61
 
60
62
  return {
61
- specPath: defaultSpecPath.startsWith('/') ? defaultSpecPath : `/${defaultSpecPath}`,
63
+ specPath: resolvedDefault.startsWith('/') ? resolvedDefault : `/${resolvedDefault}`,
62
64
  method: method.toUpperCase() as HttpMethod,
63
65
  path: endpointPath,
66
+ isShortFormat: true,
64
67
  };
65
68
  }
66
69
  }
@@ -106,6 +109,7 @@ export function parseOpenApiFrontmatter(value: string, defaultSpecPath?: string)
106
109
  specPath,
107
110
  method: upperMethod,
108
111
  path: endpointPath,
112
+ isShortFormat: false,
109
113
  };
110
114
  }
111
115
 
@@ -164,6 +164,7 @@ export interface ParsedOpenApiFrontmatter {
164
164
  specPath: string;
165
165
  method: HttpMethod;
166
166
  path: string;
167
+ isShortFormat: boolean;
167
168
  }
168
169
 
169
170
  /**
@@ -2980,9 +2980,9 @@
2980
2980
  "license": "MIT"
2981
2981
  },
2982
2982
  "node_modules/electron-to-chromium": {
2983
- "version": "1.5.336",
2984
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
2985
- "integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
2983
+ "version": "1.5.338",
2984
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz",
2985
+ "integrity": "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg==",
2986
2986
  "license": "ISC"
2987
2987
  },
2988
2988
  "node_modules/enhanced-resolve": {
@@ -5586,9 +5586,9 @@
5586
5586
  }
5587
5587
  },
5588
5588
  "node_modules/postcss": {
5589
- "version": "8.5.9",
5590
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
5591
- "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
5589
+ "version": "8.5.10",
5590
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
5591
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
5592
5592
  "funding": [
5593
5593
  {
5594
5594
  "type": "opencollective",