mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 02:12:48 +08:00
fix(telegram): harden mini app URL and page serving
This commit is contained in:
@@ -56,7 +56,7 @@ describe("createTelegramMiniAppDashboardCommand", () => {
|
||||
resolveTelegramMiniAppUrls.mockResolvedValue({
|
||||
pageUrl: "https://host.tailnet.ts.net/__openclaw_tg_miniapp/",
|
||||
controlUiUrl: "https://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net",
|
||||
});
|
||||
const command = createTelegramMiniAppDashboardCommand(
|
||||
createTestPluginApi({
|
||||
|
||||
@@ -59,6 +59,8 @@ function currentConfig(api: OpenClawPluginApi): OpenClawConfig {
|
||||
}
|
||||
|
||||
function isTelegramDirectCommand(ctx: PluginCommandContext): boolean {
|
||||
// Parses OpenClaw's canonical telegram:<id> / telegram:group:<id> from/sessionKey encoding.
|
||||
// DM-only because Telegram permits web_app inline buttons only in private chats.
|
||||
const from = ctx.from?.trim() ?? "";
|
||||
const sessionKey = ctx.sessionKey?.trim() ?? "";
|
||||
if (from.startsWith("telegram:group:") || sessionKey.includes(":telegram:group:")) {
|
||||
|
||||
@@ -15,6 +15,8 @@ export async function isTelegramMiniAppOwner(params: {
|
||||
}
|
||||
const account = mergeTelegramAccountConfig(params.cfg, params.accountId);
|
||||
const allowFrom = [...(account.allowFrom ?? []), ...(params.cfg.commands?.ownerAllowFrom ?? [])];
|
||||
// Dashboard access is stricter than core senderIsOwner: wildcard and username
|
||||
// allowFrom entries never grant the numeric-id match that mints an operator credential.
|
||||
const expanded = await expandTelegramAllowFromWithAccessGroups({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
|
||||
12
extensions/telegram/src/miniapp/page.test.ts
Normal file
12
extensions/telegram/src/miniapp/page.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderTelegramMiniAppPage } from "./page.js";
|
||||
|
||||
describe("renderTelegramMiniAppPage", () => {
|
||||
it("builds the dashboard redirect from the authenticated payload", () => {
|
||||
const html = renderTelegramMiniAppPage({ accountId: "ops", scriptNonce: "nonce" });
|
||||
|
||||
expect(html).toContain('const accountId = "ops";');
|
||||
expect(html).toContain("new URL(payload.controlUiUrl)");
|
||||
expect(html).not.toContain("const controlUiUrl =");
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,9 @@ export const TELEGRAM_MINIAPP_EXPIRED_MESSAGE =
|
||||
|
||||
export function renderTelegramMiniAppPage(params: {
|
||||
accountId: string;
|
||||
controlUiUrl: string;
|
||||
scriptNonce: string;
|
||||
}): string {
|
||||
const accountId = JSON.stringify(params.accountId);
|
||||
const controlUiUrl = JSON.stringify(params.controlUiUrl);
|
||||
const nonce = escapeHtml(params.scriptNonce);
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -33,7 +31,6 @@ export function renderTelegramMiniAppPage(params: {
|
||||
</main>
|
||||
<script nonce="${nonce}">
|
||||
const accountId = ${accountId};
|
||||
const controlUiUrl = ${controlUiUrl};
|
||||
const status = document.getElementById("status");
|
||||
const showExpired = () => {
|
||||
status.textContent = ${JSON.stringify(TELEGRAM_MINIAPP_EXPIRED_MESSAGE)};
|
||||
@@ -55,7 +52,7 @@ export function renderTelegramMiniAppPage(params: {
|
||||
}
|
||||
return await response.json();
|
||||
}).then((payload) => {
|
||||
const next = new URL(controlUiUrl);
|
||||
const next = new URL(payload.controlUiUrl);
|
||||
next.hash = "gatewayUrl=" + encodeURIComponent(payload.gatewayUrl) +
|
||||
"&bootstrapToken=" + encodeURIComponent(payload.bootstrapToken);
|
||||
location.replace(next.toString());
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { Readable } from "node:stream";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestPluginApi } from "../../../../src/plugin-sdk/plugin-test-api.js";
|
||||
import type { OpenClawPluginHttpRouteParams } from "../../../../src/plugins/types.js";
|
||||
|
||||
@@ -13,7 +13,7 @@ const resolveTelegramMiniAppUrls = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
pageUrl: "https://host.tailnet.ts.net/__openclaw_tg_miniapp/",
|
||||
controlUiUrl: "https://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net",
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -108,6 +108,25 @@ function signedInitData(userId: string, nonce: string): string {
|
||||
}
|
||||
|
||||
describe("registerTelegramMiniAppRoutes", () => {
|
||||
beforeEach(() => {
|
||||
issueDeviceBootstrapToken.mockClear();
|
||||
resolveTelegramMiniAppUrls.mockClear();
|
||||
});
|
||||
|
||||
it("serves the page without resolving published URLs", async () => {
|
||||
const route = createRoute({});
|
||||
const res = await callRoute({
|
||||
route,
|
||||
method: "GET",
|
||||
url: "/__openclaw_tg_miniapp/?accountId=ops",
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toContain('const accountId = "ops";');
|
||||
expect(res.body).toContain("new URL(payload.controlUiUrl)");
|
||||
expect(resolveTelegramMiniAppUrls).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mints a control-ui bootstrap token for a valid owner request", async () => {
|
||||
const route = createRoute(config());
|
||||
const res = await callRoute({
|
||||
@@ -124,7 +143,8 @@ describe("registerTelegramMiniAppRoutes", () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res.body)).toEqual({
|
||||
bootstrapToken: "issued",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net/openclaw",
|
||||
controlUiUrl: "https://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net",
|
||||
});
|
||||
expect(issueDeviceBootstrapToken).toHaveBeenCalledWith({
|
||||
profile: {
|
||||
@@ -136,7 +156,6 @@ describe("registerTelegramMiniAppRoutes", () => {
|
||||
});
|
||||
|
||||
it("rejects replayed init-data without minting again", async () => {
|
||||
issueDeviceBootstrapToken.mockClear();
|
||||
const route = createRoute(config());
|
||||
const initData = signedInitData("123456", "replay");
|
||||
await callRoute({
|
||||
@@ -162,7 +181,6 @@ describe("registerTelegramMiniAppRoutes", () => {
|
||||
});
|
||||
|
||||
it("reserves validated init-data before minting", async () => {
|
||||
issueDeviceBootstrapToken.mockClear();
|
||||
const route = createRoute(config());
|
||||
const initData = signedInitData("123456", "concurrent");
|
||||
|
||||
@@ -185,7 +203,7 @@ describe("registerTelegramMiniAppRoutes", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(responses.map((res) => res.statusCode).toSorted()).toEqual([200, 401]);
|
||||
expect(responses.map((res) => res.statusCode).toSorted((a, b) => a - b)).toEqual([200, 401]);
|
||||
expect(issueDeviceBootstrapToken).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function registerTelegramMiniAppRoutes(api: OpenClawPluginApi): void {
|
||||
handler: async (req, res) => {
|
||||
const url = new URL(req.url ?? "", "http://openclaw.local");
|
||||
if (url.pathname === TELEGRAM_MINIAPP_PATH_PREFIX) {
|
||||
await handlePage(api, req, res, url);
|
||||
await handlePage(req, res, url);
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === AUTH_PATH) {
|
||||
@@ -47,24 +47,11 @@ export function registerTelegramMiniAppRoutes(api: OpenClawPluginApi): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function handlePage(
|
||||
api: OpenClawPluginApi,
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
): Promise<void> {
|
||||
async function handlePage(req: IncomingMessage, res: ServerResponse, url: URL): Promise<void> {
|
||||
if (req.method !== "GET") {
|
||||
sendText(res, 405, "Method not allowed");
|
||||
return;
|
||||
}
|
||||
const cfg = currentConfig(api);
|
||||
let urls;
|
||||
try {
|
||||
urls = await resolveTelegramMiniAppUrls({ cfg });
|
||||
} catch {
|
||||
sendText(res, 503, TELEGRAM_MINIAPP_URL_ERROR);
|
||||
return;
|
||||
}
|
||||
const accountId = normalizeAccountId(url.searchParams.get("accountId") ?? DEFAULT_ACCOUNT_ID);
|
||||
const nonce = crypto.randomBytes(16).toString("base64url");
|
||||
sendHtml(
|
||||
@@ -72,7 +59,6 @@ async function handlePage(
|
||||
200,
|
||||
renderTelegramMiniAppPage({
|
||||
accountId,
|
||||
controlUiUrl: urls.controlUiUrl,
|
||||
scriptNonce: nonce,
|
||||
}),
|
||||
nonce,
|
||||
@@ -88,7 +74,7 @@ async function handleAuth(
|
||||
sendText(res, 405, "Method not allowed");
|
||||
return;
|
||||
}
|
||||
const contentType = String(req.headers["content-type"] ?? "").toLowerCase();
|
||||
const contentType = (req.headers["content-type"] ?? "").toLowerCase();
|
||||
if (contentType.split(";")[0]?.trim() !== "application/json") {
|
||||
sendText(res, 415, "Unsupported media type");
|
||||
return;
|
||||
@@ -144,6 +130,7 @@ async function handleAuth(
|
||||
});
|
||||
sendJson(res, 200, {
|
||||
bootstrapToken: issued.token,
|
||||
controlUiUrl: urls.controlUiUrl,
|
||||
gatewayUrl: urls.gatewayUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("resolveTelegramMiniAppUrls", () => {
|
||||
await expect(resolveTelegramMiniAppUrls({ cfg, runCommand })).resolves.toEqual({
|
||||
pageUrl: "https://host.tailnet.ts.net/__openclaw_tg_miniapp/",
|
||||
controlUiUrl: "https://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net/openclaw",
|
||||
gatewayUrl: "wss://host.tailnet.ts.net",
|
||||
});
|
||||
expect(runCommand).toHaveBeenCalledWith(["tailscale", "status", "--json"], {
|
||||
timeoutMs: 5000,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Telegram Mini App published URL resolution.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
resolveTailnetHostWithRunner,
|
||||
resolveTailscalePublishedHost,
|
||||
@@ -43,7 +43,7 @@ export async function resolveTelegramMiniAppUrls(params: {
|
||||
return {
|
||||
pageUrl: `https://${publishedHost}${TELEGRAM_MINIAPP_PATH_PREFIX}`,
|
||||
controlUiUrl,
|
||||
gatewayUrl: `wss://${publishedHost}${controlUiPath}`,
|
||||
gatewayUrl: `wss://${publishedHost}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user