Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
NEXT_PUBLIC_ENV=development/production

# Koala Analytics
NEXT_PUBLIC_KOALA_PUBLIC_API_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_KOALA_PUBLIC_API_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxx

# Tavily AI Search API Key (get from https://tavily.com)
TAVILY_API_KEY=tvly-XXXXXXXXXX
1,150 changes: 209 additions & 941 deletions .source/index.ts

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .source/source.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// source.config.ts
import {
defineConfig,
defineDocs,
frontmatterSchema,
metaSchema
} from "fumadocs-mdx/config";
import { remarkAdmonition } from "fumadocs-core/mdx-plugins";
var docs = defineDocs({
// The root directory for all documentation
dir: "content/docs",
docs: {
schema: frontmatterSchema
},
meta: {
schema: metaSchema
}
});
var releaseNotes = defineDocs({
// The root directory for release notes
dir: "content/release-notes",
docs: {
schema: frontmatterSchema
},
meta: {
schema: metaSchema
}
});
var source_config_default = defineConfig({
mdxOptions: {
remarkPlugins: [remarkAdmonition],
rehypePlugins: []
}
});
export {
source_config_default as default,
docs,
releaseNotes
};
10 changes: 8 additions & 2 deletions app/(docs)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { notFound } from 'next/navigation';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { getMDXComponents } from '@/mdx-components';
import { EditOnGitHub } from './page.client';
import { CopyPageDropdown } from '@/components/CopyPageDropdown';

const owner = 'parseablehq';
const repo = 'developer-hub';
Expand All @@ -33,11 +34,16 @@ export default async function Page(props: {
const MDXContent = page.data.body;

return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsPage
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
header: <CopyPageDropdown slug={params.slug} filePath={page.file.path} />,
}}
>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center mb-4">
{/* <LLMCopyButton slug={params.slug} /> */}
<EditOnGitHub
url={`https://github.com/${owner}/${repo}/tree/main/${path}`}
/>
Expand Down
10 changes: 7 additions & 3 deletions app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import type { ReactNode } from 'react';
import { baseOptions } from '@/app/layout.config';
import { source } from '@/lib/source';
import SearchButton from '@/components/SearchButton';

export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout
tree={source.pageTree}
{...baseOptions}
// sidebar={{
// enabled: false
// }}
searchToggle={{
components: {
sm: <SearchButton />,
lg: <SearchButton />,
},
}}
>
{children}
</DocsLayout>
Expand Down
71 changes: 71 additions & 0 deletions app/api/ai-search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
try {
const { query } = await request.json();

if (!query || typeof query !== 'string') {
return NextResponse.json(
{ error: 'Query is required' },
{ status: 400 }
);
}

const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: 'Tavily API key not configured' },
{ status: 500 }
);
}

// Search specifically within Parseable documentation
const response = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: apiKey,
query: `${query} site:parseable.com OR site:parseable.io`,
search_depth: 'advanced',
include_answer: true,
include_raw_content: false,
max_results: 5,
include_domains: ['parseable.com', 'parseable.io', 'docs.parseable.com'],
}),
});

if (!response.ok) {
const errorText = await response.text();
console.error('Tavily API error:', errorText);
return NextResponse.json(
{ error: 'Failed to fetch AI search results' },
{ status: response.status }
);
}

const data = await response.json();

return NextResponse.json({
answer: data.answer || null,
results: data.results?.map((result: {
title: string;
url: string;
content: string;
score?: number;
}) => ({
title: result.title,
url: result.url,
content: result.content,
score: result.score,
})) || [],
});
} catch (error) {
console.error('AI search error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
210 changes: 210 additions & 0 deletions app/api/execute-curl/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { NextRequest, NextResponse } from 'next/server';
import { execFile } from 'child_process';
import { promisify } from 'util';

const execFileAsync = promisify(execFile);

// Allowed curl options (whitelist approach)
const ALLOWED_OPTIONS = new Set([
// Request methods
'-X', '--request',
// Headers
'-H', '--header',
// Data
'-d', '--data', '--data-raw', '--data-binary', '--data-urlencode',
// Auth
'-u', '--user',
// URL handling
'-L', '--location',
// Verbose/silent
'-v', '--verbose', '-s', '--silent', '-S', '--show-error',
// Timeout
'--connect-timeout', '-m', '--max-time',
// SSL
'-k', '--insecure',
// Content type shortcuts
'--json',
// Include headers in output
'-i', '--include',
]);

// Options that write to files (blocked)
const BLOCKED_OPTIONS = new Set([
'-o', '--output',
'-O', '--remote-name',
'-T', '--upload-file',
'--create-dirs',
'-K', '--config',
'--trace', '--trace-ascii',
'-D', '--dump-header',
'-c', '--cookie-jar',
'-b', '--cookie',
]);

// Parse curl command into arguments array safely
function parseCurlCommand(command: string): string[] | null {
const args: string[] = [];
let current = '';
let inSingleQuote = false;
let inDoubleQuote = false;
let escaped = false;

// Remove 'curl' prefix and trim
const trimmed = command.trim();
if (!trimmed.toLowerCase().startsWith('curl ')) {
return null;
}
const argsString = trimmed.slice(5).trim();

for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];

if (escaped) {
// Handle escaped characters
if (char === 'n') current += '\n';
else if (char === 't') current += '\t';
else if (char === 'r') current += '\r';
else current += char;
escaped = false;
continue;
}

if (char === '\\' && !inSingleQuote) {
// Check if it's a line continuation (backslash followed by newline or at end)
if (i + 1 < argsString.length && (argsString[i + 1] === '\n' || argsString[i + 1] === '\r')) {
i++; // Skip the newline
if (argsString[i] === '\r' && argsString[i + 1] === '\n') i++; // Handle CRLF
continue;
}
escaped = true;
continue;
}

if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}

if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
}

if ((char === ' ' || char === '\n' || char === '\t') && !inSingleQuote && !inDoubleQuote) {
if (current) {
args.push(current);
current = '';
}
continue;
}

current += char;
}

if (current) {
args.push(current);
}

// Check for unclosed quotes
if (inSingleQuote || inDoubleQuote) {
return null;
}

return args;
}

// Validate parsed arguments
function validateArgs(args: string[]): { valid: boolean; error?: string } {
for (let i = 0; i < args.length; i++) {
const arg = args[i];

// Check for shell injection attempts in any argument
if (/[;&|`$()]/.test(arg) && !arg.startsWith('http')) {
// Allow these chars in URLs and JSON data
const isUrl = arg.startsWith('http://') || arg.startsWith('https://');
const isData = i > 0 && ['-d', '--data', '--data-raw', '--data-binary', '--json'].includes(args[i - 1]);
if (!isUrl && !isData) {
return { valid: false, error: 'Invalid characters in argument' };
}
}

// Check options
if (arg.startsWith('-')) {
// Handle combined short options like -sSL
if (arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
// Split combined options
for (let j = 1; j < arg.length; j++) {
const opt = `-${arg[j]}`;
if (BLOCKED_OPTIONS.has(opt)) {
return { valid: false, error: `Option ${opt} is not allowed` };
}
}
} else {
if (BLOCKED_OPTIONS.has(arg)) {
return { valid: false, error: `Option ${arg} is not allowed` };
}
}
}

// Block file:// and other dangerous protocols
if (arg.match(/^(file|ftp|sftp|scp|dict|gopher|ldap|telnet):\/\//i)) {
return { valid: false, error: 'Only http and https protocols are allowed' };
}

// Block localhost metadata endpoints (cloud provider metadata)
if (arg.includes('169.254.169.254') || arg.includes('metadata.google')) {
return { valid: false, error: 'Access to metadata endpoints is not allowed' };
}
}

return { valid: true };
}

export async function POST(request: NextRequest) {
try {
const { command } = await request.json();

if (!command || typeof command !== 'string') {
return NextResponse.json(
{ error: 'Command is required' },
{ status: 400 }
);
}

// Parse the curl command into arguments
const args = parseCurlCommand(command);
if (!args) {
return NextResponse.json(
{ error: 'Invalid curl command syntax' },
{ status: 400 }
);
}

// Validate arguments
const validation = validateArgs(args);
if (!validation.valid) {
return NextResponse.json(
{ error: validation.error },
{ status: 400 }
);
}

// Execute curl with parsed arguments (no shell interpretation)
const { stdout, stderr } = await execFileAsync('curl', args, {
timeout: 30000, // 30 second timeout
maxBuffer: 1024 * 1024, // 1MB max output
});

return NextResponse.json({
success: true,
output: stdout || stderr,
});
} catch (error: any) {
console.error('Curl execution error:', error);
return NextResponse.json({
success: false,
error: error.message || 'Command execution failed',
output: error.stderr || error.stdout || '',
});
}
}
6 changes: 3 additions & 3 deletions app/layout.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export const baseOptions: BaseLayoutProps = {
},
links: [
{
text: "Parseable Playground",
url: "https://demo.parseable.com/login?q=eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9",
external: true,
text: "Benchmarks",
url: "/docs/benchmarks",
external: false,
icon: <IconMonkeybar />, // Icon for the link
},
{
Expand Down
Loading