Skip to content

Commit f6d11d0

Browse files
authored
fix(google-meet): guard node-host meet URLs (#104687)
1 parent 4b50658 commit f6d11d0

5 files changed

Lines changed: 62 additions & 25 deletions

File tree

extensions/google-meet/node-host.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,33 @@ describe("google-meet node host bridge sessions", () => {
6363
);
6464
});
6565

66+
it("rejects non-Meet start URLs before local Chrome side effects", async () => {
67+
const originalPlatform = process.platform;
68+
children.length = 0;
69+
vi.mocked(spawnSync).mockClear();
70+
71+
Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
72+
try {
73+
await expect(
74+
handleGoogleMeetNodeHostCommand(
75+
JSON.stringify({
76+
action: "start",
77+
url: "https://example.com/private-call",
78+
mode: "realtime",
79+
launch: true,
80+
audioInputCommand: ["mock-rec"],
81+
audioOutputCommand: ["mock-play"],
82+
}),
83+
),
84+
).rejects.toThrow("url must be an explicit https://meet.google.com/... URL");
85+
86+
expect(spawnSync).not.toHaveBeenCalled();
87+
expect(children).toHaveLength(0);
88+
} finally {
89+
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
90+
}
91+
});
92+
6693
it("starts observe-only Chrome without BlackHole or bridge processes", async () => {
6794
const originalPlatform = process.platform;
6895
children.length = 0;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Google Meet plugin module implements shared Meet URL contracts.
2+
3+
function normalizeOptionalString(value: unknown): string | undefined {
4+
if (typeof value !== "string") {
5+
return undefined;
6+
}
7+
const trimmed = value.trim();
8+
return trimmed || undefined;
9+
}
10+
11+
export function normalizeMeetUrl(input: unknown): string {
12+
const raw = normalizeOptionalString(input);
13+
if (!raw) {
14+
throw new Error("url required");
15+
}
16+
let url: URL;
17+
try {
18+
url = new URL(raw);
19+
} catch {
20+
throw new Error("url must be a valid Google Meet URL");
21+
}
22+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "meet.google.com") {
23+
throw new Error("url must be an explicit https://meet.google.com/... URL");
24+
}
25+
if (!/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i.test(url.pathname)) {
26+
throw new Error("url must include a Google Meet meeting code");
27+
}
28+
return url.toString();
29+
}

extensions/google-meet/src/node-host.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
77
DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
88
} from "./config.js";
9+
import { normalizeMeetUrl } from "./meet-url.js";
910
import {
1011
GOOGLE_MEET_SYSTEM_PROFILER_COMMAND,
1112
outputMentionsBlackHole2ch,
@@ -278,10 +279,7 @@ function clearAudio(params: Record<string, unknown>) {
278279
}
279280

280281
function startChrome(params: Record<string, unknown>) {
281-
const url = readString(params.url);
282-
if (!url) {
283-
throw new Error("url required");
284-
}
282+
const url = normalizeMeetUrl(params.url);
285283
const timeoutMs = readNumber(params.joinTimeoutMs, 30_000);
286284
const mode = readString(params.mode);
287285

extensions/google-meet/src/node-invoke-policy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
OpenClawPluginNodeInvokePolicyResult,
55
} from "openclaw/plugin-sdk/plugin-entry";
66
import type { GoogleMeetConfig } from "./config.js";
7-
import { normalizeMeetUrl } from "./runtime.js";
7+
import { normalizeMeetUrl } from "./meet-url.js";
88

99
export const GOOGLE_MEET_CHROME_NODE_COMMAND = "googlemeet.chrome";
1010

extensions/google-meet/src/runtime.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
GoogleMeetModeInput,
1313
GoogleMeetTransport,
1414
} from "./config.js";
15+
import { normalizeMeetUrl } from "./meet-url.js";
1516
import { addGoogleMeetSetupCheck, getGoogleMeetSetupStatus } from "./setup.js";
1617
import {
1718
isSameMeetUrlForReuse,
@@ -50,6 +51,8 @@ import {
5051
type VoiceCallGateway,
5152
} from "./voice-call-gateway.js";
5253

54+
export { normalizeMeetUrl } from "./meet-url.js";
55+
5356
type ChromeAudioBridgeResult = NonNullable<
5457
| Awaited<ReturnType<typeof launchChromeMeet>>["audioBridge"]
5558
| Awaited<ReturnType<typeof launchChromeMeetOnNode>>["audioBridge"]
@@ -78,26 +81,6 @@ function buildTwilioVoiceCallSessionKey(meetingSessionId: string): string {
7881
return `voice:google-meet:${meetingSessionId}`;
7982
}
8083

81-
export function normalizeMeetUrl(input: unknown): string {
82-
const raw = normalizeOptionalString(input);
83-
if (!raw) {
84-
throw new Error("url required");
85-
}
86-
let url: URL;
87-
try {
88-
url = new URL(raw);
89-
} catch {
90-
throw new Error("url must be a valid Google Meet URL");
91-
}
92-
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "meet.google.com") {
93-
throw new Error("url must be an explicit https://meet.google.com/... URL");
94-
}
95-
if (!/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i.test(url.pathname)) {
96-
throw new Error("url must include a Google Meet meeting code");
97-
}
98-
return url.toString();
99-
}
100-
10184
function resolveTransport(input: GoogleMeetTransport | undefined, config: GoogleMeetConfig) {
10285
return input ?? config.defaultTransport;
10386
}

0 commit comments

Comments
 (0)