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.
- package/dist/lib/openapi/types.d.ts +1 -0
- package/dist/lib/openapi/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/vendored/app/[[...slug]]/page.tsx +66 -29
- package/vendored/app/api/docs-search/[project]/search/route.ts +168 -0
- package/vendored/components/openapi/OpenApiError.tsx +35 -0
- package/vendored/lib/docs-search-auth.ts +95 -0
- package/vendored/lib/mcp-search.ts +39 -0
- package/vendored/lib/middleware-helpers.ts +21 -1
- package/vendored/lib/openapi/parser.ts +7 -3
- package/vendored/lib/openapi/types.ts +1 -0
- package/vendored/workspace-package-lock.json +6 -6
|
@@ -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.
|
|
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
|
-
//
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
|
@@ -2980,9 +2980,9 @@
|
|
|
2980
2980
|
"license": "MIT"
|
|
2981
2981
|
},
|
|
2982
2982
|
"node_modules/electron-to-chromium": {
|
|
2983
|
-
"version": "1.5.
|
|
2984
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2985
|
-
"integrity": "sha512-
|
|
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.
|
|
5590
|
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.
|
|
5591
|
-
"integrity": "sha512-
|
|
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",
|