mirror of https://github.com/buster-so/buster.git
Enhance CLI functionality and SDK integration
- Added new CLI commands for authentication management and improved default action handling. - Updated package.json to include new dependencies and scripts for building and running the CLI. - Integrated authentication features into the SDK, allowing for API key validation. - Modified the SDK configuration to use a new default API URL. - Expanded the server API to include authentication routes. - Updated database queries to support new API key functionalities. - Improved code structure and readability across CLI and SDK components.
This commit is contained in:
parent
3b585d09dd
commit
5726b01ecc
|
@ -23,25 +23,31 @@
|
|||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@buster/sdk": "workspace:*",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"ink": "^5.0.1",
|
||||
"ink-big-text": "^2.0.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^18.3.1",
|
||||
"zod": "^3.24.1",
|
||||
"chalk": "^5.3.0"
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@buster/env-utils": "workspace:*",
|
||||
"@buster/typescript-config": "workspace:*",
|
||||
"@buster/vitest-config": "workspace:*",
|
||||
"@buster/env-utils": "workspace:*",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^18.3.17",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"typescript": "^5.7.3",
|
||||
"tsx": "^4.19.2",
|
||||
"vitest": "^2.1.8",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
"ink-testing-library": "^4.0.0"
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"react-devtools-core": "^6.1.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, Box, useInput, useApp } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import Spinner from 'ink-spinner';
|
||||
import { createBusterSDK } from '@buster/sdk';
|
||||
import {
|
||||
saveCredentials,
|
||||
loadCredentials,
|
||||
deleteCredentials,
|
||||
getCredentials,
|
||||
type Credentials,
|
||||
} from '../utils/credentials.js';
|
||||
|
||||
interface AuthProps {
|
||||
apiKey?: string;
|
||||
host?: string;
|
||||
local?: boolean;
|
||||
cloud?: boolean;
|
||||
clear?: boolean;
|
||||
noSave?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = 'https://api2.buster.so';
|
||||
const LOCAL_HOST = 'http://localhost:3001';
|
||||
|
||||
export function Auth({ apiKey, host, local, cloud, clear, noSave }: AuthProps) {
|
||||
const { exit } = useApp();
|
||||
const [step, setStep] = useState<'clear' | 'prompt' | 'validate' | 'save' | 'done'>('clear');
|
||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||
const [hostInput, setHostInput] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [existingCreds, setExistingCreds] = useState<Credentials | null>(null);
|
||||
const [finalCreds, setFinalCreds] = useState<Credentials | null>(null);
|
||||
|
||||
// Handle clear flag
|
||||
useEffect(() => {
|
||||
if (clear) {
|
||||
deleteCredentials()
|
||||
.then(() => {
|
||||
console.log('✅ Credentials cleared successfully');
|
||||
exit();
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error('❌ Failed to clear credentials:', err.message);
|
||||
exit();
|
||||
});
|
||||
} else {
|
||||
setStep('prompt');
|
||||
}
|
||||
}, [clear, exit]);
|
||||
|
||||
// Load existing credentials
|
||||
useEffect(() => {
|
||||
if (step === 'prompt') {
|
||||
loadCredentials().then(creds => {
|
||||
setExistingCreds(creds);
|
||||
|
||||
// Determine the host to use
|
||||
let targetHost = DEFAULT_HOST;
|
||||
if (local) targetHost = LOCAL_HOST;
|
||||
else if (cloud) targetHost = DEFAULT_HOST;
|
||||
else if (host) targetHost = host;
|
||||
else if (creds?.apiUrl) targetHost = creds.apiUrl;
|
||||
|
||||
setHostInput(targetHost);
|
||||
setApiKeyInput(apiKey || '');
|
||||
|
||||
// If we have all required info from args, skip to validation
|
||||
if (apiKey) {
|
||||
setFinalCreds({ apiKey, apiUrl: targetHost });
|
||||
setStep('validate');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [step, apiKey, host, local, cloud]);
|
||||
|
||||
// Handle input completion
|
||||
useInput((input, key) => {
|
||||
if (key.return && step === 'prompt') {
|
||||
if (!hostInput) {
|
||||
setError('Host URL is required');
|
||||
return;
|
||||
}
|
||||
if (!apiKeyInput) {
|
||||
setError('API key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalCreds({ apiKey: apiKeyInput, apiUrl: hostInput });
|
||||
setStep('validate');
|
||||
}
|
||||
});
|
||||
|
||||
// Validate credentials
|
||||
useEffect(() => {
|
||||
if (step === 'validate' && finalCreds) {
|
||||
const sdk = createBusterSDK({
|
||||
apiKey: finalCreds.apiKey,
|
||||
apiUrl: finalCreds.apiUrl,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
sdk.auth.isApiKeyValid()
|
||||
.then((valid: boolean) => {
|
||||
if (valid) {
|
||||
setStep(noSave ? 'done' : 'save');
|
||||
} else {
|
||||
setError('Invalid API key');
|
||||
setStep('prompt');
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
setError(`Validation failed: ${err.message}`);
|
||||
setStep('prompt');
|
||||
});
|
||||
}
|
||||
}, [step, finalCreds, noSave]);
|
||||
|
||||
// Save credentials
|
||||
useEffect(() => {
|
||||
if (step === 'save' && finalCreds) {
|
||||
saveCredentials(finalCreds)
|
||||
.then(() => {
|
||||
setStep('done');
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
setError(`Failed to save: ${err.message}`);
|
||||
setStep('done');
|
||||
});
|
||||
}
|
||||
}, [step, finalCreds]);
|
||||
|
||||
// Display success and exit
|
||||
useEffect(() => {
|
||||
if (step === 'done' && finalCreds) {
|
||||
const masked = finalCreds.apiKey.length > 6
|
||||
? `****${finalCreds.apiKey.slice(-6)}`
|
||||
: '****';
|
||||
|
||||
console.log('\n✅ You\'ve successfully connected to Buster!\n');
|
||||
console.log('Connection details:');
|
||||
console.log(` host: ${finalCreds.apiUrl}`);
|
||||
console.log(` api_key: ${masked}`);
|
||||
|
||||
if (!noSave && step === 'done') {
|
||||
console.log('\nCredentials saved successfully!');
|
||||
} else if (noSave) {
|
||||
console.log('\nNote: Credentials were not saved due to --no-save flag');
|
||||
}
|
||||
|
||||
exit();
|
||||
}
|
||||
}, [step, finalCreds, noSave, exit]);
|
||||
|
||||
// Render based on current step
|
||||
if (step === 'clear') {
|
||||
return <Text>Clearing credentials...</Text>;
|
||||
}
|
||||
|
||||
if (step === 'prompt') {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{existingCreds && (
|
||||
<Text color="yellow">
|
||||
⚠️ Existing credentials found. They will be overwritten.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text color="red">❌ {error}</Text>
|
||||
)}
|
||||
|
||||
<Box marginY={1}>
|
||||
<Text>Enter the URL of your Buster API (default: {DEFAULT_HOST}): </Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={hostInput}
|
||||
onChange={setHostInput}
|
||||
placeholder={DEFAULT_HOST}
|
||||
/>
|
||||
|
||||
<Box marginY={1}>
|
||||
<Text>Enter your API key: </Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={apiKeyInput}
|
||||
onChange={setApiKeyInput}
|
||||
mask="*"
|
||||
/>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Find your API key at {hostInput || DEFAULT_HOST}/app/settings/api-keys
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter when ready to continue</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'validate') {
|
||||
return (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Validating API key...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'save') {
|
||||
return (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Saving credentials...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, Box, useInput, useApp } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import Spinner from 'ink-spinner';
|
||||
import BigText from 'ink-big-text';
|
||||
import { createBusterSDK } from '@buster/sdk';
|
||||
import {
|
||||
saveCredentials,
|
||||
hasCredentials,
|
||||
type Credentials,
|
||||
} from '../utils/credentials.js';
|
||||
|
||||
interface InitProps {
|
||||
apiKey?: string;
|
||||
host?: string;
|
||||
local?: boolean;
|
||||
skipBanner?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = 'https://api2.buster.so';
|
||||
const LOCAL_HOST = 'http://localhost:3001';
|
||||
|
||||
// Component for the welcome screen
|
||||
function WelcomeScreen() {
|
||||
return (
|
||||
<Box paddingY={2} paddingX={2} alignItems="center">
|
||||
<Box marginRight={4}>
|
||||
<Text color="#7C3AED">
|
||||
<BigText text="BUSTER" font="block" />
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" justifyContent="center">
|
||||
<Text bold>Welcome to Buster</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Type / to use slash commands</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Type @ to mention files</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Ctrl-C to exit</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text dimColor>/help for more</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text color="#7C3AED">"Run `buster` and fix all the errors"</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Init({ apiKey, host, local, skipBanner }: InitProps) {
|
||||
const { exit } = useApp();
|
||||
const [step, setStep] = useState<'check' | 'prompt' | 'validate' | 'save' | 'done'>('check');
|
||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||
const [hostInput, setHostInput] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [finalCreds, setFinalCreds] = useState<Credentials | null>(null);
|
||||
const [showBanner] = useState(!skipBanner);
|
||||
|
||||
// Check for existing credentials
|
||||
useEffect(() => {
|
||||
if (step === 'check') {
|
||||
hasCredentials().then(hasCreds => {
|
||||
if (hasCreds) {
|
||||
console.log('\n✅ You already have Buster configured!');
|
||||
console.log('\nTo reconfigure, run: buster auth');
|
||||
exit();
|
||||
} else {
|
||||
// Set default host based on flags
|
||||
let targetHost = DEFAULT_HOST;
|
||||
if (local) targetHost = LOCAL_HOST;
|
||||
else if (host) targetHost = host;
|
||||
|
||||
setHostInput(targetHost);
|
||||
setApiKeyInput(apiKey || '');
|
||||
|
||||
// If API key provided via args, skip to validation
|
||||
if (apiKey) {
|
||||
setFinalCreds({ apiKey, apiUrl: targetHost });
|
||||
setStep('validate');
|
||||
} else {
|
||||
setStep('prompt');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [step, apiKey, host, local, exit]);
|
||||
|
||||
// Handle input
|
||||
useInput((input, key) => {
|
||||
if (key.return && step === 'prompt' && apiKeyInput) {
|
||||
setFinalCreds({
|
||||
apiKey: apiKeyInput,
|
||||
apiUrl: hostInput || DEFAULT_HOST
|
||||
});
|
||||
setStep('validate');
|
||||
}
|
||||
});
|
||||
|
||||
// Validate API key
|
||||
useEffect(() => {
|
||||
if (step === 'validate' && finalCreds) {
|
||||
const sdk = createBusterSDK({
|
||||
apiKey: finalCreds.apiKey,
|
||||
apiUrl: finalCreds.apiUrl,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
sdk.auth.isApiKeyValid()
|
||||
.then((valid: boolean) => {
|
||||
if (valid) {
|
||||
setStep('save');
|
||||
} else {
|
||||
setError('Invalid API key. Please check your key and try again.');
|
||||
setStep('prompt');
|
||||
setApiKeyInput('');
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
setError(`Connection failed: ${err.message}`);
|
||||
setStep('prompt');
|
||||
});
|
||||
}
|
||||
}, [step, finalCreds]);
|
||||
|
||||
// Save credentials
|
||||
useEffect(() => {
|
||||
if (step === 'save' && finalCreds) {
|
||||
saveCredentials(finalCreds)
|
||||
.then(() => {
|
||||
setStep('done');
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error('Failed to save credentials:', err.message);
|
||||
setStep('done');
|
||||
});
|
||||
}
|
||||
}, [step, finalCreds]);
|
||||
|
||||
// Show success message and exit
|
||||
useEffect(() => {
|
||||
if (step === 'done' && finalCreds) {
|
||||
const masked = finalCreds.apiKey.length > 6
|
||||
? `****${finalCreds.apiKey.slice(-6)}`
|
||||
: '****';
|
||||
|
||||
console.log('\n🎉 Welcome to Buster!\n');
|
||||
console.log('✅ You\'ve successfully connected to Buster!\n');
|
||||
console.log('Connection details:');
|
||||
console.log(` host: ${finalCreds.apiUrl}`);
|
||||
console.log(` api_key: ${masked}`);
|
||||
console.log('\nYour credentials have been saved.');
|
||||
console.log('\n📚 Get started:');
|
||||
console.log(' buster --help Show available commands');
|
||||
console.log(' buster auth Reconfigure authentication');
|
||||
|
||||
exit();
|
||||
}
|
||||
}, [step, finalCreds, exit]);
|
||||
|
||||
// Render based on step - always show welcome screen at the top if enabled
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{showBanner && <WelcomeScreen />}
|
||||
|
||||
{step === 'check' && (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Checking configuration...
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{step === 'prompt' && (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text>Let's get you connected to Buster.</Text>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="red">❌ {error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!apiKey && !host && !local && (
|
||||
<>
|
||||
<Box marginBottom={1}>
|
||||
<Text>API URL: {hostInput}</Text>
|
||||
<Text dimColor> (Press Enter to use default)</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text>Enter your API key: </Text>
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
value={apiKeyInput}
|
||||
onChange={setApiKeyInput}
|
||||
mask="*"
|
||||
placeholder="sk_..."
|
||||
/>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Find your API key at {hostInput}/app/settings/api-keys
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter to continue</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{step === 'validate' && (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Validating your API key...
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{step === 'save' && (
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Saving your configuration...
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -43,7 +43,7 @@ export const InteractiveCommand: React.FC = () => {
|
|||
<Box marginTop={1} flexDirection="column">
|
||||
{options.map((option, index) => (
|
||||
<Box key={option}>
|
||||
<Text color={selectedOption === index ? 'green' : undefined}>
|
||||
<Text {...(selectedOption === index ? { color: 'green' } : {})}>
|
||||
{selectedOption === index ? chalk.bold('▶ ') : ' '}
|
||||
{option}
|
||||
</Text>
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, Box, useInput, useApp } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import Spinner from 'ink-spinner';
|
||||
import BigText from 'ink-big-text';
|
||||
import { createBusterSDK } from '@buster/sdk';
|
||||
import {
|
||||
saveCredentials,
|
||||
getCredentials,
|
||||
hasCredentials,
|
||||
type Credentials,
|
||||
} from '../utils/credentials.js';
|
||||
|
||||
const DEFAULT_HOST = 'https://api2.buster.so';
|
||||
const LOCAL_HOST = 'http://localhost:3001';
|
||||
|
||||
// Component for the welcome screen header
|
||||
function WelcomeHeader() {
|
||||
return (
|
||||
<Box paddingY={2} paddingX={2} alignItems="center">
|
||||
<Box marginRight={4}>
|
||||
<Text color="#7C3AED">
|
||||
<BigText text="BUSTER" font="block" />
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" justifyContent="center">
|
||||
<Text bold>Welcome to Buster</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Type / to use slash commands</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Type @ to mention files</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Ctrl-C to exit</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text dimColor>/help for more</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text color="#7C3AED">"Run `buster` and fix all the errors"</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Input box component for authenticated users
|
||||
function CommandInput({ onSubmit }: { onSubmit: (input: string) => void }) {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (input.trim()) {
|
||||
onSubmit(input);
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box paddingX={2} paddingBottom={1}>
|
||||
<Box borderStyle="single" borderColor="#7C3AED" paddingX={1} width="100%">
|
||||
<Text color="#7C3AED">❯ </Text>
|
||||
<TextInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="Enter a command or question..."
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Auth prompt component for unauthenticated users
|
||||
function AuthPrompt({ onAuth }: { onAuth: (creds: Credentials) => void }) {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [host] = useState(DEFAULT_HOST);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
setError('API key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const sdk = createBusterSDK({
|
||||
apiKey: apiKey.trim(),
|
||||
apiUrl: host,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const isValid = await sdk.auth.isApiKeyValid();
|
||||
|
||||
if (isValid) {
|
||||
const creds = { apiKey: apiKey.trim(), apiUrl: host };
|
||||
await saveCredentials(creds);
|
||||
onAuth(creds);
|
||||
} else {
|
||||
setError('Invalid API key. Please check your key and try again.');
|
||||
setApiKey('');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Connection failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (validating) {
|
||||
return (
|
||||
<Box paddingX={2}>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Validating your API key...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={2}>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Let's get you connected to Buster.</Text>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="red">❌ {error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text>Enter your API key: </Text>
|
||||
</Box>
|
||||
|
||||
<Box borderStyle="single" borderColor="#7C3AED" paddingX={1}>
|
||||
<TextInput
|
||||
value={apiKey}
|
||||
onChange={setApiKey}
|
||||
onSubmit={handleSubmit}
|
||||
mask="*"
|
||||
placeholder="sk_..."
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Find your API key at {host}/app/settings/api-keys
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter to continue</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Main() {
|
||||
const { exit } = useApp();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const [credentials, setCredentials] = useState<Credentials | null>(null);
|
||||
const [checkingAuth, setCheckingAuth] = useState(true);
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||
|
||||
// Check authentication status on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const creds = await getCredentials();
|
||||
|
||||
if (creds && creds.apiKey) {
|
||||
// Validate the stored credentials
|
||||
const sdk = createBusterSDK({
|
||||
apiKey: creds.apiKey,
|
||||
apiUrl: creds.apiUrl,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const isValid = await sdk.auth.isApiKeyValid();
|
||||
|
||||
if (isValid) {
|
||||
setCredentials(creds);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
setCheckingAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input === 'c') {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
|
||||
const handleAuth = (creds: Credentials) => {
|
||||
setCredentials(creds);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
setCommandHistory([...commandHistory, command]);
|
||||
|
||||
// Handle special commands
|
||||
if (command === '/help') {
|
||||
console.info('\nAvailable commands:');
|
||||
console.info(' /help - Show this help message');
|
||||
console.info(' /clear - Clear the screen');
|
||||
console.info(' /exit - Exit the CLI');
|
||||
console.info('\nFor more information, visit https://docs.buster.so');
|
||||
} else if (command === '/clear') {
|
||||
console.clear();
|
||||
} else if (command === '/exit') {
|
||||
exit();
|
||||
} else {
|
||||
// TODO: Process the command with Buster
|
||||
console.info(`Processing: ${command}`);
|
||||
console.info('Command processing not yet implemented.');
|
||||
}
|
||||
};
|
||||
|
||||
if (checkingAuth) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<WelcomeHeader />
|
||||
<Box paddingX={2}>
|
||||
<Text>
|
||||
<Spinner type="dots" />
|
||||
{' '}Checking configuration...
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<WelcomeHeader />
|
||||
|
||||
{/* Show command history if authenticated */}
|
||||
{isAuthenticated && commandHistory.length > 0 && (
|
||||
<Box flexDirection="column" paddingX={2} marginBottom={1}>
|
||||
{commandHistory.slice(-5).map((cmd, idx) => (
|
||||
<Box key={idx}>
|
||||
<Text dimColor>❯ {cmd}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isAuthenticated ? (
|
||||
<CommandInput onSubmit={handleCommand} />
|
||||
) : (
|
||||
<AuthPrompt onAuth={handleAuth} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Text, Box, useApp } from 'ink';
|
||||
import { AnimatedLogo } from '../components/animated-logo.js';
|
||||
|
||||
export function Welcome() {
|
||||
const { exit } = useApp();
|
||||
|
||||
// Auto-exit after a few seconds
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
exit();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [exit]);
|
||||
|
||||
return (
|
||||
<Box paddingY={2} paddingX={2}>
|
||||
<Box marginRight={4}>
|
||||
<AnimatedLogo color="#7C3AED" />
|
||||
</Box>
|
||||
<Box flexDirection="column" justifyContent="center">
|
||||
<Text bold>Welcome to Buster</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Type / to use slash commands</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Type @ to mention files</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Ctrl-C to exit</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text dimColor>/help for more</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text color="#7C3AED">"Run `buster` and fix all the errors"</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
|
||||
// ASCII art for the Buster "b" logo - compact version
|
||||
const BUSTER_LOGO_FRAMES = [
|
||||
// Frame 1 - dimmer
|
||||
`
|
||||
.:::::.
|
||||
.=++++++=.
|
||||
:+++++++++=:
|
||||
.+++=:.:=+++:.
|
||||
:+++. .+++:
|
||||
:+++=====:.
|
||||
:+++. .:==:
|
||||
:+++: :+++:
|
||||
.+++=:.:=+++=.
|
||||
:+++++++++=:
|
||||
.=++++++=.
|
||||
.:::::.
|
||||
`,
|
||||
// Frame 2 - medium brightness
|
||||
`
|
||||
.=====.
|
||||
.=#####+=.
|
||||
:#########+:
|
||||
.###=:.:=###:.
|
||||
:###. .###:
|
||||
:###=====:.
|
||||
:###. .:==:
|
||||
:###: :###:
|
||||
.###=:.:=###+.
|
||||
:#########+:
|
||||
.=#####+=.
|
||||
.=====.
|
||||
`,
|
||||
// Frame 3 - brightest
|
||||
`
|
||||
.=@@@=.
|
||||
.=@@@@@+=.
|
||||
:@@@@@@@@@+:
|
||||
.@@@=:.:=@@@:.
|
||||
:@@@. .@@@:
|
||||
:@@@=====:.
|
||||
:@@@. .:==:
|
||||
:@@@: :@@@:
|
||||
.@@@=:.:=@@@+.
|
||||
:@@@@@@@@@+:
|
||||
.=@@@@@+=.
|
||||
.=@@@=.
|
||||
`
|
||||
];
|
||||
|
||||
interface AnimatedLogoProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function AnimatedLogo({ color = '#7C3AED' }: AnimatedLogoProps) {
|
||||
const [frame, setFrame] = useState(0);
|
||||
const [opacity, setOpacity] = useState(0);
|
||||
const [growing, setGrowing] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (growing) {
|
||||
if (opacity < 2) {
|
||||
setOpacity(opacity + 1);
|
||||
} else {
|
||||
setGrowing(false);
|
||||
}
|
||||
} else {
|
||||
if (opacity > 0) {
|
||||
setOpacity(opacity - 1);
|
||||
} else {
|
||||
setGrowing(true);
|
||||
}
|
||||
}
|
||||
setFrame((prev) => (prev + 1) % 3);
|
||||
}, 200);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [opacity, growing]);
|
||||
|
||||
// Select the appropriate frame based on opacity
|
||||
const currentFrame = BUSTER_LOGO_FRAMES[opacity];
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="center">
|
||||
<Text color={color}>{currentFrame}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AnimatedLogo } from './animated-logo.js';
|
|
@ -2,14 +2,34 @@
|
|||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { program } from 'commander';
|
||||
import { Main } from './commands/main.js';
|
||||
import { Auth } from './commands/auth.js';
|
||||
import { HelloCommand } from './commands/hello.js';
|
||||
import { InteractiveCommand } from './commands/interactive.js';
|
||||
|
||||
// CLI metadata
|
||||
program
|
||||
.name('buster')
|
||||
.description('Buster CLI - TypeScript version')
|
||||
.version('0.1.0');
|
||||
.description('Buster CLI - AI-powered data analytics platform')
|
||||
.version('0.1.0')
|
||||
.action(() => {
|
||||
// Default action when no subcommand is provided
|
||||
render(<Main />);
|
||||
});
|
||||
|
||||
// Auth command - authentication management
|
||||
program
|
||||
.command('auth')
|
||||
.description('Authenticate with Buster API')
|
||||
.option('--api-key <key>', 'Your Buster API key')
|
||||
.option('--host <url>', 'Custom API host URL')
|
||||
.option('--local', 'Use local development server (http://localhost:3001)')
|
||||
.option('--cloud', 'Use cloud instance (https://api2.buster.so)')
|
||||
.option('--clear', 'Clear saved credentials')
|
||||
.option('--no-save', "Don't save credentials to disk")
|
||||
.action(async (options) => {
|
||||
render(<Auth {...options} />);
|
||||
});
|
||||
|
||||
// Hello command - basic example
|
||||
program
|
||||
|
@ -18,7 +38,7 @@ program
|
|||
.argument('[name]', 'Name to greet', 'World')
|
||||
.option('-u, --uppercase', 'Output in uppercase')
|
||||
.action(async (name: string, options: { uppercase?: boolean }) => {
|
||||
render(<HelloCommand name={name} uppercase={options.uppercase} />);
|
||||
render(<HelloCommand name={name} uppercase={options.uppercase || false} />);
|
||||
});
|
||||
|
||||
// Interactive command - demonstrates Ink's capabilities
|
||||
|
@ -30,9 +50,4 @@ program
|
|||
});
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse(process.argv);
|
||||
|
||||
// Show help if no command provided
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
}
|
||||
program.parse(process.argv);
|
|
@ -0,0 +1,124 @@
|
|||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Credentials schema
|
||||
const credentialsSchema = z.object({
|
||||
apiKey: z.string().min(1),
|
||||
apiUrl: z.string().url(),
|
||||
});
|
||||
|
||||
export type Credentials = z.infer<typeof credentialsSchema>;
|
||||
|
||||
// Default configuration
|
||||
const DEFAULT_API_URL = 'https://api2.buster.so';
|
||||
const CREDENTIALS_DIR = join(homedir(), '.buster');
|
||||
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
|
||||
|
||||
/**
|
||||
* Ensures the credentials directory exists
|
||||
*/
|
||||
async function ensureCredentialsDir(): Promise<void> {
|
||||
try {
|
||||
await mkdir(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
||||
} catch (error) {
|
||||
console.error('Failed to create credentials directory:', error);
|
||||
throw new Error('Unable to create credentials directory');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves credentials to the local filesystem
|
||||
* @param credentials - The credentials to save
|
||||
*/
|
||||
export async function saveCredentials(credentials: Credentials): Promise<void> {
|
||||
try {
|
||||
// Validate credentials
|
||||
const validated = credentialsSchema.parse(credentials);
|
||||
|
||||
// Ensure directory exists
|
||||
await ensureCredentialsDir();
|
||||
|
||||
// Write credentials with restricted permissions
|
||||
await writeFile(
|
||||
CREDENTIALS_FILE,
|
||||
JSON.stringify(validated, null, 2),
|
||||
{ mode: 0o600 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid credentials: ${error.errors.map(e => e.message).join(', ')}`);
|
||||
}
|
||||
console.error('Failed to save credentials:', error);
|
||||
throw new Error('Unable to save credentials');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads credentials from the local filesystem
|
||||
* @returns The saved credentials or null if not found
|
||||
*/
|
||||
export async function loadCredentials(): Promise<Credentials | null> {
|
||||
try {
|
||||
const data = await readFile(CREDENTIALS_FILE, 'utf-8');
|
||||
const parsed = JSON.parse(data);
|
||||
return credentialsSchema.parse(parsed);
|
||||
} catch (error) {
|
||||
// File doesn't exist or is invalid
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes saved credentials
|
||||
*/
|
||||
export async function deleteCredentials(): Promise<void> {
|
||||
try {
|
||||
await unlink(CREDENTIALS_FILE);
|
||||
} catch (error) {
|
||||
// Ignore if file doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to delete credentials:', error);
|
||||
throw new Error('Unable to delete credentials');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets credentials from environment variables or saved file
|
||||
* @returns Credentials with environment variables taking precedence
|
||||
*/
|
||||
export async function getCredentials(): Promise<Credentials | null> {
|
||||
const envApiKey = process.env.BUSTER_API_KEY;
|
||||
const envApiUrl = process.env.BUSTER_HOST || process.env.BUSTER_API_URL;
|
||||
|
||||
// If we have env vars, use them (they take precedence)
|
||||
if (envApiKey) {
|
||||
return {
|
||||
apiKey: envApiKey,
|
||||
apiUrl: envApiUrl || DEFAULT_API_URL,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, try to load from file
|
||||
const saved = await loadCredentials();
|
||||
if (saved) {
|
||||
// Apply env overrides if present
|
||||
return {
|
||||
apiKey: saved.apiKey,
|
||||
apiUrl: envApiUrl || saved.apiUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has valid credentials configured
|
||||
* @returns true if credentials are available, false otherwise
|
||||
*/
|
||||
export async function hasCredentials(): Promise<boolean> {
|
||||
const creds = await getCredentials();
|
||||
return creds !== null && creds.apiKey.length > 0;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { validateApiKeyRequestSchema } from '@buster/server-shared';
|
||||
import { validateApiKeyHandler } from './validate-api-key';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
/**
|
||||
* POST /api/v2/auth/validate-api-key
|
||||
* Validates an API key
|
||||
*/
|
||||
app.post(
|
||||
'/validate-api-key',
|
||||
zValidator('json', validateApiKeyRequestSchema),
|
||||
async (c) => {
|
||||
const request = c.req.valid('json');
|
||||
const response = await validateApiKeyHandler(request);
|
||||
return c.json(response);
|
||||
}
|
||||
);
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,44 @@
|
|||
import type { ValidateApiKeyRequest, ValidateApiKeyResponse } from '@buster/server-shared';
|
||||
import { validateApiKey, getApiKeyDetails } from '@buster/database';
|
||||
|
||||
/**
|
||||
* Handler for validating an API key
|
||||
* @param request - The validation request containing the API key
|
||||
* @returns Promise<ValidateApiKeyResponse> - Response with validation status and details
|
||||
*/
|
||||
export async function validateApiKeyHandler(
|
||||
request: ValidateApiKeyRequest
|
||||
): Promise<ValidateApiKeyResponse> {
|
||||
try {
|
||||
// First check if the API key is valid
|
||||
const isValid = await validateApiKey(request.apiKey);
|
||||
|
||||
if (!isValid) {
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If valid, get the details
|
||||
const details = await getApiKeyDetails(request.apiKey);
|
||||
|
||||
if (!details) {
|
||||
// This shouldn't happen if validateApiKey returned true, but handle it
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
organizationId: details.organizationId,
|
||||
ownerId: details.ownerId,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error validating API key:', error);
|
||||
// Don't expose internal errors to the client
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { Hono } from 'hono';
|
||||
|
||||
import healthcheckRoutes from '../healthcheck';
|
||||
import authRoutes from './auth';
|
||||
import chatsRoutes from './chats';
|
||||
import dictionariesRoutes from './dictionaries';
|
||||
import electricShapeRoutes from './electric-shape';
|
||||
|
@ -16,6 +17,7 @@ import titleRoutes from './title';
|
|||
import userRoutes from './users';
|
||||
|
||||
const app = new Hono()
|
||||
.route('/auth', authRoutes)
|
||||
.route('/users', userRoutes)
|
||||
.route('/electric-shape', electricShapeRoutes)
|
||||
.route('/healthcheck', healthcheckRoutes)
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
"ci:check": "pnpm run check && pnpm run typecheck",
|
||||
"cli": "cd apps/cli && bun src/index.tsx",
|
||||
"cli:dev": "cd apps/cli && bun run --watch src/index.tsx",
|
||||
"cli:build": "pnpm --filter @buster-app/cli build",
|
||||
"cli:prod": "cd apps/cli && bun run dist/index.js",
|
||||
"db:check": "pnpm --filter @buster/database run db:check",
|
||||
"db:generate": "pnpm --filter @buster/database run db:generate",
|
||||
"db:generate:custom": "pnpm --filter @buster/database run db:generate:custom",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { validateApiKey, getApiKeyDetails } from './validate-api-key';
|
|
@ -0,0 +1,66 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { apiKeys } from '../../schema';
|
||||
import { getDb } from '../../connection';
|
||||
|
||||
/**
|
||||
* Validates an API key by checking if it exists in the database
|
||||
* and is not soft-deleted
|
||||
* @param apiKey - The API key string to validate
|
||||
* @returns Promise<boolean> - true if the API key is valid, false otherwise
|
||||
*/
|
||||
export async function validateApiKey(apiKey: string): Promise<boolean> {
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
.select({ id: apiKeys.id })
|
||||
.from(apiKeys)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeys.key, apiKey),
|
||||
isNull(apiKeys.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return result.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error validating API key:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API key details if it exists and is not soft-deleted
|
||||
* @param apiKey - The API key string to validate
|
||||
* @returns Promise<{id: string; organizationId: string; ownerId: string} | null>
|
||||
*/
|
||||
export async function getApiKeyDetails(apiKey: string): Promise<{
|
||||
id: string;
|
||||
organizationId: string;
|
||||
ownerId: string;
|
||||
} | null> {
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
id: apiKeys.id,
|
||||
organizationId: apiKeys.organizationId,
|
||||
ownerId: apiKeys.ownerId,
|
||||
})
|
||||
.from(apiKeys)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeys.key, apiKey),
|
||||
isNull(apiKeys.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return result[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error getting API key details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -15,3 +15,4 @@ export * from './s3-integrations';
|
|||
export * from './vault';
|
||||
export * from './cascading-permissions';
|
||||
export * from './github-integrations';
|
||||
export * from './api-keys';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { validateApiKey, isApiKeyValid } from './validate-api-key';
|
|
@ -0,0 +1,41 @@
|
|||
import type { ValidateApiKeyRequest, ValidateApiKeyResponse } from '@buster/server-shared';
|
||||
import type { SDKConfig } from '../config';
|
||||
import { post } from '../http';
|
||||
|
||||
/**
|
||||
* Validates an API key by calling the server endpoint
|
||||
* @param config - SDK configuration
|
||||
* @param apiKey - The API key to validate (optional, uses config.apiKey if not provided)
|
||||
* @returns Promise<ValidateApiKeyResponse> - Validation result with details
|
||||
*/
|
||||
export async function validateApiKey(
|
||||
config: SDKConfig,
|
||||
apiKey?: string
|
||||
): Promise<ValidateApiKeyResponse> {
|
||||
const request: ValidateApiKeyRequest = {
|
||||
apiKey: apiKey || config.apiKey,
|
||||
};
|
||||
|
||||
console.info(`Validating API key with endpoint: ${config.apiUrl}/api/v2/auth/validate-api-key`);
|
||||
|
||||
return post<ValidateApiKeyResponse>(config, '/api/v2/auth/validate-api-key', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple validation check that returns just a boolean
|
||||
* @param config - SDK configuration
|
||||
* @param apiKey - The API key to validate (optional, uses config.apiKey if not provided)
|
||||
* @returns Promise<boolean> - true if valid, false otherwise
|
||||
*/
|
||||
export async function isApiKeyValid(
|
||||
config: SDKConfig,
|
||||
apiKey?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await validateApiKey(config, apiKey);
|
||||
return response.valid;
|
||||
} catch (error) {
|
||||
console.error('API key validation error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,16 @@
|
|||
import { type SDKConfig, SDKConfigSchema } from '../config';
|
||||
import { get } from '../http';
|
||||
import { validateApiKey, isApiKeyValid } from '../auth';
|
||||
import type { ValidateApiKeyResponse } from '@buster/server-shared';
|
||||
|
||||
// SDK instance interface
|
||||
export interface BusterSDK {
|
||||
readonly config: SDKConfig;
|
||||
healthcheck: () => Promise<{ status: string; [key: string]: unknown }>;
|
||||
auth: {
|
||||
validateApiKey: (apiKey?: string) => Promise<ValidateApiKeyResponse>;
|
||||
isApiKeyValid: (apiKey?: string) => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
// Create SDK instance
|
||||
|
@ -15,5 +21,9 @@ export function createBusterSDK(config: Partial<SDKConfig>): BusterSDK {
|
|||
return {
|
||||
config: validatedConfig,
|
||||
healthcheck: () => get(validatedConfig, '/api/healthcheck'),
|
||||
auth: {
|
||||
validateApiKey: (apiKey?: string) => validateApiKey(validatedConfig, apiKey),
|
||||
isApiKeyValid: (apiKey?: string) => isApiKeyValid(validatedConfig, apiKey),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||
// SDK Configuration Schema
|
||||
export const SDKConfigSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
apiUrl: z.string().url().default('https://api.buster.so'),
|
||||
apiUrl: z.string().url().default('https://api2.buster.so'),
|
||||
timeout: z.number().min(1000).max(60000).default(30000),
|
||||
retryAttempts: z.number().min(0).max(5).default(3),
|
||||
retryDelay: z.number().min(100).max(5000).default(1000),
|
||||
|
|
|
@ -7,3 +7,6 @@ export type { SDKConfig } from './config';
|
|||
|
||||
// Error types
|
||||
export { SDKError, NetworkError } from './errors';
|
||||
|
||||
// Auth exports
|
||||
export { validateApiKey, isApiKeyValid } from './auth';
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export enum AuthErrorCode {
|
||||
INVALID_API_KEY = 'INVALID_API_KEY',
|
||||
MISSING_API_KEY = 'MISSING_API_KEY',
|
||||
API_KEY_EXPIRED = 'API_KEY_EXPIRED',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
}
|
||||
|
||||
export const authErrorSchema = z.object({
|
||||
code: z.nativeEnum(AuthErrorCode),
|
||||
message: z.string(),
|
||||
details: z.any().optional(),
|
||||
});
|
||||
|
||||
export type AuthError = z.infer<typeof authErrorSchema>;
|
|
@ -0,0 +1,3 @@
|
|||
export * from './requests';
|
||||
export * from './responses';
|
||||
export * from './errors';
|
|
@ -0,0 +1,10 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Request schema for validating an API key
|
||||
*/
|
||||
export const validateApiKeyRequestSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
});
|
||||
|
||||
export type ValidateApiKeyRequest = z.infer<typeof validateApiKeyRequestSchema>;
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Response schema for API key validation
|
||||
*/
|
||||
export const validateApiKeyResponseSchema = z.object({
|
||||
valid: z.boolean(),
|
||||
organizationId: z.string().uuid().optional(),
|
||||
ownerId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export type ValidateApiKeyResponse = z.infer<typeof validateApiKeyResponseSchema>;
|
|
@ -5,6 +5,7 @@ export * as AccessControls from './access-controls';
|
|||
|
||||
// Export other modules
|
||||
export * from './assets';
|
||||
export * from './auth';
|
||||
export * from './chats';
|
||||
export * from './dashboards';
|
||||
export * from './dictionary';
|
||||
|
|
1494
pnpm-lock.yaml
1494
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ packages:
|
|||
- "apps/electric-server"
|
||||
- "apps/trigger"
|
||||
- "apps/api"
|
||||
- "apps/cli"
|
||||
|
||||
catalog:
|
||||
"@supabase/supabase-js": "^2.50.0"
|
||||
|
|
Loading…
Reference in New Issue