Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6fa1fe8
docs(README): real quick start
snipeship Jul 28, 2025
245c509
chore(repo): rename claudeflare to ccflare and update package setup
snipeship Jul 28, 2025
0953994
refactor(core): rename claudeflare to ccflare project-wide
snipeship Jul 28, 2025
b1a5d4b
Merge branch 'chore/rename'
snipeship Jul 28, 2025
f8968e2
docs(package.json): update project description
snipeship Jul 28, 2025
b663bc6
Merge branch 'chore/rename'
snipeship Jul 28, 2025
de65bd8
Dockerfile for building
voarsh Jul 29, 2025
423208e
Docker build fixes
voarsh Jul 29, 2025
12e49dc
Dockerfile: update DB path
voarsh Jul 29, 2025
cbe4cf6
Remote deploy: basic key auth
voarsh Jul 29, 2025
17e1462
feat: implement SQLite retry mechanism and distributed filesystem opt…
voarsh Jul 29, 2025
e79080e
feat: protect dashboard with API key authentication… incl resources
voarsh Jul 29, 2025
c45c5fb
perf: optimize requests page performance and eliminate N+1 queries
voarsh Jul 29, 2025
0c77eb6
perf: fix requests page performance by eliminating JSON parsing bottl…
voarsh Jul 29, 2025
582d190
SQllite Repair scripts
voarsh Jul 29, 2025
b2ec9a8
fix: implement lazy loading and database resilience improvements
voarsh Jul 29, 2025
ba3b32e
Merge branch 'main' into remote-run
voarsh Jul 30, 2025
29d08dc
stale merge cleanup
voarsh Jul 30, 2025
ca93c7b
merge misses
voarsh Jul 30, 2025
505389d
Improves database configuration and resilience
voarsh Jul 30, 2025
b126d58
(WIP) Enables multi-database support and modernizes architecture
voarsh Jul 31, 2025
f1a0a68
Merge origin/main into remote-run
voarsh Jul 31, 2025
79215fc
Merge remote-run into db-providers
voarsh Jul 31, 2025
fce9b21
Improves database provider compatibility
voarsh Jul 31, 2025
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
12 changes: 12 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apps/
packages/
docs/
node_modules/
bun.lock
.git/
.gitignore
CLAUDE.md
tsconfig.json
biome.json
*.ts
*.tsx
63 changes: 63 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Multi-stage build for ccflare
FROM oven/bun:1-alpine AS builder

WORKDIR /app

# Copy package files for dependency caching
COPY package.json bun.lock* ./

# Copy all source code (required for workspace dependencies)
COPY . .

# Install dependencies
RUN bun install --frozen-lockfile

# Build the project
RUN bun run build

# Production stage
FROM oven/bun:1-alpine AS runner

WORKDIR /app

# Install database tools for repair and debugging
# SQLite for default database, PostgreSQL and MySQL clients for external databases
RUN apk add --no-cache sqlite postgresql-client mysql-client

# Create non-root user
RUN addgroup -g 1001 -S ccflare && \
adduser -S ccflare -u 1001 -G ccflare

# Copy built application
COPY --from=builder --chown=ccflare:ccflare /app .

# Copy repair scripts
COPY --chown=ccflare:ccflare scripts/ /app/scripts/
RUN find /app/scripts -name '*.sh' -type f -exec chmod +x {} + 2>/dev/null || true

# Create data directory for SQLite database (when using SQLite)
RUN mkdir -p /app/data && chown ccflare:ccflare /app/data

# Switch to non-root user
USER ccflare

# Set API key for authentication (change this in production!)
ENV API_KEY=ccflare-default-key

# Database configuration
# Default to SQLite with persistent volume mount
ENV DATABASE_PROVIDER=sqlite
ENV ccflare_DB_PATH=/app/data/ccflare.db

# For PostgreSQL/MySQL, override these environment variables:
# ENV DATABASE_PROVIDER=postgresql
# ENV DATABASE_URL=postgresql://user:password@host:5432/database
# or
# ENV DATABASE_PROVIDER=mysql
# ENV DATABASE_URL=mysql://user:password@host:3306/database

# Expose port
EXPOSE 8080

# Start the server (not TUI)
CMD ["bun", "run", "server"]
30 changes: 30 additions & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Test Dockerfile for running integration tests against different database providers
FROM oven/bun:1.1.29-alpine

# Install curl and other testing utilities
RUN apk add --no-cache curl wget jq

# Set working directory
WORKDIR /app

# Copy package files
COPY package.json bun.lockb ./
COPY packages/ ./packages/

# Install dependencies
RUN bun install --frozen-lockfile

# Copy source code
COPY . .

# Build the project (if build script exists)
RUN bun run build || echo "No build script found, continuing..."

# Create test directory
RUN mkdir -p /app/tests/integration

# Copy test files
COPY tests/integration/ /app/tests/integration/

# Default command runs tests
CMD ["bun", "test", "/app/tests/integration/"]
97 changes: 81 additions & 16 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
shutdown,
TIME_CONSTANTS,
} from "@ccflare/core";
import type { Account } from "@ccflare/types";
import { container, SERVICE_KEYS } from "@ccflare/core-di";
// Import React dashboard assets
import dashboardManifest from "@ccflare/dashboard-web/dist/manifest.json";
Expand Down Expand Up @@ -80,7 +81,7 @@ function serveDashboardFile(
let serverInstance: ReturnType<typeof serve> | null = null;

// Export for programmatic use
export default function startServer(options?: {
export default async function startServer(options?: {
port?: number;
withDashboard?: boolean;
}) {
Expand All @@ -97,7 +98,7 @@ export default function startServer(options?: {
};
}

const { port = NETWORK.DEFAULT_PORT, withDashboard = true } = options || {};
const { port, withDashboard = true } = options || {};

// Initialize DI container
container.registerInstance(SERVICE_KEYS.Config, new Config());
Expand All @@ -106,8 +107,8 @@ export default function startServer(options?: {
// Initialize components
const config = container.resolve<Config>(SERVICE_KEYS.Config);
const runtime = config.getRuntime();
// Override port if provided
if (port !== runtime.port) {
// Override port if explicitly provided in options
if (port !== undefined && port !== runtime.port) {
runtime.port = port;
}
DatabaseFactory.initialize(undefined, runtime);
Expand Down Expand Up @@ -151,7 +152,7 @@ export default function startServer(options?: {
"session_duration_ms",
TIME_CONSTANTS.SESSION_DURATION_DEFAULT,
) as number,
port,
port: runtime.port,
};

// Now create the strategy with runtime config
Expand Down Expand Up @@ -196,24 +197,65 @@ export default function startServer(options?: {
return apiResponse;
}

// Check API key for auth protection
const apiKey = process.env.API_KEY;

// Dashboard routes (only if enabled)
if (withDashboard) {
if (url.pathname === "/" || url.pathname === "/dashboard") {
// Dashboard routes with API key protection
if (url.pathname === "/" || url.pathname === "/dashboard" ||
(apiKey && url.pathname === `/${apiKey}/`)) {

// If API key is required, only allow /{key}/ access
if (apiKey && url.pathname !== `/${apiKey}/`) {
return new Response("Not Found", { status: HTTP_STATUS.NOT_FOUND });
}

return serveDashboardFile("/index.html", "text/html");
}

// Serve dashboard static assets
if ((dashboardManifest as Record<string, string>)[url.pathname]) {
// Serve dashboard static assets with auth protection
let assetPathname = url.pathname;
let isAuthenticatedAssetRequest = false;

// If API key is set, check for auth-prefixed asset paths
if (apiKey && url.pathname.startsWith(`/${apiKey}/`)) {
// Strip the key prefix for asset lookup
assetPathname = url.pathname.substring(`/${apiKey}`.length);
isAuthenticatedAssetRequest = true;
}

if ((dashboardManifest as Record<string, string>)[assetPathname]) {
// If API key is required but request is not authenticated, block access
if (apiKey && !isAuthenticatedAssetRequest) {
return new Response("Not Found", { status: HTTP_STATUS.NOT_FOUND });
}

return serveDashboardFile(
url.pathname,
assetPathname,
undefined,
CACHE.CACHE_CONTROL_STATIC,
);
}
}

// All other paths go to proxy
return handleProxy(req, url, proxyContext);
// Handle API authentication and proxying
if (apiKey) {
// Auth required - check for /key/v1/ format
const pathParts = url.pathname.split('/').filter(Boolean);
if (pathParts[0] === apiKey && pathParts[1] === 'v1') {
// Valid auth - rewrite path and proxy
url.pathname = '/' + pathParts.slice(1).join('/');
return handleProxy(req, url, proxyContext);
}
return new Response("Not Found", { status: HTTP_STATUS.NOT_FOUND });
} else {
// No auth required - allow direct /v1/ access
if (!url.pathname.startsWith("/v1/")) {
return new Response("Not Found", { status: HTTP_STATUS.NOT_FOUND });
}
return handleProxy(req, url, proxyContext);
}
},
});

Expand Down Expand Up @@ -243,10 +285,30 @@ Available endpoints:
);

// Log initial account status
const accounts = dbOps.getAllAccounts();
const activeAccounts = accounts.filter(
(a) => !a.paused && (!a.expires_at || a.expires_at > Date.now()),
);
let accounts: Account[] = [];
let activeAccounts: Account[] = [];

// Use async method if available (new DrizzleDatabaseOperations)
if ('getAllAccountsAsync' in dbOps) {
try {
accounts = await (dbOps as any).getAllAccountsAsync();
activeAccounts = accounts.filter(
(a) => !a.paused && (!a.expires_at || a.expires_at > Date.now()),
);
} catch (error) {
log.warn("Failed to get accounts asynchronously, falling back to sync method");
accounts = dbOps.getAllAccounts();
activeAccounts = accounts.filter(
(a) => !a.paused && (!a.expires_at || a.expires_at > Date.now()),
);
}
} else {
// Fallback to sync method for legacy DatabaseOperations
accounts = dbOps.getAllAccounts();
activeAccounts = accounts.filter(
(a) => !a.paused && (!a.expires_at || a.expires_at > Date.now()),
);
}
log.info(
`Loaded ${accounts.length} accounts (${activeAccounts.length} active)`,
);
Expand Down Expand Up @@ -287,5 +349,8 @@ process.on("SIGTERM", () => handleGracefulShutdown("SIGTERM"));

// Run server if this is the main entry point
if (import.meta.main) {
startServer();
startServer().catch(error => {
console.error("Failed to start server:", error);
process.exit(1);
});
}
2 changes: 1 addition & 1 deletion apps/tui/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let runningServer: ReturnType<typeof startServer> | null = null;

async function ensureServer(port: number) {
if (!runningServer) {
runningServer = startServer({ port, withDashboard: true });
runningServer = await startServer({ port, withDashboard: true });
}
return runningServer;
}
Expand Down
Loading