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
12 changes: 10 additions & 2 deletions apps/server/src/api/routes/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/api/routes/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export async function createHttpServer(config: HttpServerConfig) {
createSdkRoutes({
port,
browserosId,
hmacSecret: config.codegenHmacSecret,
}),
)
.route(
Expand All @@ -92,6 +93,8 @@ export async function createHttpServer(config: HttpServerConfig) {
port,
tempDir,
codegenServiceUrl: config.codegenServiceUrl,
browserosId,
hmacSecret: config.codegenHmacSecret,
}),
)

Expand Down
45 changes: 39 additions & 6 deletions apps/server/src/api/services/graph-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, string> = {}

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) {
Expand Down Expand Up @@ -201,13 +220,27 @@ export class GraphService {
body: { query: string },
signal?: AbortSignal,
): Promise<Response> {
const bodyStr = JSON.stringify(body)
const headers: Record<string, string> = {
'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,
})

Expand Down
40 changes: 32 additions & 8 deletions apps/server/src/api/services/sdk/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
Expand All @@ -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<unknown> {
const { instruction, schema, content, context } = options

const bodyStr = JSON.stringify({
instruction,
schema,
content,
context,
})

const headers: Record<string, string> = {
'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) {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/api/services/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type VerifyRequest = z.infer<typeof VerifyRequestSchema>
export interface SdkDeps {
port: number
browserosId?: string
hmacSecret?: string
}

export interface ActiveTab {
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions apps/server/src/api/utils/codegen-auth.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -41,6 +42,7 @@ type PartialConfig = {
executionDir?: string
mcpAllowRemote?: boolean
codegenServiceUrl?: string
codegenHmacSecret?: string
instanceClientId?: string
instanceInstallId?: string
instanceBrowserosVersion?: string
Expand Down Expand Up @@ -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,
})
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down