diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc2b32f8..e49fdc74a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b - macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b - macOS remote: route settings through gateway config and avoid local config reads in remote mode. +- Telegram: align token resolution for cron/agent/CLI sends (env/config/tokenFile) to prevent isolated delivery failures (#76). - Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl - Restart: use systemd on Linux (and report actual restart method) instead of always launchctl. - Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS. diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 55015f407..b9bacd362 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -51,6 +51,7 @@ function mockConfig( storePath: string, routingOverrides?: Partial>, agentOverrides?: Partial>, + telegramOverrides?: Partial>, ) { configSpy.mockReturnValue({ agent: { @@ -60,6 +61,7 @@ function mockConfig( }, session: { store: storePath, mainKey: "main" }, routing: routingOverrides ? { ...routingOverrides } : undefined, + telegram: telegramOverrides ? { ...telegramOverrides } : undefined, }); } @@ -198,4 +200,46 @@ describe("agentCommand", () => { expect(callArgs?.prompt).toBe("ping"); }); }); + + it("passes telegram token when delivering", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + mockConfig(home, store, undefined, undefined, { botToken: "t-1" }); + const deps = { + sendMessageWhatsApp: vi.fn(), + sendMessageTelegram: vi + .fn() + .mockResolvedValue({ messageId: "t1", chatId: "123" }), + sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), + }; + + const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; + process.env.TELEGRAM_BOT_TOKEN = ""; + try { + await agentCommand( + { + message: "hi", + to: "123", + deliver: true, + provider: "telegram", + }, + runtime, + deps, + ); + + expect(deps.sendMessageTelegram).toHaveBeenCalledWith( + "123", + "ok", + expect.objectContaining({ token: "t-1" }), + ); + } finally { + if (prevTelegramToken === undefined) { + delete process.env.TELEGRAM_BOT_TOKEN; + } else { + process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; + } + } + }); + }); }); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 3501221d1..018d3ec42 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -38,6 +38,7 @@ import { } from "../config/sessions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { resolveTelegramToken } from "../telegram/token.js"; import { normalizeE164 } from "../utils.js"; type AgentCommandOpts = { @@ -218,6 +219,8 @@ export async function agentCommand( ? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg }) : sessionEntry?.skillsSnapshot; + const { token: telegramToken } = resolveTelegramToken(cfg); + if (skillsSnapshot && sessionStore && sessionKey && needsSkillsSnapshot) { const current = sessionEntry ?? { sessionId, @@ -544,6 +547,7 @@ export async function agentCommand( for (const chunk of chunkText(text, 4000)) { await deps.sendMessageTelegram(telegramTarget, chunk, { verbose: false, + token: telegramToken || undefined, }); } } else { @@ -554,6 +558,7 @@ export async function agentCommand( await deps.sendMessageTelegram(telegramTarget, caption, { verbose: false, mediaUrl: url, + token: telegramToken || undefined, }); } } diff --git a/src/commands/health.ts b/src/commands/health.ts index a01401512..3eeab6340 100644 --- a/src/commands/health.ts +++ b/src/commands/health.ts @@ -1,5 +1,3 @@ -import fs from "node:fs"; - import { loadConfig } from "../config/config.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { type DiscordProbe, probeDiscord } from "../discord/probe.js"; @@ -7,6 +5,7 @@ import { callGateway } from "../gateway/call.js"; import { info } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; import { probeTelegram, type TelegramProbe } from "../telegram/probe.js"; +import { resolveTelegramToken } from "../telegram/token.js"; import { resolveHeartbeatSeconds } from "../web/reconnect.js"; import { getWebAuthAgeMs, @@ -55,25 +54,6 @@ export type HealthSummary = { const DEFAULT_TIMEOUT_MS = 10_000; -function loadTelegramToken(cfg: ReturnType): string { - const env = process.env.TELEGRAM_BOT_TOKEN?.trim(); - if (env) return env; - - const tokenFile = cfg.telegram?.tokenFile?.trim(); - if (tokenFile) { - try { - if (fs.existsSync(tokenFile)) { - const token = fs.readFileSync(tokenFile, "utf-8").trim(); - if (token) return token; - } - } catch { - // Ignore errors; health should be non-fatal. - } - } - - return cfg.telegram?.botToken?.trim() ?? ""; -} - export async function getHealthSnapshot( timeoutMs?: number, ): Promise { @@ -95,7 +75,7 @@ export async function getHealthSnapshot( const start = Date.now(); const cappedTimeout = Math.max(1000, timeoutMs ?? DEFAULT_TIMEOUT_MS); - const telegramToken = loadTelegramToken(cfg); + const { token: telegramToken } = resolveTelegramToken(cfg); const telegramConfigured = telegramToken.trim().length > 0; const telegramProxy = cfg.telegram?.proxy; const telegramProbe = telegramConfigured diff --git a/src/commands/send.test.ts b/src/commands/send.test.ts index ee12be9c2..f62a18457 100644 --- a/src/commands/send.test.ts +++ b/src/commands/send.test.ts @@ -4,6 +4,11 @@ import type { CliDeps } from "../cli/deps.js"; import type { RuntimeEnv } from "../runtime.js"; import { sendCommand } from "./send.js"; +let testConfig: Record = {}; +vi.mock("../config/config.js", () => ({ + loadConfig: () => testConfig, +})); + const callGatewayMock = vi.fn(); vi.mock("../gateway/call.js", () => ({ callGateway: (...args: unknown[]) => callGatewayMock(...args), @@ -16,6 +21,7 @@ const originalDiscordToken = process.env.DISCORD_BOT_TOKEN; beforeEach(() => { process.env.TELEGRAM_BOT_TOKEN = "token-abc"; process.env.DISCORD_BOT_TOKEN = "token-discord"; + testConfig = {}; }); afterAll(() => { @@ -75,6 +81,7 @@ describe("sendCommand", () => { .fn() .mockResolvedValue({ messageId: "t1", chatId: "123" }), }); + testConfig = { telegram: { botToken: "token-abc" } }; await sendCommand( { to: "123", message: "hi", provider: "telegram" }, deps, @@ -88,6 +95,26 @@ describe("sendCommand", () => { expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled(); }); + it("uses config token for telegram when env is missing", async () => { + process.env.TELEGRAM_BOT_TOKEN = ""; + testConfig = { telegram: { botToken: "cfg-token" } }; + const deps = makeDeps({ + sendMessageTelegram: vi + .fn() + .mockResolvedValue({ messageId: "t1", chatId: "123" }), + }); + await sendCommand( + { to: "123", message: "hi", provider: "telegram" }, + deps, + runtime, + ); + expect(deps.sendMessageTelegram).toHaveBeenCalledWith( + "123", + "hi", + expect.objectContaining({ token: "cfg-token" }), + ); + }); + it("routes to discord provider", async () => { const deps = makeDeps({ sendMessageDiscord: vi diff --git a/src/commands/send.ts b/src/commands/send.ts index f708f8293..6f98c772e 100644 --- a/src/commands/send.ts +++ b/src/commands/send.ts @@ -1,7 +1,9 @@ import type { CliDeps } from "../cli/deps.js"; +import { loadConfig } from "../config/config.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { success } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; +import { resolveTelegramToken } from "../telegram/token.js"; export async function sendCommand( opts: { @@ -25,8 +27,9 @@ export async function sendCommand( } if (provider === "telegram") { + const { token } = resolveTelegramToken(loadConfig()); const result = await deps.sendMessageTelegram(opts.to, opts.message, { - token: process.env.TELEGRAM_BOT_TOKEN, + token: token || undefined, mediaUrl: opts.media, }); runtime.log( diff --git a/src/cron/isolated-agent.test.ts b/src/cron/isolated-agent.test.ts index 8fc319d08..89adfeed0 100644 --- a/src/cron/isolated-agent.test.ts +++ b/src/cron/isolated-agent.test.ts @@ -53,14 +53,19 @@ async function writeSessionStore(home: string) { return storePath; } -function makeCfg(home: string, storePath: string): ClawdisConfig { - return { +function makeCfg( + home: string, + storePath: string, + overrides: Partial = {}, +): ClawdisConfig { + const base: ClawdisConfig = { agent: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, session: { store: storePath, mainKey: "main" }, } as ClawdisConfig; + return { ...base, ...overrides }; } function makeJob(payload: CronJob["payload"]): CronJob { @@ -91,6 +96,7 @@ describe("runCronIsolatedAgentTurn", () => { sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi.fn(), sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), }; vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "first" }, { text: " " }, { text: " last " }], @@ -121,6 +127,7 @@ describe("runCronIsolatedAgentTurn", () => { sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi.fn(), sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), }; const long = "a".repeat(2001); vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ @@ -152,6 +159,7 @@ describe("runCronIsolatedAgentTurn", () => { sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi.fn(), sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), }; vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "hello" }], @@ -190,6 +198,7 @@ describe("runCronIsolatedAgentTurn", () => { sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi.fn(), sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), }; vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "hello" }], @@ -220,6 +229,60 @@ describe("runCronIsolatedAgentTurn", () => { }); }); + it("passes telegram token from config for delivery", async () => { + await withTempHome(async (home) => { + const storePath = await writeSessionStore(home); + const deps: CliDeps = { + sendMessageWhatsApp: vi.fn(), + sendMessageTelegram: vi.fn().mockResolvedValue({ + messageId: "t1", + chatId: "123", + }), + sendMessageDiscord: vi.fn(), + sendMessageSignal: vi.fn(), + }; + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "hello from cron" }], + meta: { + durationMs: 5, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); + + const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; + process.env.TELEGRAM_BOT_TOKEN = ""; + try { + const res = await runCronIsolatedAgentTurn({ + cfg: makeCfg(home, storePath, { telegram: { botToken: "t-1" } }), + deps, + job: makeJob({ + kind: "agentTurn", + message: "do it", + deliver: true, + channel: "telegram", + to: "123", + }), + message: "do it", + sessionKey: "cron:job-1", + lane: "cron", + }); + + expect(res.status).toBe("ok"); + expect(deps.sendMessageTelegram).toHaveBeenCalledWith( + "123", + "hello from cron", + expect.objectContaining({ token: "t-1" }), + ); + } finally { + if (prevTelegramToken === undefined) { + delete process.env.TELEGRAM_BOT_TOKEN; + } else { + process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; + } + } + }); + }); + it("delivers via discord when configured", async () => { await withTempHome(async (home) => { const storePath = await writeSessionStore(home); @@ -230,6 +293,7 @@ describe("runCronIsolatedAgentTurn", () => { messageId: "d1", channelId: "chan", }), + sendMessageSignal: vi.fn(), }; vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "hello from cron" }], diff --git a/src/cron/isolated-agent.ts b/src/cron/isolated-agent.ts index 0c34be543..8e21f9c7d 100644 --- a/src/cron/isolated-agent.ts +++ b/src/cron/isolated-agent.ts @@ -24,6 +24,7 @@ import { type SessionEntry, saveSessionStore, } from "../config/sessions.js"; +import { resolveTelegramToken } from "../telegram/token.js"; import { normalizeE164 } from "../utils.js"; import type { CronJob } from "./types.js"; @@ -206,6 +207,7 @@ export async function runCronIsolatedAgentTurn(params: { ? params.job.payload.to : undefined, }); + const { token: telegramToken } = resolveTelegramToken(params.cfg); const base = `[cron:${params.job.id} ${params.job.name}] ${params.message}`.trim(); @@ -352,6 +354,7 @@ export async function runCronIsolatedAgentTurn(params: { for (const chunk of chunkText(payload.text ?? "", 4000)) { await params.deps.sendMessageTelegram(chatId, chunk, { verbose: false, + token: telegramToken || undefined, }); } } else { @@ -362,6 +365,7 @@ export async function runCronIsolatedAgentTurn(params: { await params.deps.sendMessageTelegram(chatId, caption, { verbose: false, mediaUrl: url, + token: telegramToken || undefined, }); } } diff --git a/src/gateway/server.ts b/src/gateway/server.ts index a93f122ec..3ddf91841 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -144,6 +144,7 @@ import { probeSignal, type SignalProbe } from "../signal/probe.js"; import { monitorTelegramProvider } from "../telegram/monitor.js"; import { probeTelegram, type TelegramProbe } from "../telegram/probe.js"; import { sendMessageTelegram } from "../telegram/send.js"; +import { resolveTelegramToken } from "../telegram/token.js"; import { normalizeE164, resolveUserPath } from "../utils.js"; import type { WebProviderStatus } from "../web/auto-reply.js"; import { startWebLoginWithQr, waitForWebLogin } from "../web/login-qr.js"; @@ -292,33 +293,6 @@ const telegramRuntimeEnv = runtimeForLogger(logTelegram); const discordRuntimeEnv = runtimeForLogger(logDiscord); const signalRuntimeEnv = runtimeForLogger(logSignal); -function loadTelegramToken( - config: ClawdisConfig, - opts: { logMissing?: boolean } = {}, -): string { - if (process.env.TELEGRAM_BOT_TOKEN) { - return process.env.TELEGRAM_BOT_TOKEN.trim(); - } - if (config.telegram?.tokenFile) { - const filePath = config.telegram.tokenFile; - if (!fs.existsSync(filePath)) { - if (opts.logMissing) { - logTelegram.warn(`telegram.tokenFile not found: ${filePath}`); - } - return ""; - } - try { - return fs.readFileSync(filePath, "utf-8").trim(); - } catch (err) { - if (opts.logMissing) { - logTelegram.warn(`telegram.tokenFile read failed: ${String(err)}`); - } - return ""; - } - } - return config.telegram?.botToken?.trim() ?? ""; -} - function resolveBonjourCliPath(): string | undefined { const envPath = process.env.CLAWDIS_CLI_PATH?.trim(); if (envPath) return envPath; @@ -1956,7 +1930,9 @@ export async function startGatewayServer( logTelegram.info("skipping provider start (telegram.enabled=false)"); return; } - const telegramToken = loadTelegramToken(cfg, { logMissing: true }); + const { token: telegramToken } = resolveTelegramToken(cfg, { + logMissingFile: (message) => logTelegram.warn(message), + }); if (!telegramToken.trim()) { telegramRuntime = { ...telegramRuntime, @@ -1964,7 +1940,7 @@ export async function startGatewayServer( lastError: "not configured", }; logTelegram.info( - "skipping provider start (no TELEGRAM_BOT_TOKEN/config)", + "skipping provider start (no TELEGRAM_BOT_TOKEN/telegram config)", ); return; } @@ -4058,14 +4034,8 @@ export async function startGatewayServer( ? Math.max(1000, timeoutMsRaw) : 10_000; const cfg = loadConfig(); - const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim(); - const configToken = cfg.telegram?.botToken?.trim(); - const telegramToken = envToken || configToken || ""; - const tokenSource = envToken - ? "env" - : configToken - ? "config" - : "none"; + const { token: telegramToken, source: tokenSource } = + resolveTelegramToken(cfg); let telegramProbe: TelegramProbe | undefined; let lastProbeAt: number | null = null; if (probe && telegramToken) { @@ -6023,7 +5993,7 @@ export async function startGatewayServer( try { if (provider === "telegram") { const cfg = loadConfig(); - const token = loadTelegramToken(cfg); + const { token } = resolveTelegramToken(cfg); const result = await sendMessageTelegram(to, message, { mediaUrl: params.mediaUrl, verbose: isVerbose(), diff --git a/src/infra/provider-summary.ts b/src/infra/provider-summary.ts index 440d11f43..3e16056c3 100644 --- a/src/infra/provider-summary.ts +++ b/src/infra/provider-summary.ts @@ -1,6 +1,6 @@ -import fs from "node:fs"; import chalk from "chalk"; import { type ClawdisConfig, loadConfig } from "../config/config.js"; +import { resolveTelegramToken } from "../telegram/token.js"; import { normalizeE164 } from "../utils.js"; import { getWebAuthAgeMs, @@ -35,12 +35,8 @@ export async function buildProviderSummary( if (!telegramEnabled) { lines.push(chalk.cyan("Telegram: disabled")); } else { - const telegramToken = - process.env.TELEGRAM_BOT_TOKEN ?? effective.telegram?.botToken; - const telegramTokenFile = effective.telegram?.tokenFile?.trim(); - const telegramConfigured = - Boolean(telegramToken) || - Boolean(telegramTokenFile ? fs.existsSync(telegramTokenFile) : false); + const { token: telegramToken } = resolveTelegramToken(effective); + const telegramConfigured = Boolean(telegramToken); lines.push( telegramConfigured ? chalk.green("Telegram: configured") diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 2426bc0c3..9f8328f8b 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -2,6 +2,7 @@ import { loadConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import { createTelegramBot } from "./bot.js"; import { makeProxyFetch } from "./proxy.js"; +import { resolveTelegramToken } from "./token.js"; import { startTelegramWebhook } from "./webhook.js"; export type MonitorTelegramOpts = { @@ -17,10 +18,12 @@ export type MonitorTelegramOpts = { }; export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { - const token = (opts.token ?? process.env.TELEGRAM_BOT_TOKEN)?.trim(); + const { token } = resolveTelegramToken(loadConfig(), { + envToken: opts.token, + }); if (!token) { throw new Error( - "TELEGRAM_BOT_TOKEN or telegram.botToken is required for Telegram gateway", + "TELEGRAM_BOT_TOKEN or telegram.botToken/tokenFile is required for Telegram gateway", ); } diff --git a/src/telegram/token.test.ts b/src/telegram/token.test.ts new file mode 100644 index 000000000..bb117dceb --- /dev/null +++ b/src/telegram/token.test.ts @@ -0,0 +1,59 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { ClawdisConfig } from "../config/config.js"; +import { resolveTelegramToken } from "./token.js"; + +function withTempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "clawdis-telegram-token-")); +} + +describe("resolveTelegramToken", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("prefers env token over config", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); + const cfg = { telegram: { botToken: "cfg-token" } } as ClawdisConfig; + const res = resolveTelegramToken(cfg); + expect(res.token).toBe("env-token"); + expect(res.source).toBe("env"); + }); + + it("uses tokenFile when configured", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); + const dir = withTempDir(); + const tokenFile = path.join(dir, "token.txt"); + fs.writeFileSync(tokenFile, "file-token\n", "utf-8"); + const cfg = { telegram: { tokenFile } } as ClawdisConfig; + const res = resolveTelegramToken(cfg); + expect(res.token).toBe("file-token"); + expect(res.source).toBe("tokenFile"); + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it("falls back to config token when no env or tokenFile", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); + const cfg = { telegram: { botToken: "cfg-token" } } as ClawdisConfig; + const res = resolveTelegramToken(cfg); + expect(res.token).toBe("cfg-token"); + expect(res.source).toBe("config"); + }); + + it("does not fall back to config when tokenFile is missing", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); + const dir = withTempDir(); + const tokenFile = path.join(dir, "missing-token.txt"); + const cfg = { + telegram: { tokenFile, botToken: "cfg-token" }, + } as ClawdisConfig; + const res = resolveTelegramToken(cfg); + expect(res.token).toBe(""); + expect(res.source).toBe("none"); + fs.rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/src/telegram/token.ts b/src/telegram/token.ts new file mode 100644 index 000000000..b82d35ab8 --- /dev/null +++ b/src/telegram/token.ts @@ -0,0 +1,50 @@ +import fs from "node:fs"; + +import type { ClawdisConfig } from "../config/config.js"; + +export type TelegramTokenSource = "env" | "tokenFile" | "config" | "none"; + +export type TelegramTokenResolution = { + token: string; + source: TelegramTokenSource; +}; + +type ResolveTelegramTokenOpts = { + envToken?: string | null; + logMissingFile?: (message: string) => void; +}; + +export function resolveTelegramToken( + cfg?: ClawdisConfig, + opts: ResolveTelegramTokenOpts = {}, +): TelegramTokenResolution { + const envToken = (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim(); + if (envToken) { + return { token: envToken, source: "env" }; + } + + const tokenFile = cfg?.telegram?.tokenFile?.trim(); + if (tokenFile) { + if (!fs.existsSync(tokenFile)) { + opts.logMissingFile?.(`telegram.tokenFile not found: ${tokenFile}`); + return { token: "", source: "none" }; + } + try { + const token = fs.readFileSync(tokenFile, "utf-8").trim(); + if (token) { + return { token, source: "tokenFile" }; + } + } catch (err) { + opts.logMissingFile?.(`telegram.tokenFile read failed: ${String(err)}`); + return { token: "", source: "none" }; + } + return { token: "", source: "none" }; + } + + const configToken = cfg?.telegram?.botToken?.trim(); + if (configToken) { + return { token: configToken, source: "config" }; + } + + return { token: "", source: "none" }; +}