@kodelyth/google-meet 2026.5.42 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/setup.ts DELETED
@@ -1,285 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { isBlockedHostnameOrIp } from "klaw/plugin-sdk/ssrf-runtime";
5
- import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
6
-
7
- type SetupCheck = {
8
- id: string;
9
- ok: boolean;
10
- message: string;
11
- };
12
-
13
- type GoogleMeetSetupStatus = {
14
- ok: boolean;
15
- checks: SetupCheck[];
16
- };
17
-
18
- function resolveUserPath(input: string): string {
19
- if (input === "~") {
20
- return os.homedir();
21
- }
22
- if (input.startsWith("~/")) {
23
- return path.join(os.homedir(), input.slice(2));
24
- }
25
- return input;
26
- }
27
-
28
- function isProviderUnreachableWebhookUrl(webhookUrl: string): boolean {
29
- try {
30
- const parsed = new URL(webhookUrl);
31
- return isBlockedHostnameOrIp(parsed.hostname);
32
- } catch {
33
- return false;
34
- }
35
- }
36
-
37
- function getVoiceCallWebhookExposureCheck(voiceCallConfig: Record<string, unknown>): SetupCheck {
38
- const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
39
- const tunnel = asRecord(voiceCallConfig.tunnel);
40
- const tailscale = asRecord(voiceCallConfig.tailscale);
41
- const tunnelProvider = normalizeOptionalString(tunnel.provider);
42
- const tailscaleMode = normalizeOptionalString(tailscale.mode);
43
-
44
- if (publicUrl) {
45
- const ok = !isProviderUnreachableWebhookUrl(publicUrl);
46
- return {
47
- id: "twilio-voice-call-webhook",
48
- ok,
49
- message: ok
50
- ? `Voice-call public webhook URL configured: ${publicUrl}`
51
- : `Voice-call publicUrl is local/private and cannot be reached by Twilio: ${publicUrl}`,
52
- };
53
- }
54
-
55
- if (tunnelProvider && tunnelProvider !== "none") {
56
- return {
57
- id: "twilio-voice-call-webhook",
58
- ok: true,
59
- message: "Voice-call webhook exposure configured through tunnel",
60
- };
61
- }
62
-
63
- if (tailscaleMode && tailscaleMode !== "off") {
64
- return {
65
- id: "twilio-voice-call-webhook",
66
- ok: true,
67
- message: "Voice-call webhook exposure configured through Tailscale",
68
- };
69
- }
70
-
71
- return {
72
- id: "twilio-voice-call-webhook",
73
- ok: false,
74
- message:
75
- "Set plugins.entries.voice-call.config.publicUrl or configure voice-call tunnel/tailscale exposure for Twilio dialing",
76
- };
77
- }
78
-
79
- export function getGoogleMeetSetupStatus(config: GoogleMeetConfig): {
80
- ok: boolean;
81
- checks: SetupCheck[];
82
- };
83
- export function getGoogleMeetSetupStatus(
84
- config: GoogleMeetConfig,
85
- options?: {
86
- env?: NodeJS.ProcessEnv;
87
- fullConfig?: unknown;
88
- mode?: GoogleMeetMode;
89
- transport?: GoogleMeetTransport;
90
- twilioDialInNumber?: string;
91
- },
92
- ): {
93
- ok: boolean;
94
- checks: SetupCheck[];
95
- };
96
- export function getGoogleMeetSetupStatus(
97
- config: GoogleMeetConfig,
98
- options?: {
99
- env?: NodeJS.ProcessEnv;
100
- fullConfig?: unknown;
101
- mode?: GoogleMeetMode;
102
- transport?: GoogleMeetTransport;
103
- twilioDialInNumber?: string;
104
- },
105
- ) {
106
- const checks: SetupCheck[] = [];
107
- const env = options?.env ?? process.env;
108
- const fullConfig = asRecord(options?.fullConfig);
109
- const mode = options?.mode ?? config.defaultMode;
110
- const transport = options?.transport ?? config.defaultTransport;
111
- const needsChromeRealtimeAudio =
112
- (mode === "agent" || mode === "bidi") &&
113
- (transport === "chrome" || transport === "chrome-node");
114
- const pluginEntries = asRecord(asRecord(fullConfig.plugins).entries);
115
- const pluginAllow = asRecord(fullConfig.plugins).allow;
116
- const voiceCallEntry = asRecord(pluginEntries["voice-call"]);
117
- const voiceCallConfig = asRecord(voiceCallEntry.config);
118
- const voiceCallTwilioConfig = asRecord(voiceCallConfig.twilio);
119
-
120
- if (config.auth.tokenPath) {
121
- const tokenPath = resolveUserPath(config.auth.tokenPath);
122
- checks.push({
123
- id: "google-oauth-token",
124
- ok: fs.existsSync(tokenPath),
125
- message: fs.existsSync(tokenPath)
126
- ? "Google OAuth token file found"
127
- : `Google OAuth token file missing at ${config.auth.tokenPath}`,
128
- });
129
- } else {
130
- checks.push({
131
- id: "google-oauth-token",
132
- ok: true,
133
- message: "Google OAuth token path not configured; Chrome profile auth will be used",
134
- });
135
- }
136
-
137
- checks.push({
138
- id: "chrome-profile",
139
- ok: true,
140
- message: config.chrome.browserProfile
141
- ? "Local Chrome uses the Klaw browser profile; chrome.browserProfile is passed to chrome-node hosts"
142
- : "Local Chrome uses the Klaw browser profile; configure browser.defaultProfile to choose another profile",
143
- });
144
-
145
- if (needsChromeRealtimeAudio) {
146
- const hasCommandPair = Boolean(
147
- config.chrome.audioInputCommand && config.chrome.audioOutputCommand,
148
- );
149
- const hasExternalBridge = Boolean(config.chrome.audioBridgeCommand);
150
- const agentModeExternalBridgeInvalid = mode === "agent" && hasExternalBridge;
151
- checks.push({
152
- id: "audio-bridge",
153
- ok:
154
- mode === "agent"
155
- ? hasCommandPair && !agentModeExternalBridgeInvalid
156
- : hasExternalBridge || hasCommandPair,
157
- message: agentModeExternalBridgeInvalid
158
- ? "Chrome agent mode requires chrome.audioInputCommand and chrome.audioOutputCommand; chrome.audioBridgeCommand is bidi-only"
159
- : hasExternalBridge
160
- ? "Chrome audio bridge command configured"
161
- : hasCommandPair
162
- ? `Chrome command-pair talk-back audio bridge configured (${config.chrome.audioFormat})`
163
- : "Chrome talk-back audio bridge not configured",
164
- });
165
- } else if (transport === "chrome" || transport === "chrome-node") {
166
- checks.push({
167
- id: "audio-bridge",
168
- ok: true,
169
- message: "Chrome observe-only mode does not require a realtime audio bridge",
170
- });
171
- }
172
-
173
- checks.push({
174
- id: "guest-join-defaults",
175
- ok: Boolean(
176
- config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab,
177
- ),
178
- message:
179
- config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab
180
- ? "Guest auto-join and tab reuse defaults are enabled"
181
- : "Set chrome.guestName, chrome.autoJoin, and chrome.reuseExistingTab for unattended guest joins",
182
- });
183
-
184
- checks.push({
185
- id: "chrome-node-target",
186
- ok: config.defaultTransport !== "chrome-node" || Boolean(config.chromeNode.node),
187
- message:
188
- config.defaultTransport === "chrome-node" && !config.chromeNode.node
189
- ? "chrome-node default should pin chromeNode.node when multiple nodes may be connected"
190
- : config.chromeNode.node
191
- ? `Chrome node pinned to ${config.chromeNode.node}`
192
- : "Chrome node not pinned; automatic selection works when exactly one capable node is connected",
193
- });
194
-
195
- if (needsChromeRealtimeAudio) {
196
- checks.push({
197
- id: "intro-after-in-call",
198
- ok: config.chrome.waitForInCallMs > 0,
199
- message:
200
- config.chrome.waitForInCallMs > 0
201
- ? `Realtime intro waits up to ${config.chrome.waitForInCallMs}ms for the Meet tab to be in-call`
202
- : "Set chrome.waitForInCallMs to delay realtime intro until the Meet tab is in-call",
203
- });
204
- }
205
-
206
- if (transport === "twilio") {
207
- const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
208
- const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
209
- const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
210
- checks.push({
211
- id: "twilio-dial-plan",
212
- ok: hasDialPlan,
213
- message: hasRequestDialPlan
214
- ? "Twilio request includes a Meet dial-in number"
215
- : hasDefaultDialPlan
216
- ? "Twilio default Meet dial-in number is configured"
217
- : "Twilio joins require a Meet dial-in phone number; pass dialInNumber with optional pin/dtmfSequence or configure twilio.defaultDialInNumber",
218
- });
219
- }
220
-
221
- const shouldCheckTwilioDelegation =
222
- config.voiceCall.enabled &&
223
- (transport === "twilio" ||
224
- Boolean(config.twilio.defaultDialInNumber) ||
225
- Object.hasOwn(pluginEntries, "voice-call"));
226
- if (shouldCheckTwilioDelegation) {
227
- const voiceCallAllowed = !Array.isArray(pluginAllow) || pluginAllow.includes("voice-call");
228
- const hasVoiceCallEntry = Object.hasOwn(pluginEntries, "voice-call");
229
- const voiceCallEnabled = hasVoiceCallEntry && voiceCallEntry.enabled !== false;
230
- checks.push({
231
- id: "twilio-voice-call-plugin",
232
- ok: voiceCallAllowed && voiceCallEnabled,
233
- message:
234
- voiceCallAllowed && voiceCallEnabled
235
- ? "Twilio transport can delegate dialing to the voice-call plugin"
236
- : "Enable plugins.entries.voice-call and include voice-call in plugins.allow for Twilio dialing",
237
- });
238
-
239
- const provider = normalizeOptionalString(voiceCallConfig.provider) ?? "twilio";
240
- if (provider === "twilio") {
241
- const accountSid = normalizeOptionalString(voiceCallTwilioConfig.accountSid);
242
- const authToken = normalizeOptionalString(voiceCallTwilioConfig.authToken);
243
- const fromNumber = normalizeOptionalString(voiceCallConfig.fromNumber);
244
- const twilioReady = Boolean(
245
- (accountSid || env.TWILIO_ACCOUNT_SID) &&
246
- (authToken || env.TWILIO_AUTH_TOKEN) &&
247
- (fromNumber || env.TWILIO_FROM_NUMBER),
248
- );
249
- checks.push({
250
- id: "twilio-voice-call-credentials",
251
- ok: twilioReady,
252
- message: twilioReady
253
- ? "Twilio voice-call credentials are configured"
254
- : "Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER or configure voice-call Twilio credentials",
255
- });
256
- checks.push(getVoiceCallWebhookExposureCheck(voiceCallConfig));
257
- }
258
- }
259
-
260
- return {
261
- ok: checks.every((check) => check.ok),
262
- checks,
263
- };
264
- }
265
-
266
- export function addGoogleMeetSetupCheck(
267
- status: GoogleMeetSetupStatus,
268
- check: SetupCheck,
269
- ): GoogleMeetSetupStatus {
270
- const checks = [...status.checks, check];
271
- return {
272
- ok: checks.every((item) => item.ok),
273
- checks,
274
- };
275
- }
276
-
277
- function asRecord(value: unknown): Record<string, unknown> {
278
- return value && typeof value === "object" && !Array.isArray(value)
279
- ? (value as Record<string, unknown>)
280
- : {};
281
- }
282
-
283
- function normalizeOptionalString(value: unknown): string | undefined {
284
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
285
- }
@@ -1,232 +0,0 @@
1
- import type { KlawPluginApi } from "klaw/plugin-sdk/plugin-entry";
2
- import { createTestPluginApi } from "klaw/plugin-sdk/plugin-test-api";
3
- import { vi } from "vitest";
4
-
5
- type GoogleMeetTestPluginEntry = {
6
- register(api: KlawPluginApi): void;
7
- };
8
-
9
- export const noopLogger = {
10
- info: vi.fn(),
11
- warn: vi.fn(),
12
- error: vi.fn(),
13
- debug: vi.fn(),
14
- };
15
-
16
- type GoogleMeetTestNodeListResult = {
17
- nodes: Array<{
18
- nodeId: string;
19
- displayName?: string;
20
- connected?: boolean;
21
- commands?: string[];
22
- caps?: string[];
23
- remoteIp?: string;
24
- }>;
25
- };
26
-
27
- type CommandResult = {
28
- code: number;
29
- stdout?: string;
30
- stderr?: string;
31
- };
32
-
33
- export function captureStdout() {
34
- let output = "";
35
- const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
36
- output += String(chunk);
37
- return true;
38
- }) as typeof process.stdout.write);
39
- return {
40
- output: () => output,
41
- restore: () => writeSpy.mockRestore(),
42
- };
43
- }
44
-
45
- export function setupGoogleMeetPlugin(
46
- plugin: GoogleMeetTestPluginEntry,
47
- config: Record<string, unknown> = {},
48
- options: {
49
- fullConfig?: Record<string, unknown>;
50
- nodesListResult?: GoogleMeetTestNodeListResult;
51
- nodesInvokeResult?: unknown;
52
- browserActResult?: Record<string, unknown>;
53
- nodesInvokeHandler?: (params: {
54
- nodeId: string;
55
- command: string;
56
- params?: unknown;
57
- timeoutMs?: number;
58
- }) => Promise<unknown>;
59
- runCommandWithTimeoutHandler?: (
60
- argv: string[],
61
- options?: { timeoutMs?: number },
62
- ) => Promise<CommandResult>;
63
- registerPlatform?: NodeJS.Platform;
64
- toolContext?: Record<string, unknown>;
65
- } = {},
66
- ) {
67
- const methods = new Map<string, unknown>();
68
- const tools: unknown[] = [];
69
- const cliRegistrations: unknown[] = [];
70
- const nodeHostCommands: unknown[] = [];
71
- const nodesList = vi.fn(
72
- async () =>
73
- options.nodesListResult ?? {
74
- nodes: [
75
- {
76
- nodeId: "node-1",
77
- displayName: "parallels-macos",
78
- connected: true,
79
- caps: ["browser"],
80
- commands: ["browser.proxy", "googlemeet.chrome"],
81
- },
82
- ],
83
- },
84
- );
85
- const nodesInvoke = vi.fn(async (params) => {
86
- if (options.nodesInvokeHandler) {
87
- return options.nodesInvokeHandler(params);
88
- }
89
- if (params.command === "browser.proxy") {
90
- const proxy = params.params as { path?: string; body?: { url?: string; targetId?: string } };
91
- if (proxy.path === "/tabs") {
92
- return { payload: { result: { running: true, tabs: [] } } };
93
- }
94
- if (proxy.path === "/tabs/open") {
95
- return {
96
- payload: {
97
- result: {
98
- targetId: "tab-1",
99
- title: "Meet",
100
- url: proxy.body?.url ?? "https://meet.google.com/abc-defg-hij",
101
- },
102
- },
103
- };
104
- }
105
- if (proxy.path === "/act") {
106
- return {
107
- payload: {
108
- result: {
109
- ok: true,
110
- targetId: proxy.body?.targetId ?? "tab-1",
111
- result: JSON.stringify(
112
- options.browserActResult ?? {
113
- inCall: true,
114
- micMuted: false,
115
- title: "Meet call",
116
- url: "https://meet.google.com/abc-defg-hij",
117
- },
118
- ),
119
- },
120
- },
121
- };
122
- }
123
- return { payload: { result: { ok: true } } };
124
- }
125
- return options.nodesInvokeResult ?? { launched: true };
126
- });
127
- const runCommandWithTimeout = vi.fn(
128
- async (argv: string[], runOptions?: { timeoutMs?: number }) => {
129
- if (options.runCommandWithTimeoutHandler) {
130
- return options.runCommandWithTimeoutHandler(argv, runOptions);
131
- }
132
- if (argv[0] === "/usr/sbin/system_profiler") {
133
- return { code: 0, stdout: "BlackHole 2ch", stderr: "" };
134
- }
135
- return { code: 0, stdout: "", stderr: "" };
136
- },
137
- );
138
- const api = createTestPluginApi({
139
- id: "google-meet",
140
- name: "Google Meet",
141
- description: "test",
142
- version: "0",
143
- source: "test",
144
- config: options.fullConfig ?? {},
145
- pluginConfig: config,
146
- runtime: {
147
- system: {
148
- runCommandWithTimeout,
149
- formatNativeDependencyHint: vi.fn(() => "Install with brew install blackhole-2ch."),
150
- },
151
- nodes: {
152
- list: nodesList,
153
- invoke: nodesInvoke,
154
- },
155
- } as unknown as KlawPluginApi["runtime"],
156
- logger: noopLogger,
157
- registerGatewayMethod: (method: string, handler: unknown) => methods.set(method, handler),
158
- registerTool: (tool: unknown) => {
159
- tools.push(
160
- typeof tool === "function"
161
- ? (tool as (ctx: Record<string, unknown>) => unknown)(options.toolContext ?? {})
162
- : tool,
163
- );
164
- },
165
- registerCli: (_registrar: unknown, opts: unknown) => cliRegistrations.push(opts),
166
- registerNodeHostCommand: (command: unknown) => nodeHostCommands.push(command),
167
- });
168
- const originalPlatform = process.platform;
169
- Object.defineProperty(process, "platform", {
170
- configurable: true,
171
- value: options.registerPlatform ?? "darwin",
172
- });
173
- try {
174
- plugin.register(api);
175
- } finally {
176
- Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
177
- }
178
- return {
179
- cliRegistrations,
180
- methods,
181
- tools,
182
- runCommandWithTimeout,
183
- nodesList,
184
- nodesInvoke,
185
- nodeHostCommands,
186
- };
187
- }
188
-
189
- export async function invokeGoogleMeetGatewayMethodForTest(
190
- methods: Map<string, unknown>,
191
- method: string,
192
- params?: unknown,
193
- ): Promise<unknown> {
194
- const handler = methods.get(method) as
195
- | ((opts: {
196
- params: Record<string, unknown>;
197
- respond: (
198
- ok: boolean,
199
- payload?: unknown,
200
- error?: { message?: string; details?: unknown },
201
- ) => void;
202
- }) => Promise<void> | void)
203
- | undefined;
204
- if (!handler) {
205
- throw new Error(`gateway method not registered: ${method}`);
206
- }
207
- return await new Promise((resolve, reject) => {
208
- const respond = (
209
- ok: boolean,
210
- payload?: unknown,
211
- error?: { message?: string; details?: unknown },
212
- ) => {
213
- if (ok) {
214
- resolve(payload);
215
- return;
216
- }
217
- const err = new Error(error?.message ?? "gateway request failed") as Error & {
218
- details?: unknown;
219
- };
220
- err.details = error?.details ?? payload;
221
- reject(err);
222
- };
223
- void Promise.resolve(
224
- handler({
225
- params: (params && typeof params === "object" && !Array.isArray(params)
226
- ? params
227
- : {}) as Record<string, unknown>,
228
- respond,
229
- }),
230
- ).catch(reject);
231
- });
232
- }
@@ -1,39 +0,0 @@
1
- import type { PluginRuntime } from "klaw/plugin-sdk/plugin-runtime";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { callBrowserProxyOnNode } from "./chrome-browser-proxy.js";
4
-
5
- describe("Google Meet Chrome browser proxy", () => {
6
- it("reports malformed node proxy payloadJSON with an owned error", async () => {
7
- const invoke = vi.fn(async () => ({
8
- ok: true,
9
- payloadJSON: "{not json",
10
- }));
11
- const runtime = {
12
- nodes: {
13
- invoke,
14
- },
15
- } as unknown as PluginRuntime;
16
-
17
- await expect(
18
- callBrowserProxyOnNode({
19
- runtime,
20
- nodeId: "node-1",
21
- method: "GET",
22
- path: "/tabs",
23
- timeoutMs: 100,
24
- }),
25
- ).rejects.toThrow("Google Meet browser proxy returned malformed payloadJSON.");
26
-
27
- expect(invoke).toHaveBeenCalledWith({
28
- nodeId: "node-1",
29
- command: "browser.proxy",
30
- params: {
31
- method: "GET",
32
- path: "/tabs",
33
- body: undefined,
34
- timeoutMs: 100,
35
- },
36
- timeoutMs: 5_100,
37
- });
38
- });
39
- });