Add Google Meet space access controls

This commit is contained in:
BSnizND
2026-04-29 21:11:23 -07:00
committed by Peter Steinberger
parent 53c4217110
commit f2c1a56bbd
10 changed files with 386 additions and 13 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Providers/xAI: add Grok 4.3 to the bundled catalog and make it the default xAI chat model.
- Google Meet: let API-created rooms set `accessType` and `entryPointAccess`, and add `googlemeet end-active-conference` for closing managed spaces after a call. (#74824) Thanks @BsnizND.
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
- Plugins/ClawHub: allow official bundled-plugin cutovers to prefer ClawHub installs with npm fallback only when the ClawHub package or version is absent. Thanks @vincentkoc.

View File

@@ -125,6 +125,24 @@ Create a new meeting and join it:
openclaw googlemeet create --transport chrome-node --mode realtime
```
For API-created rooms, use Google Meet `SpaceConfig.accessType` when you want
the room's no-knock policy to be explicit instead of inherited from the Google
account defaults:
```bash
openclaw googlemeet create --access-type OPEN --transport chrome-node --mode realtime
```
`OPEN` lets anyone with the Meet URL join without knocking. `TRUSTED` lets the
host organization's trusted users, invited external users, and dial-in users
join without knocking. `RESTRICTED` limits no-knock entry to invitees. These
settings only apply to the official Google Meet API creation path, so OAuth
credentials must be configured.
If you authenticated Google Meet before this option was available, rerun
`openclaw googlemeet auth login --json` after adding the
`meetings.space.settings` scope to your Google OAuth consent screen.
Create only the URL without joining:
```bash
@@ -504,6 +522,7 @@ In Google Cloud Console:
4. Add the scopes OpenClaw requests:
- `https://www.googleapis.com/auth/meetings.space.created`
- `https://www.googleapis.com/auth/meetings.space.readonly`
- `https://www.googleapis.com/auth/meetings.space.settings`
- `https://www.googleapis.com/auth/meetings.conference.media.readonly`
5. Create an OAuth client ID.
- Application type: **Web application**.
@@ -517,6 +536,8 @@ In Google Cloud Console:
`meetings.space.created` is required by Google Meet `spaces.create`.
`meetings.space.readonly` lets OpenClaw resolve Meet URLs/codes to spaces.
`meetings.space.settings` lets OpenClaw pass `SpaceConfig` settings such as
`accessType` during API room creation.
`meetings.conference.media.readonly` is for Meet Media API preflight and media
work; Google may require Developer Preview enrollment for actual Media API use.
If you only need browser-based Chrome joins, skip OAuth entirely.
@@ -708,6 +729,21 @@ openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --jso
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
```
End an active conference for an API-created space when you want to close the
room after the call:
```bash
openclaw googlemeet end-active-conference https://meet.google.com/abc-defg-hij
```
This calls Google Meet `spaces.endActiveConference` and requires OAuth with the
`meetings.space.created` scope for a space the authorized account can manage.
OpenClaw accepts a Meet URL, meeting code, or `spaces/{id}` input and resolves it
to the API space resource before ending the active conference.
It is separate from `googlemeet leave`: `leave` stops OpenClaw's local/session
participation, while `end-active-conference` asks Google Meet to end the active
conference for the space.
Write a readable report:
```bash
@@ -764,6 +800,26 @@ Agents can also create the same bundle through the `google_meet` tool:
Set `"dryRun": true` to return only the export manifest and skip file writes.
Agents can also create an API-backed room with an explicit access policy:
```json
{
"action": "create",
"transport": "chrome-node",
"mode": "realtime",
"accessType": "OPEN"
}
```
And they can end the active conference for a known room:
```json
{
"action": "end_active_conference",
"meeting": "https://meet.google.com/abc-defg-hij"
}
```
Run the guarded live smoke against a real retained meeting:
```bash
@@ -1502,6 +1558,8 @@ argument list, and do not point it at scripts from untrusted locations.
`googlemeet speak` triggers the active realtime audio bridge for a Chrome
session. `googlemeet leave` stops that bridge. For Twilio sessions delegated
through the Voice Call plugin, `leave` also hangs up the underlying voice call.
Use `googlemeet end-active-conference` when you also want to close the active
Google Meet conference for an API-managed space.
## Related

View File

@@ -108,7 +108,7 @@ describe("google-meet create flow", () => {
googleMeetPluginTesting.setCallGatewayFromCliForTests();
});
it("CLI create prints the new meeting URL", async () => {
it("CLI create can configure API-created space access", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
const url = input instanceof Request ? input.url : input.toString();
if (url.includes("oauth2.googleapis.com")) {
@@ -142,9 +142,27 @@ describe("google-meet create flow", () => {
});
try {
await program.parseAsync(["googlemeet", "create", "--no-join"], { from: "user" });
await program.parseAsync(
[
"googlemeet",
"create",
"--no-join",
"--access-type",
"OPEN",
"--entry-point-access",
"ALL",
],
{ from: "user" },
);
expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
expect(stdout.output()).toContain("space: spaces/new-space");
expect(fetchMock).toHaveBeenCalledWith(
"https://meet.googleapis.com/v2/spaces",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),
}),
);
} finally {
stdout.restore();
}
@@ -220,6 +238,27 @@ describe("google-meet create flow", () => {
);
});
it("rejects access policy flags when tool create would use browser fallback", async () => {
const { methods } = setup(
{
defaultTransport: "chrome-node",
chromeNode: { node: "parallels-macos" },
},
{
nodesInvokeHandler: async () => {
throw new Error("browser fallback should not run");
},
},
);
await expect(
invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.create", {
join: false,
accessType: "OPEN",
}),
).rejects.toThrow("access policy options require OAuth/API room creation");
});
it("reports structured manual action when browser creation needs Google login", async () => {
const { methods } = setup(
{

View File

@@ -557,6 +557,7 @@ describe("google-meet plugin", () => {
"export",
"recover_current_tab",
"leave",
"end_active_conference",
"speak",
"test_speech",
],

View File

@@ -22,6 +22,7 @@ import {
} from "./src/config.js";
import {
buildGoogleMeetPreflightReport,
endGoogleMeetActiveConference,
fetchGoogleMeetArtifacts,
fetchGoogleMeetAttendance,
fetchLatestGoogleMeetConferenceRecord,
@@ -201,6 +202,7 @@ const GoogleMeetToolSchema = Type.Object({
"export",
"recover_current_tab",
"leave",
"end_active_conference",
"speak",
"test_speech",
],
@@ -212,6 +214,19 @@ const GoogleMeetToolSchema = Type.Object({
description: "For action=create, set false to create the URL without joining.",
}),
),
accessType: Type.Optional(
Type.String({
enum: ["OPEN", "TRUSTED", "RESTRICTED"],
description:
"For action=create with Google Meet OAuth, configure who can join without knocking.",
}),
),
entryPointAccess: Type.Optional(
Type.String({
enum: ["ALL", "CREATOR_APP_ONLY"],
description: "For action=create with Google Meet OAuth, configure allowed join entry points.",
}),
),
url: Type.Optional(Type.String({ description: "Explicit https://meet.google.com/... URL" })),
transport: Type.Optional(
Type.String({ enum: ["chrome", "chrome-node", "twilio"], description: "Join transport" }),
@@ -343,6 +358,7 @@ type GoogleMeetGatewayToolAction =
| "recover_current_tab"
| "setup_status"
| "leave"
| "end_active_conference"
| "speak"
| "test_speech";
@@ -354,6 +370,8 @@ function googleMeetGatewayMethodForToolAction(action: GoogleMeetGatewayToolActio
return "googlemeet.setup";
case "test_speech":
return "googlemeet.testSpeech";
case "end_active_conference":
return "googlemeet.endActiveConference";
default:
return `googlemeet.${action}`;
}
@@ -842,6 +860,25 @@ export default definePluginEntry({
},
);
api.registerGatewayMethod(
"googlemeet.endActiveConference",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asParamRecord(params);
const token = await resolveGoogleMeetTokenFromParams(config, raw);
respond(
true,
await endGoogleMeetActiveConference({
accessToken: token.accessToken,
meeting: resolveMeetingInput(config, raw.meeting),
}),
);
} catch (err) {
sendError(respond, err);
}
},
);
api.registerGatewayMethod(
"googlemeet.speak",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
@@ -999,6 +1036,15 @@ export default definePluginEntry({
}
return json(await callGoogleMeetGatewayFromTool({ config, action: "leave", raw }));
}
case "end_active_conference": {
return json(
await callGoogleMeetGatewayFromTool({
config,
action: "end_active_conference",
raw,
}),
);
}
case "speak": {
const sessionId = normalizeOptionalString(raw.sessionId);
if (!sessionId) {

View File

@@ -324,6 +324,64 @@ describe("google-meet CLI", () => {
}
});
it("ends an active conference for a Meet space", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
const url = requestUrl(input);
if (url.pathname === "/v2/spaces/abc-defg-hij") {
return jsonResponse({
name: "spaces/space-resource-123",
meetingCode: "abc-defg-hij",
meetingUri: "https://meet.google.com/abc-defg-hij",
});
}
if (url.pathname === "/v2/spaces/space-resource-123:endActiveConference") {
return jsonResponse({});
}
return new Response("not found", { status: 404 });
});
vi.stubGlobal("fetch", fetchMock);
const stdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet",
"end-active-conference",
"https://meet.google.com/abc-defg-hij",
"--access-token",
"token",
"--expires-at",
String(Date.now() + 120_000),
"--json",
],
{ from: "user" },
);
expect(JSON.parse(stdout.output())).toMatchObject({
space: "spaces/space-resource-123",
ended: true,
tokenSource: "cached-access-token",
});
expect(fetchMock).toHaveBeenCalledWith(
"https://meet.googleapis.com/v2/spaces/space-resource-123:endActiveConference",
expect.objectContaining({ method: "POST", body: "{}" }),
);
} finally {
stdout.restore();
}
});
it("rejects access policy flags when create would use browser fallback", async () => {
await expect(
setupCli({
runtime: {
createViaBrowser: vi.fn(async () => {
throw new Error("browser fallback should not run");
}),
},
}).parseAsync(["googlemeet", "create", "--access-type", "OPEN"], { from: "user" }),
).rejects.toThrow("access policy options require OAuth/API room creation");
});
it("prints the latest conference record", async () => {
stubMeetArtifactsApi();
const stdout = captureStdout();

View File

@@ -10,9 +10,11 @@ import {
type GoogleMeetCalendarLookupResult,
} from "./calendar.js";
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
import { hasCreateSpaceConfigInput, resolveCreateSpaceConfig } from "./create.js";
import {
buildGoogleMeetPreflightReport,
createGoogleMeetSpace,
endGoogleMeetActiveConference,
fetchGoogleMeetArtifacts,
fetchGoogleMeetAttendance,
fetchLatestGoogleMeetConferenceRecord,
@@ -159,6 +161,8 @@ type CreateOptions = {
clientId?: string;
clientSecret?: string;
expiresAt?: string;
accessType?: string;
entryPointAccess?: string;
join?: boolean;
transport?: GoogleMeetTransport;
mode?: GoogleMeetMode;
@@ -1367,6 +1371,14 @@ export function registerGoogleMeetCli(params: {
.option("--client-id <id>", "OAuth client id override")
.option("--client-secret <secret>", "OAuth client secret override")
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
.option(
"--access-type <type>",
"Google Meet SpaceConfig accessType for API create: OPEN, TRUSTED, or RESTRICTED",
)
.option(
"--entry-point-access <type>",
"Google Meet SpaceConfig entryPointAccess for API create: ALL or CREATOR_APP_ONLY",
)
.option("--no-join", "Only create the meeting URL; do not join it")
.option("--transport <transport>", "Join transport: chrome, chrome-node, or twilio")
.option(
@@ -1380,6 +1392,11 @@ export function registerGoogleMeetCli(params: {
.option("--json", "Print JSON output", false)
.action(async (options: CreateOptions) => {
if (!hasCreateOAuth(params.config, options)) {
if (hasCreateSpaceConfigInput(options as Record<string, unknown>)) {
throw new Error(
"Google Meet access policy options require OAuth/API room creation. Configure Google Meet OAuth or remove --access-type/--entry-point-access.",
);
}
const rt = await params.ensureRuntime();
const result = await rt.createViaBrowser();
const join =
@@ -1423,7 +1440,10 @@ export function registerGoogleMeetCli(params: {
const token = await resolveGoogleMeetAccessToken(
resolveCreateTokenOptions(params.config, options),
);
const result = await createGoogleMeetSpace({ accessToken: token.accessToken });
const result = await createGoogleMeetSpace({
accessToken: token.accessToken,
config: resolveCreateSpaceConfig(options as Record<string, unknown>),
});
const join =
options.join !== false
? await (
@@ -1463,6 +1483,39 @@ export function registerGoogleMeetCli(params: {
}
});
root
.command("end-active-conference")
.description("End the active conference for a Google Meet space")
.argument("[meeting]", "Meet URL, meeting code, or spaces/{id}")
.option("--access-token <token>", "Access token override")
.option("--refresh-token <token>", "Refresh token override")
.option("--client-id <id>", "OAuth client id override")
.option("--client-secret <secret>", "OAuth client secret override")
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
.option("--json", "Print JSON output", false)
.action(async (meeting: string | undefined, options: ResolveSpaceOptions & JsonOptions) => {
const token = await resolveGoogleMeetAccessToken(
resolveOAuthTokenOptions(params.config, options),
);
const result = await endGoogleMeetActiveConference({
accessToken: token.accessToken,
meeting: resolveMeetingInput(params.config, meeting ?? options.meeting),
});
if (options.json) {
writeStdoutJson({
...result,
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
});
return;
}
writeStdoutLine("space: %s", result.space);
writeStdoutLine("ended: yes");
writeStdoutLine(
"token source: %s",
token.refreshed ? "refresh-token" : "cached-access-token",
);
});
root
.command("join")
.argument("[url]", "Explicit https://meet.google.com/... URL")

View File

@@ -1,7 +1,12 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
import { createGoogleMeetSpace } from "./meet.js";
import {
createGoogleMeetSpace,
type GoogleMeetAccessType,
type GoogleMeetEntryPointAccess,
type GoogleMeetSpaceConfig,
} from "./meet.js";
import { resolveGoogleMeetAccessToken } from "./oauth.js";
import type { GoogleMeetRuntime } from "./runtime.js";
import { createMeetWithBrowserProxyOnNode } from "./transports/chrome-create.js";
@@ -14,6 +19,47 @@ function normalizeMode(value: unknown): GoogleMeetMode | undefined {
return value === "realtime" || value === "transcribe" ? value : undefined;
}
export function normalizeGoogleMeetAccessType(value: unknown): GoogleMeetAccessType | undefined {
const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
return normalized === "OPEN" || normalized === "TRUSTED" || normalized === "RESTRICTED"
? normalized
: undefined;
}
export function normalizeGoogleMeetEntryPointAccess(
value: unknown,
): GoogleMeetEntryPointAccess | undefined {
const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
return normalized === "ALL" || normalized === "CREATOR_APP_ONLY" ? normalized : undefined;
}
export function resolveCreateSpaceConfig(
raw: Record<string, unknown>,
): GoogleMeetSpaceConfig | undefined {
const rawAccessType = normalizeOptionalString(raw.accessType);
const rawEntryPointAccess = normalizeOptionalString(raw.entryPointAccess);
const accessType = normalizeGoogleMeetAccessType(raw.accessType);
const entryPointAccess = normalizeGoogleMeetEntryPointAccess(raw.entryPointAccess);
if (rawAccessType !== undefined && !accessType) {
throw new Error("Invalid Google Meet accessType. Expected OPEN, TRUSTED, or RESTRICTED.");
}
if (rawEntryPointAccess !== undefined && !entryPointAccess) {
throw new Error("Invalid Google Meet entryPointAccess. Expected ALL or CREATOR_APP_ONLY.");
}
const config = {
...(accessType ? { accessType } : {}),
...(entryPointAccess ? { entryPointAccess } : {}),
};
return Object.keys(config).length > 0 ? config : undefined;
}
export function hasCreateSpaceConfigInput(raw: Record<string, unknown>): boolean {
return (
normalizeOptionalString(raw.accessType) !== undefined ||
normalizeOptionalString(raw.entryPointAccess) !== undefined
);
}
async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
const token = await resolveGoogleMeetAccessToken({
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
@@ -22,7 +68,10 @@ async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<strin
accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt,
});
const result = await createGoogleMeetSpace({ accessToken: token.accessToken });
const result = await createGoogleMeetSpace({
accessToken: token.accessToken,
config: resolveCreateSpaceConfig(raw),
});
return { source: "api" as const, token, ...result };
}
@@ -53,6 +102,11 @@ export async function createMeetFromParams(params: {
"URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
};
}
if (hasCreateSpaceConfigInput(params.raw)) {
throw new Error(
"Google Meet access policy options require OAuth/API room creation. Configure Google Meet OAuth or remove accessType/entryPointAccess.",
);
}
const browser = await createMeetWithBrowserProxyOnNode({
runtime: params.runtime,
config: params.config,

View File

@@ -9,16 +9,26 @@ const GOOGLE_MEET_API_HOST = "meet.googleapis.com";
const GOOGLE_MEET_MEDIA_SCOPE =
"https://www.googleapis.com/auth/meetings.conference.media.readonly";
const GOOGLE_MEET_SPACE_SCOPE = "https://www.googleapis.com/auth/meetings.space.readonly";
const GOOGLE_MEET_SPACE_CREATED_SCOPE = "https://www.googleapis.com/auth/meetings.space.created";
const GOOGLE_MEET_SPACE_SETTINGS_SCOPE = "https://www.googleapis.com/auth/meetings.space.settings";
type GoogleMeetSpace = {
export type GoogleMeetAccessType = "OPEN" | "TRUSTED" | "RESTRICTED";
export type GoogleMeetEntryPointAccess = "ALL" | "CREATOR_APP_ONLY";
export type GoogleMeetSpaceConfig = {
accessType?: GoogleMeetAccessType;
entryPointAccess?: GoogleMeetEntryPointAccess;
};
export type GoogleMeetSpace = {
name: string;
meetingCode?: string;
meetingUri?: string;
activeConference?: Record<string, unknown>;
config?: Record<string, unknown>;
config?: GoogleMeetSpaceConfig & Record<string, unknown>;
};
type GoogleMeetPreflightReport = {
export type GoogleMeetPreflightReport = {
input: string;
resolvedSpaceName: string;
meetingCode?: string;
@@ -29,12 +39,17 @@ type GoogleMeetPreflightReport = {
blockers: string[];
};
type GoogleMeetCreateSpaceResult = {
export type GoogleMeetCreateSpaceResult = {
space: GoogleMeetSpace;
meetingUri: string;
};
type GoogleMeetConferenceRecord = {
export type GoogleMeetEndActiveConferenceResult = {
space: string;
ended: true;
};
export type GoogleMeetConferenceRecord = {
name: string;
space?: string;
startTime?: string;
@@ -353,7 +368,12 @@ export async function fetchGoogleMeetSpace(params: {
export async function createGoogleMeetSpace(params: {
accessToken: string;
config?: GoogleMeetSpaceConfig;
}): Promise<GoogleMeetCreateSpaceResult> {
const body =
params.config && Object.keys(params.config).length > 0
? JSON.stringify({ config: params.config })
: "{}";
const { response, release } = await fetchWithSsrFGuard({
url: `${GOOGLE_MEET_API_BASE_URL}/spaces`,
init: {
@@ -363,7 +383,7 @@ export async function createGoogleMeetSpace(params: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: "{}",
body,
},
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
auditContext: "google-meet.spaces.create",
@@ -375,7 +395,10 @@ export async function createGoogleMeetSpace(params: {
response,
detail,
prefix: "Google Meet spaces.create",
scopes: ["https://www.googleapis.com/auth/meetings.space.created"],
scopes:
params.config && Object.keys(params.config).length > 0
? [GOOGLE_MEET_SPACE_CREATED_SCOPE, GOOGLE_MEET_SPACE_SETTINGS_SCOPE]
: [GOOGLE_MEET_SPACE_CREATED_SCOPE],
});
}
const payload = (await response.json()) as GoogleMeetSpace;
@@ -392,7 +415,46 @@ export async function createGoogleMeetSpace(params: {
}
}
async function fetchGoogleMeetConferenceRecord(params: {
export async function endGoogleMeetActiveConference(params: {
accessToken: string;
meeting: string;
}): Promise<GoogleMeetEndActiveConferenceResult> {
const resolved = await fetchGoogleMeetSpace({
accessToken: params.accessToken,
meeting: params.meeting,
});
const space = resolved.name;
const { response, release } = await fetchWithSsrFGuard({
url: `${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(space)}:endActiveConference`,
init: {
method: "POST",
headers: {
Authorization: `Bearer ${params.accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: "{}",
},
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
auditContext: "google-meet.spaces.endActiveConference",
});
try {
if (!response.ok) {
const detail = await response.text();
throw await googleApiError({
response,
detail,
prefix: "Google Meet spaces.endActiveConference",
scopes: [GOOGLE_MEET_SPACE_CREATED_SCOPE],
});
}
return { space, ended: true };
} finally {
await release();
}
}
export async function fetchGoogleMeetConferenceRecord(params: {
accessToken: string;
conferenceRecord: string;
}): Promise<GoogleMeetConferenceRecord> {

View File

@@ -13,6 +13,7 @@ const GOOGLE_MEET_TOKEN_HOST = "oauth2.googleapis.com";
export const GOOGLE_MEET_SCOPES = [
"https://www.googleapis.com/auth/meetings.space.created",
"https://www.googleapis.com/auth/meetings.space.readonly",
"https://www.googleapis.com/auth/meetings.space.settings",
"https://www.googleapis.com/auth/meetings.conference.media.readonly",
"https://www.googleapis.com/auth/calendar.events.readonly",
"https://www.googleapis.com/auth/drive.meet.readonly",