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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN ARCH="$(dpkg --print-architecture)" \
*) echo "Unsupported architecture: ${ARCH}" >&2; exit 1 ;; \
esac \
&& apt-get update && apt-get install -y xz-utils ca-certificates rsync \
&& curl -fsSLk https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz -o /tmp/node.tar.xz \
&& curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz -o /tmp/node.tar.xz \
&& tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \
&& rm /tmp/node.tar.xz \
&& node --version \
Expand Down
10 changes: 9 additions & 1 deletion skills/cloudflare-browser/scripts/cdp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ function createClient(options = {}) {
}

const workerUrl = (options.workerUrl || process.env.WORKER_URL).replace(/^https?:\/\//, '');
// Keep query param for backwards compatibility, but also use Authorization header (preferred)
const wsUrl = `wss://${workerUrl}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;
const timeout = options.timeout || 60000;

// Security: Use Authorization header (preferred over query param to avoid logging secrets)
const WS_OPTIONS = {
headers: {
'Authorization': `Bearer ${CDP_SECRET}`,
},
};

return new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl);
const ws = new WebSocket(wsUrl, WS_OPTIONS);
let messageId = 1;
const pending = new Map();
let targetId = null;
Expand Down
10 changes: 9 additions & 1 deletion skills/cloudflare-browser/scripts/screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ if (!CDP_SECRET) {
}

const WORKER_URL = process.env.WORKER_URL.replace(/^https?:\/\//, '');
// Keep query param for backwards compatibility
const WS_URL = `wss://${WORKER_URL}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;

// Security: Use Authorization header (preferred over query param to avoid logging secrets)
const WS_OPTIONS = {
headers: {
'Authorization': `Bearer ${CDP_SECRET}`,
},
};

const url = process.argv[2];
const output = process.argv[3] || 'screenshot.png';

Expand All @@ -31,7 +39,7 @@ const pending = new Map();
async function main() {
console.log(`Capturing screenshot of ${url}`);

const ws = new WebSocket(WS_URL);
const ws = new WebSocket(WS_URL, WS_OPTIONS);
let targetResolve;
const targetReady = new Promise(r => { targetResolve = r; });

Expand Down
10 changes: 9 additions & 1 deletion skills/cloudflare-browser/scripts/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ if (!CDP_SECRET) {
}

const WORKER_URL = process.env.WORKER_URL.replace(/^https?:\/\//, '');
// Keep query param for backwards compatibility
const WS_URL = `wss://${WORKER_URL}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;

// Security: Use Authorization header (preferred over query param to avoid logging secrets)
const WS_OPTIONS = {
headers: {
'Authorization': `Bearer ${CDP_SECRET}`,
},
};

// Parse args
const args = process.argv.slice(2);
const urlArg = args.find(a => !a.startsWith('--'));
Expand All @@ -44,7 +52,7 @@ async function main() {
console.log(`Creating video from ${urls.length} URL(s)`);
console.log(`Output: ${output}, FPS: ${fps}, Scroll: ${doScroll}\n`);

const ws = new WebSocket(WS_URL);
const ws = new WebSocket(WS_URL, WS_OPTIONS);
let targetResolve;
const targetReady = new Promise(r => { targetResolve = r; });

Expand Down
32 changes: 32 additions & 0 deletions src/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,20 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) {

// Get JWT
const jwt = extractJWT(c);
const clientIP = c.req.header('CF-Connecting-IP') || 'unknown';
const userAgent = c.req.header('User-Agent') || 'unknown';

if (!jwt) {
// Security: Log authentication failure
console.log(JSON.stringify({
event: 'auth_failure',
reason: 'missing_jwt',
ip: clientIP,
userAgent,
path: new URL(c.req.url).pathname,
timestamp: new Date().toISOString(),
}));

if (type === 'html' && redirectOnMissing) {
return c.redirect(`https://${teamDomain}`, 302);
}
Expand All @@ -107,8 +119,28 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) {
try {
const payload = await verifyAccessJWT(jwt, teamDomain, expectedAud);
c.set('accessUser', { email: payload.email, name: payload.name });

// Security: Log successful authentication
console.log(JSON.stringify({
event: 'auth_success',
email: payload.email,
ip: clientIP,
path: new URL(c.req.url).pathname,
timestamp: new Date().toISOString(),
}));

await next();
} catch (err) {
// Security: Log authentication failure with details
console.log(JSON.stringify({
event: 'auth_failure',
reason: 'invalid_jwt',
error: err instanceof Error ? err.message : 'Unknown error',
ip: clientIP,
userAgent,
path: new URL(c.req.url).pathname,
timestamp: new Date().toISOString(),
}));
console.error('Access JWT verification failed:', err);

if (type === 'json') {
Expand Down
39 changes: 35 additions & 4 deletions src/gateway/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ import { MOLTBOT_PORT, STARTUP_TIMEOUT_MS } from '../config';
import { buildEnvVars } from './env';
import { mountR2Storage } from './r2';

/**
* Lock to prevent race conditions when starting the gateway
*/
let gatewayLock: Promise<void> | null = null;

/**
* Execute a function with a lock to prevent concurrent execution
*/
async function withGatewayLock<T>(fn: () => Promise<T>): Promise<T> {
// Wait for any existing lock to complete
while (gatewayLock) {
await gatewayLock;
}

// Create our lock
let releaseLock: () => void;
gatewayLock = new Promise((resolve) => {
releaseLock = resolve;
});

try {
return await fn();
} finally {
gatewayLock = null;
releaseLock!();
}
}

/**
* Find an existing Moltbot gateway process
*
Expand Down Expand Up @@ -48,9 +76,10 @@ export async function findExistingMoltbotProcess(sandbox: Sandbox): Promise<Proc
* @returns The running gateway process
*/
export async function ensureMoltbotGateway(sandbox: Sandbox, env: MoltbotEnv): Promise<Process> {
// Mount R2 storage for persistent data (non-blocking if not configured)
// R2 is used as a backup - the startup script will restore from it on boot
await mountR2Storage(sandbox, env);
return withGatewayLock(async () => {
// Mount R2 storage for persistent data (non-blocking if not configured)
// R2 is used as a backup - the startup script will restore from it on boot
await mountR2Storage(sandbox, env);

// Check if Moltbot is already running or starting
const existingProcess = await findExistingMoltbotProcess(sandbox);
Expand Down Expand Up @@ -82,7 +111,8 @@ export async function ensureMoltbotGateway(sandbox: Sandbox, env: MoltbotEnv): P
const command = '/usr/local/bin/start-moltbot.sh';

console.log('Starting process with command:', command);
console.log('Environment vars being passed:', Object.keys(envVars));
// Security: Only log count of env vars, not their names (which could leak info)
console.log('Environment vars count:', Object.keys(envVars).length);

let process: Process;
try {
Expand Down Expand Up @@ -121,4 +151,5 @@ export async function ensureMoltbotGateway(sandbox: Sandbox, env: MoltbotEnv): P
console.log('[Gateway] Verifying gateway health...');

return process;
});
}
100 changes: 65 additions & 35 deletions src/gateway/r2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@ import type { Sandbox } from '@cloudflare/sandbox';
import type { MoltbotEnv } from '../types';
import { R2_MOUNT_PATH, getR2BucketName } from '../config';

/**
* Lock to prevent race conditions during mount operations
*/
let mountLock: Promise<void> | null = null;

/**
* Execute a function with a lock to prevent concurrent mount operations
*/
async function withMountLock<T>(fn: () => Promise<T>): Promise<T> {
// Wait for any existing lock to complete
while (mountLock) {
await mountLock;
}

// Create our lock
let releaseLock: () => void;
mountLock = new Promise((resolve) => {
releaseLock = resolve;
});

try {
return await fn();
} finally {
mountLock = null;
releaseLock!();
}
}

/**
* Check if R2 is already mounted by looking at the mount table
*/
Expand Down Expand Up @@ -33,43 +61,45 @@ async function isR2Mounted(sandbox: Sandbox): Promise<boolean> {
* @returns true if mounted successfully, false otherwise
*/
export async function mountR2Storage(sandbox: Sandbox, env: MoltbotEnv): Promise<boolean> {
// Skip if R2 credentials are not configured
if (!env.R2_ACCESS_KEY_ID || !env.R2_SECRET_ACCESS_KEY || !env.CF_ACCOUNT_ID) {
console.log('R2 storage not configured (missing R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, or CF_ACCOUNT_ID)');
return false;
}

// Check if already mounted first - this avoids errors and is faster
if (await isR2Mounted(sandbox)) {
console.log('R2 bucket already mounted at', R2_MOUNT_PATH);
return true;
}
return withMountLock(async () => {
// Skip if R2 credentials are not configured
if (!env.R2_ACCESS_KEY_ID || !env.R2_SECRET_ACCESS_KEY || !env.CF_ACCOUNT_ID) {
console.log('R2 storage not configured (missing R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, or CF_ACCOUNT_ID)');
return false;
}

const bucketName = getR2BucketName(env);
try {
console.log('Mounting R2 bucket', bucketName, 'at', R2_MOUNT_PATH);
await sandbox.mountBucket(bucketName, R2_MOUNT_PATH, {
endpoint: `https://${env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Pass credentials explicitly since we use R2_* naming instead of AWS_*
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
console.log('R2 bucket mounted successfully - moltbot data will persist across sessions');
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.log('R2 mount error:', errorMessage);

// Check again if it's mounted - the error might be misleading
// Check if already mounted first - this avoids errors and is faster
if (await isR2Mounted(sandbox)) {
console.log('R2 bucket is mounted despite error');
console.log('R2 bucket already mounted at', R2_MOUNT_PATH);
return true;
}

// Don't fail if mounting fails - moltbot can still run without persistent storage
console.error('Failed to mount R2 bucket:', err);
return false;
}

const bucketName = getR2BucketName(env);
try {
console.log('Mounting R2 bucket', bucketName, 'at', R2_MOUNT_PATH);
await sandbox.mountBucket(bucketName, R2_MOUNT_PATH, {
endpoint: `https://${env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Pass credentials explicitly since we use R2_* naming instead of AWS_*
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
console.log('R2 bucket mounted successfully - moltbot data will persist across sessions');
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.log('R2 mount error:', errorMessage);

// Check again if it's mounted - the error might be misleading
if (await isR2Mounted(sandbox)) {
console.log('R2 bucket is mounted despite error');
return true;
}

// Don't fail if mounting fails - moltbot can still run without persistent storage
console.error('Failed to mount R2 bucket:', err);
return false;
}
});
}
Loading