diff --git a/apps/desktop/src/renderer/src/service/request.ts b/apps/desktop/src/renderer/src/service/request.ts new file mode 100644 index 00000000..ee4c27a3 --- /dev/null +++ b/apps/desktop/src/renderer/src/service/request.ts @@ -0,0 +1,72 @@ +import { useAuthStore } from '../stores/auth' + +// Backend API host — change this when switching environments +const API_HOST = 'https://api-dev.copilothub.ai' + +/** + * Fetch request wrapper for desktop app. + * Attaches sid, device-id, and os-type headers automatically. + */ +export async function request(url: string, options: RequestInit = {}): Promise { + const sid = useAuthStore.getState().sid + const deviceIdHeader = await window.electronAPI.auth.getDeviceIdHeader() + + const config: RequestInit = { + ...options, + headers: { + 'Content-Type': 'application/json', + 'os-type': '3', + ...(deviceIdHeader && { 'device-id': deviceIdHeader }), + ...(sid && { sid }), + ...options.headers, + }, + } + + const response = await fetch(`${API_HOST}${url}`, config) + + let data: T + const contentType = response.headers.get('content-type') + if (contentType?.includes('application/json')) { + data = await response.json() + } else { + const text = await response.text() + data = { message: text || response.statusText } as T + } + + if (!response.ok) { + console.error('API Error:', { + status: response.status, + url, + data, + }) + throw new Error( + (data as { errMsg?: string; message?: string })?.errMsg || + (data as { message?: string })?.message || + `Request failed with status ${response.status}`, + ) + } + + return data +} + +// GET request +export function get(url: string, params?: Record) { + const filteredParams = params + ? Object.fromEntries( + Object.entries(params).filter(([, v]) => v !== undefined && v !== null), + ) + : undefined + const queryString = + filteredParams && Object.keys(filteredParams).length > 0 + ? `?${new URLSearchParams(filteredParams as Record).toString()}` + : '' + return request(url + queryString, { method: 'GET' }) +} + +// POST request +export function post(url: string, data?: unknown) { + return request(url, { + method: 'POST', + body: JSON.stringify(data), + }) +} diff --git a/packages/core/src/agent/tools/data/finance/api.ts b/packages/core/src/agent/tools/data/finance/api.ts index e869f655..ab0a3a22 100644 --- a/packages/core/src/agent/tools/data/finance/api.ts +++ b/packages/core/src/agent/tools/data/finance/api.ts @@ -1,32 +1,15 @@ /** * Financial Datasets API client. * - * Base URL: https://api.financialdatasets.ai - * Auth: X-API-KEY header + * Proxied through api-dev.copilothub.ai with auth headers (sid / device-id / os-type). * All endpoints use GET with query parameters. */ -import { credentialManager } from "../../../credentials.js"; +import { API_BASE_URL, getAuthHeaders } from "../../../../hub/api-client.js"; -const BASE_URL = "https://api.financialdatasets.ai"; +const PATH_PREFIX = "/api/v1/financial"; const TIMEOUT_MS = 30_000; -function getApiKey(): string { - // 1. credentials.json5 → tools.data.apiKey (preferred) - const toolConfig = credentialManager.getToolConfig("data"); - if (toolConfig?.apiKey) return toolConfig.apiKey; - - // 2. Fallback: env var (skills.env.json5 or process.env) - const envKey = credentialManager.getEnv("FINANCIAL_DATASETS_API_KEY"); - if (envKey) return envKey; - - throw new Error( - "Financial Datasets API key not configured. " + - 'Set it in ~/.super-multica/credentials.json5 under tools.data.apiKey, ' + - "or set FINANCIAL_DATASETS_API_KEY in ~/.super-multica/skills.env.json5.", - ); -} - /** * Fetch data from the Financial Datasets API. * @@ -39,9 +22,9 @@ export async function financeFetch>( params: Record, signal?: AbortSignal, ): Promise<{ data: T; url: string }> { - const apiKey = getApiKey(); + const authHeaders = getAuthHeaders("to use financial data tools"); - const url = new URL(path, BASE_URL); + const url = new URL(PATH_PREFIX + path, API_BASE_URL); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { @@ -61,8 +44,8 @@ export async function financeFetch>( const res = await fetch(url.toString(), { method: "GET", headers: { - "X-API-KEY": apiKey, Accept: "application/json", + ...authHeaders, }, signal: combinedSignal, }); diff --git a/packages/core/src/agent/tools/web/web-search.ts b/packages/core/src/agent/tools/web/web-search.ts index 19cd1739..67558acc 100644 --- a/packages/core/src/agent/tools/web/web-search.ts +++ b/packages/core/src/agent/tools/web/web-search.ts @@ -1,9 +1,7 @@ -import { createHmac } from "node:crypto"; import { Type } from "@sinclair/typebox"; import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { v7 as uuidv7 } from "uuid"; -import { getHubId } from "../../../hub/hub-identity.js"; +import { API_BASE_URL, getAuthHeaders } from "../../../hub/api-client.js"; import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_TIMEOUT_SECONDS, @@ -16,8 +14,7 @@ import { import type { CacheEntry } from "./cache.js"; import { jsonResult, readStringParam } from "./param-helpers.js"; -const DEVV_SEARCH_ENDPOINT = "https://api-dev.copilothub.ai/web-search"; -const SIGNING_KEY = "019c2d34-e8b2-75da-ace5-99f887c090c9"; +const WEB_SEARCH_PATH = "/api/v1/web-search"; const SEARCH_CACHE = new Map>>(); @@ -51,15 +48,6 @@ export type WebSearchResult = { }>; }; -function buildReqId(): string { - const hubId = getHubId(); - const nonce = uuidv7(); - const timestamp = Math.floor(Date.now() / 1000); - const message = `${hubId}.${nonce}.${timestamp}`; - const signature = createHmac("sha256", SIGNING_KEY).update(message).digest("hex"); - return `${signature}.${hubId}.${nonce}.${timestamp}`; -} - async function runDevvSearch(params: { query: string; timeoutSeconds: number; @@ -71,16 +59,21 @@ async function runDevvSearch(params: { snippet: string; }>; }> { - const res = await fetch(DEVV_SEARCH_ENDPOINT, { + const authHeaders = getAuthHeaders("to use web search"); + + const res = await fetch(`${API_BASE_URL}${WEB_SEARCH_PATH}`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ q: params.query, reqId: buildReqId() }), + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify({ q: params.query }), signal: withTimeout(undefined, params.timeoutSeconds * 1000), }); if (!res.ok) { const detail = await readResponseText(res); - throw new Error(`Devv Search API error (${res.status}): ${detail || res.statusText}`); + throw new Error(`Web Search API error (${res.status}): ${detail || res.statusText}`); } const data = (await res.json()) as DevvSearchResponse; diff --git a/packages/core/src/hub/api-client.ts b/packages/core/src/hub/api-client.ts new file mode 100644 index 00000000..273ec258 --- /dev/null +++ b/packages/core/src/hub/api-client.ts @@ -0,0 +1,25 @@ +import { getLocalAuth } from "./auth-store.js"; + +export const API_BASE_URL = "https://api-dev.copilothub.ai"; + +/** + * Return auth headers for the proxy API. + * Throws if the user is not logged in. + * + * @param context - Optional feature name appended to the error message + * (e.g. "to use web search"). + */ +export function getAuthHeaders(context?: string): Record { + const auth = getLocalAuth(); + if (!auth) { + const suffix = context ? ` ${context}` : ""; + throw new Error( + `Not logged in. Please sign in via the Desktop app${suffix}.`, + ); + } + return { + sid: auth.sid, + "device-id": auth.deviceId, + "os-type": "3", + }; +} diff --git a/packages/core/src/hub/auth-store.ts b/packages/core/src/hub/auth-store.ts new file mode 100644 index 00000000..ff2342ba --- /dev/null +++ b/packages/core/src/hub/auth-store.ts @@ -0,0 +1,34 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { DATA_DIR } from "@multica/utils"; + +const AUTH_FILE_PATH = join(DATA_DIR, "auth.json"); + +export type LocalAuthData = { sid: string; deviceId: string }; + +/** + * Read sid and deviceId from ~/.super-multica/auth.json. + * Returns null if the file is missing, unreadable, or incomplete. + */ +export function getLocalAuth(): LocalAuthData | null { + try { + const raw = readFileSync(AUTH_FILE_PATH, "utf8").trim(); + if (!raw) return null; + + const data = JSON.parse(raw); + if ( + typeof data !== "object" || + data === null || + typeof data.sid !== "string" || + !data.sid || + typeof data.deviceId !== "string" || + !data.deviceId + ) { + return null; + } + + return { sid: data.sid, deviceId: data.deviceId }; + } catch { + return null; + } +} diff --git a/packages/core/src/hub/index.ts b/packages/core/src/hub/index.ts index 0b47e705..93c15b7a 100644 --- a/packages/core/src/hub/index.ts +++ b/packages/core/src/hub/index.ts @@ -1,4 +1,6 @@ export { Hub } from "./hub.js"; export type { MessageSource, InboundMessageEvent } from "./hub.js"; export { getHubId } from "./hub-identity.js"; +export { getLocalAuth, type LocalAuthData } from "./auth-store.js"; +export { API_BASE_URL, getAuthHeaders } from "./api-client.js"; export type { HubOptions } from "./types.js";