mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
Add Google Meet space access controls
This commit is contained in:
committed by
Peter Steinberger
parent
53c4217110
commit
f2c1a56bbd
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -557,6 +557,7 @@ describe("google-meet plugin", () => {
|
||||
"export",
|
||||
"recover_current_tab",
|
||||
"leave",
|
||||
"end_active_conference",
|
||||
"speak",
|
||||
"test_speech",
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user