///
/**
* Qiaoqiao (敲敲) backend agent authentication.
*
* When the clawdbot-qiaoqiao plugin is running with Qiaoqiao credentials,
* this module verifies the agent identity against the Qiaoqiao backend
* before allowing messages to be processed.
*/
export type QiaoqiaoAgentInfo = {
agentId: string;
appId: string;
username: string;
};
export type QiaoqiaoAuthConfig = {
appId: string;
appSecret: string;
apiBase?: string;
};
const DEFAULT_QIAOQIAO_API_BASE = "https://qiaoqiao.social/api";
function resolveApiBase(preferred?: string): string {
const raw = (preferred || DEFAULT_QIAOQIAO_API_BASE).trim();
if (!raw) return DEFAULT_QIAOQIAO_API_BASE;
try {
const url = new URL(raw);
const normalizedPath = url.pathname.replace(/\/+$/, "");
if (!normalizedPath) {
url.pathname = "/api";
} else {
url.pathname = normalizedPath;
}
return url.toString().replace(/\/+$/, "");
} catch {
return raw.replace(/\/+$/, "");
}
}
function resolveAgentVerifyUrl(apiBase: string): string {
const normalized = resolveApiBase(apiBase);
if (normalized.endsWith("/api")) {
return `${normalized}/ws/verify-agent`;
}
return `${normalized}/api/ws/verify-agent`;
}
/**
* Normalize Qiaoqiao credentials from explicit config.
* Returns null if credentials are incomplete.
*/
export function resolveQiaoqiaoCredentials(preferred?: Partial | null): QiaoqiaoAuthConfig | null {
const appId = preferred?.appId?.trim();
const appSecret = preferred?.appSecret?.trim();
if (!appId || !appSecret) return null;
return {
appId,
appSecret,
apiBase: resolveApiBase(preferred?.apiBase),
};
}
/**
* Verify Qiaoqiao agent credentials against the backend.
* Returns agent info on success, throws on failure.
*/
export async function verifyQiaoqiaoAgent(
config: QiaoqiaoAuthConfig,
log: (msg: string) => void = console.log,
): Promise {
const appId = config.appId?.trim();
const appSecret = config.appSecret?.trim();
if (!appId || !appSecret) {
throw new Error("[qiaoqiao] Agent verification requires both appId and appSecret");
}
const apiBase = resolveApiBase(config.apiBase);
const url = resolveAgentVerifyUrl(apiBase);
log(`[qiaoqiao] Verifying agent identity: appId=${appId} url=${url}`);
const res = await fetch(url, {
method: "GET",
headers: {
"X-App-ID": appId,
"X-App-Secret": appSecret,
"Content-Type": "application/json",
},
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(
`[qiaoqiao] Agent verification failed (HTTP ${res.status}): ${body}`,
);
}
const data = (await res.json()) as {
success: boolean;
data?: QiaoqiaoAgentInfo;
error?: string;
};
if (!data.success || !data.data) {
throw new Error(`[qiaoqiao] Agent verification rejected: ${data.error ?? "unknown error"}`);
}
if (data.data.appId !== appId) {
throw new Error(
`[qiaoqiao] Agent verification appId mismatch: expected=${appId}, actual=${data.data.appId}`,
);
}
log(`[qiaoqiao] Agent verified: ${data.data.username} (${data.data.agentId})`);
return data.data;
}
/**
* Notify the Qiaoqiao backend when a Qiaoqiao conversation message is processed.
* This allows the backend to track conversations tied to this agent.
*/
export async function notifyQiaoqiaoMessage(
config: QiaoqiaoAuthConfig,
payload: {
agentId: string;
senderId: string;
chatId: string;
content: string;
},
): Promise {
const { appId, appSecret } = config;
const apiBase = resolveApiBase(config.apiBase);
const url = `${apiBase}/agent/qiaoqiao-message`;
try {
await fetch(url, {
method: "POST",
headers: {
"X-App-ID": appId,
"X-App-Secret": appSecret,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
} catch {
// Non-critical: just log, don't fail message processing
}
}