diff --git a/apps/server/src/api/routes/graph.ts b/apps/server/src/api/routes/graph.ts index edc2fd7b..761b4d16 100644 --- a/apps/server/src/api/routes/graph.ts +++ b/apps/server/src/api/routes/graph.ts @@ -67,16 +67,24 @@ interface GraphRouteDeps { port: number tempDir?: string codegenServiceUrl?: string + browserosId?: string + hmacSecret?: string } export function createGraphRoutes(deps: GraphRouteDeps) { - const { port, codegenServiceUrl } = deps + const { port, codegenServiceUrl, browserosId, hmacSecret } = deps const serverUrl = `http://127.0.0.1:${port}` const tempDir = deps.tempDir || PATHS.DEFAULT_TEMP_DIR const graphService = codegenServiceUrl - ? new GraphService({ codegenServiceUrl, serverUrl, tempDir }) + ? new GraphService({ + codegenServiceUrl, + serverUrl, + tempDir, + browserosId, + hmacSecret, + }) : null // Chain route definitions for proper Hono RPC type inference diff --git a/apps/server/src/api/routes/sdk.ts b/apps/server/src/api/routes/sdk.ts index 87defcc0..b28b2627 100644 --- a/apps/server/src/api/routes/sdk.ts +++ b/apps/server/src/api/routes/sdk.ts @@ -30,13 +30,13 @@ import { VerifyService } from '../services/sdk/verify' import type { Env } from '../types' export function createSdkRoutes(deps: SdkDeps) { - const { port, browserosId } = deps + const { port, browserosId, hmacSecret } = deps const mcpServerUrl = `http://127.0.0.1:${port}/mcp` const browserService = new BrowserService(mcpServerUrl) const chatService = new ChatService(port) - const extractService = new ExtractService() + const extractService = new ExtractService({ browserosId, hmacSecret }) const verifyService = new VerifyService() // Chain route definitions for proper Hono RPC type inference diff --git a/apps/server/src/api/server.ts b/apps/server/src/api/server.ts index 801befe6..ef6f4364 100644 --- a/apps/server/src/api/server.ts +++ b/apps/server/src/api/server.ts @@ -84,6 +84,7 @@ export async function createHttpServer(config: HttpServerConfig) { createSdkRoutes({ port, browserosId, + hmacSecret: config.codegenHmacSecret, }), ) .route( @@ -92,6 +93,8 @@ export async function createHttpServer(config: HttpServerConfig) { port, tempDir, codegenServiceUrl: config.codegenServiceUrl, + browserosId, + hmacSecret: config.codegenHmacSecret, }), ) diff --git a/apps/server/src/api/services/graph-service.ts b/apps/server/src/api/services/graph-service.ts index cff690fa..9d000e4f 100644 --- a/apps/server/src/api/services/graph-service.ts +++ b/apps/server/src/api/services/graph-service.ts @@ -16,11 +16,17 @@ import { type RunGraphRequest, type WorkflowGraph, } from '../types' +import { + createCodegenAuthHeaders, + extractPathFromUrl, +} from '../utils/codegen-auth' export interface GraphServiceDeps { codegenServiceUrl: string serverUrl: string tempDir: string + browserosId?: string + hmacSecret?: string } interface SessionState { @@ -77,7 +83,20 @@ export class GraphService { logger.debug('Fetching graph from codegen service', { url, sessionId }) try { - const response = await fetch(url) + const headers: Record = {} + + if (this.deps.hmacSecret && this.deps.browserosId) { + const path = extractPathFromUrl(url) + const authHeaders = createCodegenAuthHeaders( + { hmacSecret: this.deps.hmacSecret, userId: this.deps.browserosId }, + 'GET', + path, + '', + ) + Object.assign(headers, authHeaders) + } + + const response = await fetch(url, { headers }) if (!response.ok) { if (response.status === 404) { @@ -201,13 +220,27 @@ export class GraphService { body: { query: string }, signal?: AbortSignal, ): Promise { + const bodyStr = JSON.stringify(body) + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + } + + if (this.deps.hmacSecret && this.deps.browserosId) { + const path = extractPathFromUrl(url) + const authHeaders = createCodegenAuthHeaders( + { hmacSecret: this.deps.hmacSecret, userId: this.deps.browserosId }, + method, + path, + bodyStr, + ) + Object.assign(headers, authHeaders) + } + const response = await fetch(url, { method, - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream', - }, - body: JSON.stringify(body), + headers, + body: bodyStr, signal, }) diff --git a/apps/server/src/api/services/sdk/extract.ts b/apps/server/src/api/services/sdk/extract.ts index 3da1aa56..e8d7f0fc 100644 --- a/apps/server/src/api/services/sdk/extract.ts +++ b/apps/server/src/api/services/sdk/extract.ts @@ -7,8 +7,14 @@ */ import { EXTERNAL_URLS } from '@browseros/shared/constants/urls' +import { createCodegenAuthHeaders } from '../../utils/codegen-auth' import { SdkError } from './types' +export interface ExtractServiceDeps { + browserosId?: string + hmacSecret?: string +} + export interface ExtractOptions { instruction: string schema: Record @@ -22,23 +28,41 @@ export interface ExtractResult { export class ExtractService { private serviceUrl: string + private deps: ExtractServiceDeps - constructor() { + constructor(deps: ExtractServiceDeps = {}) { this.serviceUrl = `${EXTERNAL_URLS.CODEGEN_SERVICE}/api/extract` + this.deps = deps } async extract(options: ExtractOptions): Promise { const { instruction, schema, content, context } = options + const bodyStr = JSON.stringify({ + instruction, + schema, + content, + context, + }) + + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (this.deps.hmacSecret && this.deps.browserosId) { + const authHeaders = createCodegenAuthHeaders( + { hmacSecret: this.deps.hmacSecret, userId: this.deps.browserosId }, + 'POST', + '/api/extract', + bodyStr, + ) + Object.assign(headers, authHeaders) + } + const response = await fetch(this.serviceUrl, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - instruction, - schema, - content, - context, - }), + headers, + body: bodyStr, }) if (!response.ok) { diff --git a/apps/server/src/api/services/sdk/types.ts b/apps/server/src/api/services/sdk/types.ts index c61b01a3..3ece3ae2 100644 --- a/apps/server/src/api/services/sdk/types.ts +++ b/apps/server/src/api/services/sdk/types.ts @@ -47,6 +47,7 @@ export type VerifyRequest = z.infer export interface SdkDeps { port: number browserosId?: string + hmacSecret?: string } export interface ActiveTab { diff --git a/apps/server/src/api/types.ts b/apps/server/src/api/types.ts index 509845f3..ab86f369 100644 --- a/apps/server/src/api/types.ts +++ b/apps/server/src/api/types.ts @@ -83,8 +83,9 @@ export interface HttpServerConfig { tempDir?: string rateLimiter?: RateLimiter - // For Graph routes + // For Graph/SDK routes (codegen service) codegenServiceUrl?: string + codegenHmacSecret?: string } // Graph request schemas diff --git a/apps/server/src/api/utils/codegen-auth.ts b/apps/server/src/api/utils/codegen-auth.ts new file mode 100644 index 00000000..c6eed583 --- /dev/null +++ b/apps/server/src/api/utils/codegen-auth.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * HMAC authentication for codegen service requests. + */ + +import { createHmac } from 'node:crypto' + +export interface CodegenAuthConfig { + hmacSecret: string + userId: string +} + +export interface CodegenAuthHeaders { + 'X-BrowserOS-User-Id': string + 'X-BrowserOS-Timestamp': string + 'X-BrowserOS-Signature': string +} + +/** + * Compute HMAC-SHA256 signature for codegen service authentication. + * + * Signature format: HMAC-SHA256(secret, "${METHOD}:${PATH}:${TIMESTAMP}:${USER_ID}:${BODY}") + */ +export function computeCodegenSignature( + hmacSecret: string, + method: string, + path: string, + timestamp: string, + userId: string, + body: string, +): string { + const message = `${method}:${path}:${timestamp}:${userId}:${body}` + return createHmac('sha256', hmacSecret).update(message).digest('hex') +} + +/** + * Create authentication headers for codegen service requests. + * + * @param config - Auth configuration with hmacSecret and userId + * @param method - HTTP method (GET, POST, PUT, DELETE) + * @param path - Request path (e.g., '/api/code', '/api/code/abc123') + * @param body - Request body as string (empty string for GET requests) + * @returns Headers object with authentication headers + */ +export function createCodegenAuthHeaders( + config: CodegenAuthConfig, + method: string, + path: string, + body: string = '', +): CodegenAuthHeaders { + const timestamp = Date.now().toString() + const signature = computeCodegenSignature( + config.hmacSecret, + method, + path, + timestamp, + config.userId, + body, + ) + + return { + 'X-BrowserOS-User-Id': config.userId, + 'X-BrowserOS-Timestamp': timestamp, + 'X-BrowserOS-Signature': signature, + } +} + +/** + * Extract the path from a full URL for signature computation. + */ +export function extractPathFromUrl(url: string): string { + const urlObj = new URL(url) + return urlObj.pathname +} diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index e949efee..f91b8e3d 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -24,6 +24,7 @@ export const ServerConfigSchema = z.object({ executionDir: z.string(), mcpAllowRemote: z.boolean(), codegenServiceUrl: z.string().optional(), + codegenHmacSecret: z.string().optional(), instanceClientId: z.string().optional(), instanceInstallId: z.string().optional(), instanceBrowserosVersion: z.string().optional(), @@ -41,6 +42,7 @@ type PartialConfig = { executionDir?: string mcpAllowRemote?: boolean codegenServiceUrl?: string + codegenHmacSecret?: string instanceClientId?: string instanceInstallId?: string instanceBrowserosVersion?: string @@ -280,6 +282,7 @@ function loadEnv(env: NodeJS.ProcessEnv): PartialConfig { ? resolvePath(env.BROWSEROS_EXECUTION_DIR, cwd) : undefined, codegenServiceUrl: env.CODEGEN_SERVICE_URL, + codegenHmacSecret: env.CODEGEN_HMAC_SECRET, instanceInstallId: env.BROWSEROS_INSTALL_ID, instanceClientId: env.BROWSEROS_CLIENT_ID, }) diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index dae8ad9b..d2bfd220 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -79,6 +79,7 @@ export class Application { tempDir: this.config.executionDir || this.config.resourcesDir, rateLimiter: new RateLimiter(this.getDb(), dailyRateLimit), codegenServiceUrl: this.config.codegenServiceUrl, + codegenHmacSecret: this.config.codegenHmacSecret, }) } catch (error) { this.handleStartupError('HTTP server', this.config.serverPort, error)