Skip to content
Closed
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
25 changes: 18 additions & 7 deletions src/lib/auto-fix/client/auto-fix-worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,27 @@ export interface DispatchFixResponse {
* Handles all communication with the Cloudflare Worker for auto fix
*/
class AutoFixWorkerClient {
private readonly baseUrl: string;
private readonly authToken: string;
private _baseUrl: string | null = null;
private _authToken: string | null = null;

constructor() {
if (!AUTO_FIX_URL || !AUTO_FIX_AUTH_TOKEN) {
throw new Error('AUTO_FIX_URL or AUTO_FIX_AUTH_TOKEN not configured');
private get baseUrl(): string {
if (!this._baseUrl) {
if (!AUTO_FIX_URL) {
throw new Error('AUTO_FIX_URL not configured');
}
this._baseUrl = AUTO_FIX_URL;
}
return this._baseUrl;
}

this.baseUrl = AUTO_FIX_URL;
this.authToken = AUTO_FIX_AUTH_TOKEN;
private get authToken(): string {
if (!this._authToken) {
if (!AUTO_FIX_AUTH_TOKEN) {
throw new Error('AUTO_FIX_AUTH_TOKEN not configured');
}
this._authToken = AUTO_FIX_AUTH_TOKEN;
}
return this._authToken;
}

/**
Expand Down
25 changes: 18 additions & 7 deletions src/lib/auto-triage/client/triage-worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,27 @@ export interface DispatchTriageResponse {
* Handles all communication with the Cloudflare Worker for auto triage
*/
class TriageWorkerClient {
private readonly baseUrl: string;
private readonly authToken: string;
private _baseUrl: string | null = null;
private _authToken: string | null = null;

constructor() {
if (!AUTO_TRIAGE_URL || !AUTO_TRIAGE_AUTH_TOKEN) {
throw new Error('AUTO_TRIAGE_URL or AUTO_TRIAGE_AUTH_TOKEN not configured');
private get baseUrl(): string {
if (!this._baseUrl) {
if (!AUTO_TRIAGE_URL) {
throw new Error('AUTO_TRIAGE_URL not configured');
}
this._baseUrl = AUTO_TRIAGE_URL;
}
return this._baseUrl;
}

this.baseUrl = AUTO_TRIAGE_URL;
this.authToken = AUTO_TRIAGE_AUTH_TOKEN;
private get authToken(): string {
if (!this._authToken) {
if (!AUTO_TRIAGE_AUTH_TOKEN) {
throw new Error('AUTO_TRIAGE_AUTH_TOKEN not configured');
}
this._authToken = AUTO_TRIAGE_AUTH_TOKEN;
}
return this._authToken;
}

/**
Expand Down
27 changes: 19 additions & 8 deletions src/lib/code-reviews/client/code-review-worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,27 @@ export interface CancelReviewResponse {
* Handles all communication with the Cloudflare Worker for code reviews
*/
class CodeReviewWorkerClient {
private readonly baseUrl: string;
private readonly authToken: string;

constructor() {
if (!CODE_REVIEW_WORKER_URL || !CODE_REVIEW_WORKER_AUTH_TOKEN) {
throw new Error('CODE_REVIEW_WORKER_URL or CODE_REVIEW_WORKER_AUTH_TOKEN not configured');
private _baseUrl: string | null = null;
private _authToken: string | null = null;

private get baseUrl(): string {
if (!this._baseUrl) {
if (!CODE_REVIEW_WORKER_URL) {
throw new Error('CODE_REVIEW_WORKER_URL not configured');
}
this._baseUrl = CODE_REVIEW_WORKER_URL;
}
return this._baseUrl;
}

this.baseUrl = CODE_REVIEW_WORKER_URL;
this.authToken = CODE_REVIEW_WORKER_AUTH_TOKEN;
private get authToken(): string {
if (!this._authToken) {
if (!CODE_REVIEW_WORKER_AUTH_TOKEN) {
throw new Error('CODE_REVIEW_WORKER_AUTH_TOKEN not configured');
}
this._authToken = CODE_REVIEW_WORKER_AUTH_TOKEN;
}
return this._authToken;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/r2/cli-sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Readable } from 'node:stream';

jest.mock('./client', () => ({
r2Client: { send: jest.fn() },
r2CliSessionsBucketName: 'test-bucket',
getR2CliSessionsBucketName: () => 'test-bucket',
}));

jest.mock('@aws-sdk/s3-request-presigner', () => ({
Expand Down
19 changes: 10 additions & 9 deletions src/lib/r2/cli-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { Readable } from 'node:stream';
import { r2Client, r2CliSessionsBucketName } from './client';
import { r2Client, getR2CliSessionsBucketName } from './client';
import type { CliSession } from '@/db/schema';

export type FolderName = 'sessions' | 'shared-sessions';
Expand Down Expand Up @@ -41,7 +41,7 @@ export async function uploadBlob(
const key = getBlobKey(sessionId, folderName, filename);

const command = {
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Key: key,
Body: rawContent,
ContentType: 'application/json',
Expand Down Expand Up @@ -70,7 +70,7 @@ export async function generateSignedUploadUrl(
const key = getBlobKey(sessionId, folderName, filename);

const command = new PutObjectCommand({
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Key: key,
ContentType: 'application/json',
ContentLength: contentLength,
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function generateSignedUrls(
const urlPromises = filenames.map(async filename => {
const key = getBlobKey(sessionId, folderName, filename);
const command = new GetObjectCommand({
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Key: key,
});

Expand Down Expand Up @@ -129,7 +129,7 @@ export async function deleteBlobs(

await r2Client.send(
new DeleteObjectsCommand({
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Delete: {
Objects: objects,
Quiet: true,
Expand All @@ -141,7 +141,7 @@ export async function deleteBlobs(
export async function getBlobContent(blobKey: string): Promise<unknown> {
const response = await r2Client.send(
new GetObjectCommand({
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Key: blobKey,
})
);
Expand Down Expand Up @@ -169,18 +169,19 @@ export async function copyBlobs(
try {
await r2Client.send(
new HeadObjectCommand({
Bucket: r2CliSessionsBucketName,
Bucket: getR2CliSessionsBucketName(),
Key: sourceKey,
})
);
} catch {
return null;
}

const bucketName = getR2CliSessionsBucketName();
await r2Client.send(
new CopyObjectCommand({
Bucket: r2CliSessionsBucketName,
CopySource: `${r2CliSessionsBucketName}/${sourceKey}`,
Bucket: bucketName,
CopySource: `${bucketName}/${sourceKey}`,
Key: destinationKey,
})
);
Expand Down
102 changes: 74 additions & 28 deletions src/lib/r2/client.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,91 @@
import { S3Client } from '@aws-sdk/client-s3';
import { getEnvVariable } from '@/lib/dotenvx';

// R2 configuration from environment variables
const R2_ACCOUNT_ID = getEnvVariable('R2_ACCOUNT_ID');
const R2_ACCESS_KEY_ID = getEnvVariable('R2_ACCESS_KEY_ID');
const R2_SECRET_ACCESS_KEY = getEnvVariable('R2_SECRET_ACCESS_KEY');
const R2_CLI_SESSIONS_BUCKET_NAME = getEnvVariable('R2_CLI_SESSIONS_BUCKET_NAME');
const CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME = getEnvVariable(
'CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME'
);

if (!R2_ACCOUNT_ID) {
throw new Error('R2_ACCOUNT_ID environment variable is required');
}
// R2 configuration from environment variables (lazy-loaded to avoid breaking the app at startup)
function getR2Config() {
const R2_ACCOUNT_ID = getEnvVariable('R2_ACCOUNT_ID');
const R2_ACCESS_KEY_ID = getEnvVariable('R2_ACCESS_KEY_ID');
const R2_SECRET_ACCESS_KEY = getEnvVariable('R2_SECRET_ACCESS_KEY');
const R2_CLI_SESSIONS_BUCKET_NAME = getEnvVariable('R2_CLI_SESSIONS_BUCKET_NAME');
const CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME = getEnvVariable(
'CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME'
);

if (!R2_ACCESS_KEY_ID) {
throw new Error('R2_ACCESS_KEY_ID environment variable is required');
}
if (!R2_ACCOUNT_ID) {
throw new Error('R2_ACCOUNT_ID environment variable is required');
}

if (!R2_ACCESS_KEY_ID) {
throw new Error('R2_ACCESS_KEY_ID environment variable is required');
}

if (!R2_SECRET_ACCESS_KEY) {
throw new Error('R2_SECRET_ACCESS_KEY environment variable is required');
}

if (!R2_SECRET_ACCESS_KEY) {
throw new Error('R2_SECRET_ACCESS_KEY environment variable is required');
if (!R2_CLI_SESSIONS_BUCKET_NAME) {
throw new Error('R2_CLI_SESSIONS_BUCKET_NAME environment variable is required');
}

return {
R2_ACCOUNT_ID,
R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY,
R2_CLI_SESSIONS_BUCKET_NAME,
CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME,
};
}

if (!R2_CLI_SESSIONS_BUCKET_NAME) {
throw new Error('R2_CLI_SESSIONS_BUCKET_NAME environment variable is required');
let _r2Client: S3Client | null = null;
let _r2CliSessionsBucketName: string | null = null;
let _r2CloudAgentAttachmentsBucketName: string | undefined = undefined;

function initR2Client() {
if (_r2Client) return;

const config = getR2Config();

_r2Client = new S3Client({
region: 'auto',
endpoint: `https://${config.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: config.R2_ACCESS_KEY_ID,
secretAccessKey: config.R2_SECRET_ACCESS_KEY,
},
});

_r2CliSessionsBucketName = config.R2_CLI_SESSIONS_BUCKET_NAME;
_r2CloudAgentAttachmentsBucketName = config.CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME;
}

/**
* Singleton S3 client configured for Cloudflare R2.
*
* R2 is Cloudflare's S3-compatible object storage service.
* The client is configured with R2-specific endpoint and credentials.
*
* Note: This is lazy-initialized to avoid breaking the app at startup
* when R2 environment variables are not configured.
*/
export const r2Client = new S3Client({
region: 'auto',
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
export const r2Client = {
send: (...args: Parameters<S3Client['send']>) => {
initR2Client();
if (!_r2Client) {
throw new Error('R2 client failed to initialize');
}
return _r2Client.send(...args);
},
});
} as S3Client;

export const r2CliSessionsBucketName = R2_CLI_SESSIONS_BUCKET_NAME;
export const r2CloudAgentAttachmentsBucketName = CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME;
export function getR2CliSessionsBucketName(): string {
initR2Client();
if (!_r2CliSessionsBucketName) {
throw new Error('R2 CLI sessions bucket name not configured');
}
return _r2CliSessionsBucketName;
}

export function getR2CloudAgentAttachmentsBucketName(): string | undefined {
initR2Client();
return _r2CloudAgentAttachmentsBucketName;
}
4 changes: 2 additions & 2 deletions src/lib/r2/cloud-agent-attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CLOUD_AGENT_IMAGE_PRESIGNED_URL_EXPIRY_SECONDS,
} from '@/lib/cloud-agent/constants';
import type { CloudAgentImageAllowedType } from '@/lib/cloud-agent/constants';
import { r2Client, r2CloudAgentAttachmentsBucketName } from '@/lib/r2/client';
import { r2Client, getR2CloudAgentAttachmentsBucketName } from '@/lib/r2/client';

type Service = 'app-builder';

Expand Down Expand Up @@ -50,7 +50,7 @@ export async function generateImageUploadUrl({
const key = getImageKey(service, userId, messageUuid, imageId, contentType);

const command = new PutObjectCommand({
Bucket: r2CloudAgentAttachmentsBucketName,
Bucket: getR2CloudAgentAttachmentsBucketName(),
Key: key,
ContentType: contentType,
ContentLength: contentLength,
Expand Down