Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bun test
bun run test:bun
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:bun": "bun run vitest run",
"test:bun:watch": "bun run vitest",
"db:generate": "drizzle-kit generate --config=drizzle.config.local.ts",
"db:generate:remote": "drizzle-kit generate --config=drizzle.config.remote.ts",
"db:migrate:local": "wrangler d1 migrations apply vibesdk-db --local",
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.worker.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"types": ["@cloudflare/workers-types", "./worker-configuration.d.ts", "vite/client", "jest"],
"lib": ["ES2023"]
},
"include": ["./worker-configuration.d.ts", "./shared", "./worker", "./worker/types"]
"include": ["./worker-configuration.d.ts", "./shared", "./worker", "./worker/types"],
"exclude": ["**/__tests__/**/*", "**/*.test.ts"]
}
23 changes: 20 additions & 3 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,35 @@ import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';

export default defineWorkersConfig({
test: {
globals: true,
pool: '@cloudflare/vitest-pool-workers',
deps: {
optimizer: {
ssr: {
enabled: true,
include: [
'ajv',
'@cloudflare/containers',
'@cloudflare/sandbox',
'@babel/traverse',
'@babel/types'
],
},
},
},
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.test.jsonc' },
miniflare: {
compatibilityDate: '2024-12-12',
compatibilityFlags: ['nodejs_compat'],
bindings: {
SECRETS_ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
},
},
},
},
globals: true,
setupFiles: ['./test/setup.ts'],
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
exclude: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/test/**', '**/worker/api/routes/**'],
exclude: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/worker/api/routes/**'],
},
});
1 change: 1 addition & 0 deletions worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ declare namespace Cloudflare {
CodeGenObject: DurableObjectNamespace<import("./worker/index").CodeGeneratorAgent>;
Sandbox: DurableObjectNamespace<import("./worker/index").UserAppSandboxService>;
DORateLimitStore: DurableObjectNamespace<import("./worker/index").DORateLimitStore>;
UserSecretsStore: DurableObjectNamespace<import("./worker/index").UserSecretsStore>;
TEMPLATES_BUCKET: R2Bucket;
DB: D1Database;
DISPATCHER: DispatchNamespace;
Expand Down
232 changes: 232 additions & 0 deletions worker/api/controllers/user-secrets/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
* User Secrets Controller - RPC wrapper for UserSecretsStore DO
*/

import { BaseController } from '../baseController';
import { ApiResponse, ControllerResponse } from '../types';
import { RouteContext } from '../../types/route-context';
import { createLogger } from '../../../logger';
import type { SecretMetadata, StoreSecretRequest, UpdateSecretRequest } from '../../../services/secrets/types';

type UserSecretsListData = { secrets: SecretMetadata[] };
type UserSecretStoreData = { secret: SecretMetadata; message: string };
type UserSecretValueData = { value: string; metadata: SecretMetadata };
type UserSecretUpdateData = { secret: SecretMetadata; message: string };
type UserSecretDeleteData = { message: string };

export class UserSecretsController extends BaseController {
static logger = createLogger('UserSecretsController');

/**
* Get Durable Object stub for user
*/
private static getUserSecretsStub(env: Env, userId: string) {
const id = env.UserSecretsStore.idFromName(userId);
return env.UserSecretsStore.get(id);
}

/**
* List all secrets (metadata only)
* GET /api/user-secrets
*/
static async listSecrets(
_request: Request,
env: Env,
_ctx: ExecutionContext,
context: RouteContext
): Promise<ControllerResponse<ApiResponse<UserSecretsListData>>> {
try {
const user = context.user!;
const stub = this.getUserSecretsStub(env, user.id);

const secrets = await stub.listSecrets();

return UserSecretsController.createSuccessResponse({ secrets });
} catch (error) {
this.logger.error('Error listing secrets:', error);
return UserSecretsController.createErrorResponse<UserSecretsListData>(
'Failed to list secrets',
500
);
}
}

/**
* Store a new secret
* POST /api/user-secrets
*/
static async storeSecret(
request: Request,
env: Env,
_ctx: ExecutionContext,
context: RouteContext
): Promise<ControllerResponse<ApiResponse<UserSecretStoreData>>> {
try {
const user = context.user!;
const stub = this.getUserSecretsStub(env, user.id);

const bodyResult = await UserSecretsController.parseJsonBody<StoreSecretRequest>(request);

if (!bodyResult.success) {
return bodyResult.response! as ControllerResponse<ApiResponse<UserSecretStoreData>>;
}

const secret = await stub.storeSecret(bodyResult.data!);

if (!secret) {
return UserSecretsController.createErrorResponse<UserSecretStoreData>(
'Validation failed: Invalid secret data',
400
);
}

return UserSecretsController.createSuccessResponse({
secret,
message: 'Secret stored successfully'
});
} catch (error) {
this.logger.error('Error storing secret:', error);
return UserSecretsController.createErrorResponse<UserSecretStoreData>(
error instanceof Error ? error.message : 'Failed to store secret',
500
);
}
}

/**
* Get decrypted secret value
* GET /api/user-secrets/:secretId/value
*/
static async getSecretValue(
_request: Request,
env: Env,
_ctx: ExecutionContext,
context: RouteContext
): Promise<ControllerResponse<ApiResponse<UserSecretValueData>>> {
try {
const user = context.user!;
const secretId = context.pathParams.secretId;

if (!secretId) {
return UserSecretsController.createErrorResponse<UserSecretValueData>(
'Secret ID is required',
400
);
}

const stub = this.getUserSecretsStub(env, user.id);

const result = await stub.getSecretValue(secretId);

if (!result) {
return UserSecretsController.createErrorResponse<UserSecretValueData>(
'Secret not found or has expired',
404
);
}

return UserSecretsController.createSuccessResponse(result);
} catch (error) {
this.logger.error('Error getting secret value:', error);
return UserSecretsController.createErrorResponse<UserSecretValueData>(
'Failed to get secret value',
500
);
}
}

/**
* Update secret
* PATCH /api/user-secrets/:secretId
*/
static async updateSecret(
request: Request,
env: Env,
_ctx: ExecutionContext,
context: RouteContext
): Promise<ControllerResponse<ApiResponse<UserSecretUpdateData>>> {
try {
const user = context.user!;
const secretId = context.pathParams.secretId;

if (!secretId) {
return UserSecretsController.createErrorResponse<UserSecretUpdateData>(
'Secret ID is required',
400
);
}

const bodyResult = await UserSecretsController.parseJsonBody<UpdateSecretRequest>(request);

if (!bodyResult.success) {
return bodyResult.response! as ControllerResponse<ApiResponse<UserSecretUpdateData>>;
}

const stub = this.getUserSecretsStub(env, user.id);

const secret = await stub.updateSecret(secretId, bodyResult.data!);

if (!secret) {
return UserSecretsController.createErrorResponse<UserSecretUpdateData>(
'Secret not found or validation failed',
404
);
}

return UserSecretsController.createSuccessResponse({
secret,
message: 'Secret updated successfully'
});
} catch (error) {
this.logger.error('Error updating secret:', error);
return UserSecretsController.createErrorResponse<UserSecretUpdateData>(
'Failed to update secret',
500
);
}
}

/**
* Delete a secret (soft delete)
* DELETE /api/user-secrets/:secretId
*/
static async deleteSecret(
_request: Request,
env: Env,
_ctx: ExecutionContext,
context: RouteContext
): Promise<ControllerResponse<ApiResponse<UserSecretDeleteData>>> {
try {
const user = context.user!;
const secretId = context.pathParams.secretId;

if (!secretId) {
return UserSecretsController.createErrorResponse<UserSecretDeleteData>(
'Secret ID is required',
400
);
}

const stub = this.getUserSecretsStub(env, user.id);

const deleted = await stub.deleteSecret(secretId);

if (!deleted) {
return UserSecretsController.createErrorResponse<UserSecretDeleteData>(
'Secret not found',
404
);
}

return UserSecretsController.createSuccessResponse({
message: 'Secret deleted successfully'
});
} catch (error) {
this.logger.error('Error deleting secret:', error);
return UserSecretsController.createErrorResponse<UserSecretDeleteData>(
'Failed to delete secret',
500
);
}
}
}
26 changes: 26 additions & 0 deletions worker/api/controllers/user-secrets/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* User Secrets Controller Types
*/

export interface UserSecretsListData {
secrets: unknown[];
}

export interface UserSecretStoreData {
secret: unknown;
message: string;
}

export interface UserSecretValueData {
value: string;
metadata: unknown;
}

export interface UserSecretUpdateData {
secret: unknown;
message: string;
}

export interface UserSecretDeleteData {
message: string;
}
10 changes: 7 additions & 3 deletions worker/api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { setupAppRoutes } from './appRoutes';
import { setupUserRoutes } from './userRoutes';
import { setupStatsRoutes } from './statsRoutes';
import { setupAnalyticsRoutes } from './analyticsRoutes';
import { setupSecretsRoutes } from './secretsRoutes';
// import { setupSecretsRoutes } from './secretsRoutes';
// import { setupUserSecretsRoutes } from './userSecretsRoutes';
import { setupModelConfigRoutes } from './modelConfigRoutes';
import { setupModelProviderRoutes } from './modelProviderRoutes';
import { setupGitHubExporterRoutes } from './githubExporterRoutes';
Expand Down Expand Up @@ -44,8 +45,11 @@ export function setupRoutes(app: Hono<AppEnv>): void {
// AI Gateway Analytics routes
setupAnalyticsRoutes(app);

// Secrets management routes
setupSecretsRoutes(app);
// // Secrets management routes (legacy D1-based)
// setupSecretsRoutes(app);

// // User secrets routes (new DO-backed)
// setupUserSecretsRoutes(app);

// Model configuration and provider keys routes
setupModelConfigRoutes(app);
Expand Down
Loading