From 309c27b4dd72e1bd66908d391c3a109d305dcd87 Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 16:31:43 +0800 Subject: [PATCH 1/7] feat(desktop): add API client with auth headers Add fetch wrapper for desktop renderer to call Multica backend REST API. Attaches sid, device-id, and os-type headers automatically using useAuthStore and electronAPI.auth.getDeviceIdHeader(). Co-Authored-By: Claude Opus 4.6 --- .../src/renderer/src/service/request.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 apps/desktop/src/renderer/src/service/request.ts 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), + }) +} From ec413effb92f1a8a844302ae452b90d367844279 Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:25:37 +0800 Subject: [PATCH 2/7] feat(core): add auth-store module to read local auth data Reads sid and deviceId from ~/.super-multica/auth.json for use by tools that need authenticated API access. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/hub/auth-store.ts | 34 +++++++++++++++++++++++++++++ packages/core/src/hub/index.ts | 1 + 2 files changed, 35 insertions(+) create mode 100644 packages/core/src/hub/auth-store.ts 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..671f72bd 100644 --- a/packages/core/src/hub/index.ts +++ b/packages/core/src/hub/index.ts @@ -1,4 +1,5 @@ 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 type { HubOptions } from "./types.js"; From 92a137414d77481848eddce53e7e82a7e1dec666 Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:25:43 +0800 Subject: [PATCH 3/7] feat(web-search): replace HMAC signing with auth headers Migrate web_search tool from HMAC-SHA256 reqId signing to sid/device-id/os-type auth headers, matching the desktop API client pattern. Update endpoint to /api/v1/web-search. Co-Authored-By: Claude Opus 4.6 --- .../core/src/agent/tools/web/web-search.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/core/src/agent/tools/web/web-search.ts b/packages/core/src/agent/tools/web/web-search.ts index 19cd1739..81d045eb 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 { getLocalAuth } from "../../../hub/auth-store.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_ENDPOINT = "https://api-dev.copilothub.ai/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,26 @@ async function runDevvSearch(params: { snippet: string; }>; }> { - const res = await fetch(DEVV_SEARCH_ENDPOINT, { + const auth = getLocalAuth(); + if (!auth) { + throw new Error("Not logged in. Please sign in via the Desktop app to use web search."); + } + + const res = await fetch(WEB_SEARCH_ENDPOINT, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ q: params.query, reqId: buildReqId() }), + headers: { + "Content-Type": "application/json", + sid: auth.sid, + "device-id": auth.deviceId, + "os-type": "3", + }, + 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; From 77953a611d605da4d8bcafdc368ce6be3bd15d29 Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:52:46 +0800 Subject: [PATCH 4/7] feat(finance): replace API key auth with auth headers Route all financial data requests through api-dev.copilothub.ai/api/v1/financial proxy and authenticate via sid/device-id/os-type headers instead of X-API-KEY. Co-Authored-By: Claude Opus 4.6 --- .../core/src/agent/tools/data/finance/api.ts | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/core/src/agent/tools/data/finance/api.ts b/packages/core/src/agent/tools/data/finance/api.ts index e869f655..8a2b01de 100644 --- a/packages/core/src/agent/tools/data/finance/api.ts +++ b/packages/core/src/agent/tools/data/finance/api.ts @@ -1,32 +1,16 @@ /** * 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 { getLocalAuth } from "../../../../hub/auth-store.js"; -const BASE_URL = "https://api.financialdatasets.ai"; +const BASE_URL = "https://api-dev.copilothub.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 +23,14 @@ export async function financeFetch>( params: Record, signal?: AbortSignal, ): Promise<{ data: T; url: string }> { - const apiKey = getApiKey(); + const auth = getLocalAuth(); + if (!auth) { + throw new Error( + "Not logged in. Please sign in via the Desktop app to use financial data tools.", + ); + } - const url = new URL(path, BASE_URL); + const url = new URL(PATH_PREFIX + path, BASE_URL); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { @@ -61,8 +50,10 @@ export async function financeFetch>( const res = await fetch(url.toString(), { method: "GET", headers: { - "X-API-KEY": apiKey, Accept: "application/json", + sid: auth.sid, + "device-id": auth.deviceId, + "os-type": "3", }, signal: combinedSignal, }); From f76312511df384ccfd7a833a0994c70b4978d73c Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:56:20 +0800 Subject: [PATCH 5/7] feat(core): add shared api-client module for auth headers Extract API_BASE_URL and getAuthHeaders() into a reusable module so that tools don't duplicate base URL and auth header construction. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/hub/api-client.ts | 21 +++++++++++++++++++++ packages/core/src/hub/index.ts | 1 + 2 files changed, 22 insertions(+) create mode 100644 packages/core/src/hub/api-client.ts diff --git a/packages/core/src/hub/api-client.ts b/packages/core/src/hub/api-client.ts new file mode 100644 index 00000000..a622c67a --- /dev/null +++ b/packages/core/src/hub/api-client.ts @@ -0,0 +1,21 @@ +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. + */ +export function getAuthHeaders(): Record { + const auth = getLocalAuth(); + if (!auth) { + throw new Error( + "Not logged in. Please sign in via the Desktop app.", + ); + } + return { + sid: auth.sid, + "device-id": auth.deviceId, + "os-type": "3", + }; +} diff --git a/packages/core/src/hub/index.ts b/packages/core/src/hub/index.ts index 671f72bd..93c15b7a 100644 --- a/packages/core/src/hub/index.ts +++ b/packages/core/src/hub/index.ts @@ -2,4 +2,5 @@ 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"; From 53cffe923b4b28bd2449f0034cf714221f4d3f5e Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:56:26 +0800 Subject: [PATCH 6/7] refactor(tools): use shared api-client for auth headers Replace duplicated getLocalAuth() + manual header construction in finance/api.ts and web-search.ts with the shared getAuthHeaders() and API_BASE_URL from hub/api-client. Co-Authored-By: Claude Opus 4.6 --- .../core/src/agent/tools/data/finance/api.ts | 16 ++++------------ packages/core/src/agent/tools/web/web-search.ts | 15 +++++---------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/packages/core/src/agent/tools/data/finance/api.ts b/packages/core/src/agent/tools/data/finance/api.ts index 8a2b01de..367927a1 100644 --- a/packages/core/src/agent/tools/data/finance/api.ts +++ b/packages/core/src/agent/tools/data/finance/api.ts @@ -5,9 +5,8 @@ * All endpoints use GET with query parameters. */ -import { getLocalAuth } from "../../../../hub/auth-store.js"; +import { API_BASE_URL, getAuthHeaders } from "../../../../hub/api-client.js"; -const BASE_URL = "https://api-dev.copilothub.ai"; const PATH_PREFIX = "/api/v1/financial"; const TIMEOUT_MS = 30_000; @@ -23,14 +22,9 @@ export async function financeFetch>( params: Record, signal?: AbortSignal, ): Promise<{ data: T; url: string }> { - const auth = getLocalAuth(); - if (!auth) { - throw new Error( - "Not logged in. Please sign in via the Desktop app to use financial data tools.", - ); - } + const authHeaders = getAuthHeaders(); - const url = new URL(PATH_PREFIX + 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)) { @@ -51,9 +45,7 @@ export async function financeFetch>( method: "GET", headers: { Accept: "application/json", - sid: auth.sid, - "device-id": auth.deviceId, - "os-type": "3", + ...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 81d045eb..3b508b26 100644 --- a/packages/core/src/agent/tools/web/web-search.ts +++ b/packages/core/src/agent/tools/web/web-search.ts @@ -1,7 +1,7 @@ import { Type } from "@sinclair/typebox"; import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { getLocalAuth } from "../../../hub/auth-store.js"; +import { API_BASE_URL, getAuthHeaders } from "../../../hub/api-client.js"; import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_TIMEOUT_SECONDS, @@ -14,7 +14,7 @@ import { import type { CacheEntry } from "./cache.js"; import { jsonResult, readStringParam } from "./param-helpers.js"; -const WEB_SEARCH_ENDPOINT = "https://api-dev.copilothub.ai/api/v1/web-search"; +const WEB_SEARCH_PATH = "/api/v1/web-search"; const SEARCH_CACHE = new Map>>(); @@ -59,18 +59,13 @@ async function runDevvSearch(params: { snippet: string; }>; }> { - const auth = getLocalAuth(); - if (!auth) { - throw new Error("Not logged in. Please sign in via the Desktop app to use web search."); - } + const authHeaders = getAuthHeaders(); - const res = await fetch(WEB_SEARCH_ENDPOINT, { + const res = await fetch(`${API_BASE_URL}${WEB_SEARCH_PATH}`, { method: "POST", headers: { "Content-Type": "application/json", - sid: auth.sid, - "device-id": auth.deviceId, - "os-type": "3", + ...authHeaders, }, body: JSON.stringify({ q: params.query }), signal: withTimeout(undefined, params.timeoutSeconds * 1000), From 882e34c49128da197ff1133f0ce02529c34fcb1d Mon Sep 17 00:00:00 2001 From: yushen Date: Fri, 13 Feb 2026 18:58:27 +0800 Subject: [PATCH 7/7] fix(core): preserve context-specific auth error messages Add optional context parameter to getAuthHeaders() so callers can provide feature-specific suffixes (e.g. "to use web search") in the not-logged-in error message, restoring the original behavior. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/tools/data/finance/api.ts | 2 +- packages/core/src/agent/tools/web/web-search.ts | 2 +- packages/core/src/hub/api-client.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/agent/tools/data/finance/api.ts b/packages/core/src/agent/tools/data/finance/api.ts index 367927a1..ab0a3a22 100644 --- a/packages/core/src/agent/tools/data/finance/api.ts +++ b/packages/core/src/agent/tools/data/finance/api.ts @@ -22,7 +22,7 @@ export async function financeFetch>( params: Record, signal?: AbortSignal, ): Promise<{ data: T; url: string }> { - const authHeaders = getAuthHeaders(); + const authHeaders = getAuthHeaders("to use financial data tools"); const url = new URL(PATH_PREFIX + path, API_BASE_URL); for (const [key, value] of Object.entries(params)) { diff --git a/packages/core/src/agent/tools/web/web-search.ts b/packages/core/src/agent/tools/web/web-search.ts index 3b508b26..67558acc 100644 --- a/packages/core/src/agent/tools/web/web-search.ts +++ b/packages/core/src/agent/tools/web/web-search.ts @@ -59,7 +59,7 @@ async function runDevvSearch(params: { snippet: string; }>; }> { - const authHeaders = getAuthHeaders(); + const authHeaders = getAuthHeaders("to use web search"); const res = await fetch(`${API_BASE_URL}${WEB_SEARCH_PATH}`, { method: "POST", diff --git a/packages/core/src/hub/api-client.ts b/packages/core/src/hub/api-client.ts index a622c67a..273ec258 100644 --- a/packages/core/src/hub/api-client.ts +++ b/packages/core/src/hub/api-client.ts @@ -5,12 +5,16 @@ 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(): Record { +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.", + `Not logged in. Please sign in via the Desktop app${suffix}.`, ); } return {