mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:50:43 +00:00
feat(google-meet): add oauth doctor
This commit is contained in:
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Plugins/Google Meet: default Chrome realtime sessions to OpenAI plus SoX `rec`/`play` audio bridge commands, so the usual setup only needs the plugin enabled and `OPENAI_API_KEY`. Thanks @steipete.
|
- Plugins/Google Meet: default Chrome realtime sessions to OpenAI plus SoX `rec`/`play` audio bridge commands, so the usual setup only needs the plugin enabled and `OPENAI_API_KEY`. Thanks @steipete.
|
||||||
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
|
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
|
||||||
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts, smart notes, and participant sessions. Thanks @steipete.
|
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts, smart notes, and participant sessions. Thanks @steipete.
|
||||||
|
- Plugins/Google Meet: add `googlemeet doctor --oauth` so operators can verify OAuth token refresh, Meet space reads, and side-effecting space creation without printing secrets. Thanks @steipete.
|
||||||
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
|
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
|
||||||
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.
|
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.
|
||||||
- Plugins/Google Meet: add `googlemeet doctor` and a `recover_current_tab`/`recover-tab` flow so agents can inspect an already-open Meet tab and report the blocker without opening another window. Thanks @steipete.
|
- Plugins/Google Meet: add `googlemeet doctor` and a `recover_current_tab`/`recover-tab` flow so agents can inspect an already-open Meet tab and report the blocker without opening another window. Thanks @steipete.
|
||||||
|
|||||||
@@ -437,8 +437,51 @@ OAuth is optional for creating a Meet link because `googlemeet create` can fall
|
|||||||
back to browser automation. Configure OAuth when you want official API create,
|
back to browser automation. Configure OAuth when you want official API create,
|
||||||
space resolution, or Meet Media API preflight checks.
|
space resolution, or Meet Media API preflight checks.
|
||||||
|
|
||||||
Google Meet API access uses a personal OAuth client first. Configure
|
Google Meet API access uses user OAuth: create a Google Cloud OAuth client,
|
||||||
`oauth.clientId` and optionally `oauth.clientSecret`, then run:
|
request the required scopes, authorize a Google account, then store the
|
||||||
|
resulting refresh token in the Google Meet plugin config or provide the
|
||||||
|
`OPENCLAW_GOOGLE_MEET_*` environment variables.
|
||||||
|
|
||||||
|
OAuth does not replace the Chrome join path. Chrome and Chrome-node transports
|
||||||
|
still join through a signed-in Chrome profile, BlackHole/SoX, and a connected
|
||||||
|
node when you use browser participation. OAuth is only for the official Google
|
||||||
|
Meet API path: create meeting spaces, resolve spaces, and run Meet Media API
|
||||||
|
preflight checks.
|
||||||
|
|
||||||
|
### Create Google credentials
|
||||||
|
|
||||||
|
In Google Cloud Console:
|
||||||
|
|
||||||
|
1. Create or select a Google Cloud project.
|
||||||
|
2. Enable **Google Meet REST API** for that project.
|
||||||
|
3. Configure the OAuth consent screen.
|
||||||
|
- **Internal** is simplest for a Google Workspace organization.
|
||||||
|
- **External** works for personal/test setups; while the app is in Testing,
|
||||||
|
add each Google account that will authorize the app as a test user.
|
||||||
|
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.conference.media.readonly`
|
||||||
|
5. Create an OAuth client ID.
|
||||||
|
- Application type: **Web application**.
|
||||||
|
- Authorized redirect URI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8085/oauth2callback
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Copy the client ID and client secret.
|
||||||
|
|
||||||
|
`meetings.space.created` is required by Google Meet `spaces.create`.
|
||||||
|
`meetings.space.readonly` lets OpenClaw resolve Meet URLs/codes to spaces.
|
||||||
|
`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.
|
||||||
|
|
||||||
|
### Mint the refresh token
|
||||||
|
|
||||||
|
Configure `oauth.clientId` and optionally `oauth.clientSecret`, or pass them as
|
||||||
|
environment variables, then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openclaw googlemeet auth login --json
|
openclaw googlemeet auth login --json
|
||||||
@@ -448,11 +491,116 @@ The command prints an `oauth` config block with a refresh token. It uses PKCE,
|
|||||||
localhost callback on `http://localhost:8085/oauth2callback`, and a manual
|
localhost callback on `http://localhost:8085/oauth2callback`, and a manual
|
||||||
copy/paste flow with `--manual`.
|
copy/paste flow with `--manual`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENCLAW_GOOGLE_MEET_CLIENT_ID="your-client-id" \
|
||||||
|
OPENCLAW_GOOGLE_MEET_CLIENT_SECRET="your-client-secret" \
|
||||||
|
openclaw googlemeet auth login --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use manual mode when the browser cannot reach the local callback:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENCLAW_GOOGLE_MEET_CLIENT_ID="your-client-id" \
|
||||||
|
OPENCLAW_GOOGLE_MEET_CLIENT_SECRET="your-client-secret" \
|
||||||
|
openclaw googlemeet auth login --json --manual
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON output includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"oauth": {
|
||||||
|
"clientId": "your-client-id",
|
||||||
|
"clientSecret": "your-client-secret",
|
||||||
|
"refreshToken": "refresh-token",
|
||||||
|
"accessToken": "access-token",
|
||||||
|
"expiresAt": 1770000000000
|
||||||
|
},
|
||||||
|
"scope": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Store the `oauth` object under the Google Meet plugin config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
"google-meet": {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
oauth: {
|
||||||
|
clientId: "your-client-id",
|
||||||
|
clientSecret: "your-client-secret",
|
||||||
|
refreshToken: "refresh-token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer environment variables when you do not want the refresh token in config.
|
||||||
|
If both config and environment values are present, the plugin resolves config
|
||||||
|
first and then environment fallback.
|
||||||
|
|
||||||
The OAuth consent includes Meet space creation, Meet space read access, and Meet
|
The OAuth consent includes Meet space creation, Meet space read access, and Meet
|
||||||
conference media read access. If you authenticated before meeting creation
|
conference media read access. If you authenticated before meeting creation
|
||||||
support existed, rerun `openclaw googlemeet auth login --json` so the refresh
|
support existed, rerun `openclaw googlemeet auth login --json` so the refresh
|
||||||
token has the `meetings.space.created` scope.
|
token has the `meetings.space.created` scope.
|
||||||
|
|
||||||
|
### Verify OAuth with doctor
|
||||||
|
|
||||||
|
Run the OAuth doctor when you want a fast, non-secret health check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw googlemeet doctor --oauth --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This does not load the Chrome runtime or require a connected Chrome node. It
|
||||||
|
checks that OAuth config exists and that the refresh token can mint an access
|
||||||
|
token. The JSON report includes only status fields such as `ok`, `configured`,
|
||||||
|
`tokenSource`, `expiresAt`, and check messages; it does not print the access
|
||||||
|
token, refresh token, or client secret.
|
||||||
|
|
||||||
|
Common results:
|
||||||
|
|
||||||
|
| Check | Meaning |
|
||||||
|
| -------------------- | --------------------------------------------------------------------------------------- |
|
||||||
|
| `oauth-config` | `oauth.clientId` plus `oauth.refreshToken`, or a cached access token, is present. |
|
||||||
|
| `oauth-token` | The cached access token is still valid, or the refresh token minted a new access token. |
|
||||||
|
| `meet-spaces-get` | Optional `--meeting` check resolved an existing Meet space. |
|
||||||
|
| `meet-spaces-create` | Optional `--create-space` check created a new Meet space. |
|
||||||
|
|
||||||
|
To prove Google Meet API enablement and `spaces.create` scope as well, run the
|
||||||
|
side-effecting create check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw googlemeet doctor --oauth --create-space --json
|
||||||
|
openclaw googlemeet create --no-join --json
|
||||||
|
```
|
||||||
|
|
||||||
|
`--create-space` creates a throwaway Meet URL. Use it when you need to confirm
|
||||||
|
that the Google Cloud project has the Meet API enabled and that the authorized
|
||||||
|
account has the `meetings.space.created` scope.
|
||||||
|
|
||||||
|
To prove read access for an existing meeting space:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw googlemeet doctor --oauth --meeting https://meet.google.com/abc-defg-hij --json
|
||||||
|
openclaw googlemeet resolve-space --meeting https://meet.google.com/abc-defg-hij
|
||||||
|
```
|
||||||
|
|
||||||
|
`doctor --oauth --meeting` and `resolve-space` prove read access to an existing
|
||||||
|
space that the authorized Google account can access. A `403` from these checks
|
||||||
|
usually means the Google Meet REST API is disabled, the consented refresh token
|
||||||
|
is missing the required scope, or the Google account cannot access that Meet
|
||||||
|
space. A refresh-token error means rerun `openclaw googlemeet auth login
|
||||||
|
--json` and store the new `oauth` block.
|
||||||
|
|
||||||
No OAuth credentials are needed for the browser fallback. In that mode, Google
|
No OAuth credentials are needed for the browser fallback. In that mode, Google
|
||||||
auth comes from the signed-in Chrome profile on the selected node, not from
|
auth comes from the signed-in Chrome profile on the selected node, not from
|
||||||
OpenClaw config.
|
OpenClaw config.
|
||||||
@@ -967,7 +1115,10 @@ Also verify:
|
|||||||
`googlemeet doctor [session-id]` prints the session, node, in-call state,
|
`googlemeet doctor [session-id]` prints the session, node, in-call state,
|
||||||
manual action reason, realtime provider connection, `realtimeReady`, audio
|
manual action reason, realtime provider connection, `realtimeReady`, audio
|
||||||
input/output activity, last audio timestamps, byte counters, and browser URL.
|
input/output activity, last audio timestamps, byte counters, and browser URL.
|
||||||
Use `googlemeet status [session-id]` when you need the raw JSON.
|
Use `googlemeet status [session-id]` when you need the raw JSON. Use
|
||||||
|
`googlemeet doctor --oauth` when you need to verify Google Meet OAuth refresh
|
||||||
|
without exposing tokens; add `--meeting` or `--create-space` when you need a
|
||||||
|
Google Meet API proof as well.
|
||||||
|
|
||||||
If an agent timed out and you can see a Meet tab already open, inspect that tab
|
If an agent timed out and you can see a Meet tab already open, inspect that tab
|
||||||
without opening another one:
|
without opening another one:
|
||||||
|
|||||||
@@ -966,6 +966,121 @@ describe("google-meet plugin", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("CLI doctor verifies Google Meet OAuth refresh without printing secrets", async () => {
|
||||||
|
const program = new Command();
|
||||||
|
const stdout = captureStdout();
|
||||||
|
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
access_token: "new-access-token",
|
||||||
|
expires_in: 3600,
|
||||||
|
token_type: "Bearer",
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const ensureRuntime = vi.fn(async () => {
|
||||||
|
throw new Error("runtime should not be loaded for OAuth doctor");
|
||||||
|
});
|
||||||
|
registerGoogleMeetCli({
|
||||||
|
program,
|
||||||
|
config: resolveGoogleMeetConfig({
|
||||||
|
oauth: {
|
||||||
|
clientId: "client-id",
|
||||||
|
clientSecret: "client-secret",
|
||||||
|
refreshToken: "rt-secret",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ensureRuntime: ensureRuntime as unknown as () => Promise<GoogleMeetRuntime>,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync(["googlemeet", "doctor", "--oauth", "--json"], { from: "user" });
|
||||||
|
const output = stdout.output();
|
||||||
|
expect(output).not.toContain("new-access-token");
|
||||||
|
expect(output).not.toContain("rt-secret");
|
||||||
|
expect(output).not.toContain("client-secret");
|
||||||
|
expect(JSON.parse(output)).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
configured: true,
|
||||||
|
tokenSource: "refresh-token",
|
||||||
|
checks: [
|
||||||
|
{ id: "oauth-config", ok: true },
|
||||||
|
{ id: "oauth-token", ok: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(ensureRuntime).not.toHaveBeenCalled();
|
||||||
|
const body = fetchMock.mock.calls[0]?.[1]?.body as URLSearchParams;
|
||||||
|
expect(body.get("grant_type")).toBe("refresh_token");
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("CLI doctor can prove Google Meet API create access", async () => {
|
||||||
|
const program = new Command();
|
||||||
|
const stdout = captureStdout();
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async (input: RequestInfo | URL) => {
|
||||||
|
const url =
|
||||||
|
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||||
|
if (url === "https://oauth2.googleapis.com/token") {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
access_token: "new-access-token",
|
||||||
|
expires_in: 3600,
|
||||||
|
token_type: "Bearer",
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (url === "https://meet.googleapis.com/v2/spaces") {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
name: "spaces/new-space",
|
||||||
|
meetingUri: "https://meet.google.com/new-abcd-xyz",
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response("not found", { status: 404 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
registerGoogleMeetCli({
|
||||||
|
program,
|
||||||
|
config: resolveGoogleMeetConfig({
|
||||||
|
oauth: {
|
||||||
|
clientId: "client-id",
|
||||||
|
refreshToken: "refresh-token",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ensureRuntime: async () => ({}) as GoogleMeetRuntime,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync(["googlemeet", "doctor", "--oauth", "--create-space", "--json"], {
|
||||||
|
from: "user",
|
||||||
|
});
|
||||||
|
expect(JSON.parse(stdout.output())).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
tokenSource: "refresh-token",
|
||||||
|
createdSpace: "spaces/new-space",
|
||||||
|
meetingUri: "https://meet.google.com/new-abcd-xyz",
|
||||||
|
checks: [
|
||||||
|
{ id: "oauth-config", ok: true },
|
||||||
|
{ id: "oauth-token", ok: true },
|
||||||
|
{ id: "meet-spaces-create", ok: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("CLI recover-tab focuses and summarizes an existing Meet tab", async () => {
|
it("CLI recover-tab focuses and summarizes an existing Meet tab", async () => {
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
const stdout = captureStdout();
|
const stdout = captureStdout();
|
||||||
|
|||||||
@@ -57,6 +57,18 @@ type SetupOptions = {
|
|||||||
json?: boolean;
|
json?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DoctorOptions = {
|
||||||
|
json?: boolean;
|
||||||
|
oauth?: boolean;
|
||||||
|
meeting?: string;
|
||||||
|
createSpace?: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type JsonOptions = {
|
type JsonOptions = {
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
};
|
};
|
||||||
@@ -173,6 +185,151 @@ function writeDoctorStatus(status: ReturnType<GoogleMeetRuntime["status"]>): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OAuthDoctorCheck = {
|
||||||
|
id: string;
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OAuthDoctorReport = {
|
||||||
|
ok: boolean;
|
||||||
|
configured: boolean;
|
||||||
|
tokenSource?: "cached-access-token" | "refresh-token";
|
||||||
|
expiresAt?: number;
|
||||||
|
scope?: string;
|
||||||
|
meetingUri?: string;
|
||||||
|
createdSpace?: string;
|
||||||
|
checks: OAuthDoctorCheck[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function sanitizeOAuthErrorMessage(error: unknown): string {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return message
|
||||||
|
.replace(/(access_token["'=:\s]+)[^"',\s&]+/gi, "$1[redacted]")
|
||||||
|
.replace(/(refresh_token["'=:\s]+)[^"',\s&]+/gi, "$1[redacted]")
|
||||||
|
.replace(/(client_secret["'=:\s]+)[^"',\s&]+/gi, "$1[redacted]");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildOAuthDoctorReport(
|
||||||
|
config: GoogleMeetConfig,
|
||||||
|
options: DoctorOptions,
|
||||||
|
): Promise<OAuthDoctorReport> {
|
||||||
|
const clientId = options.clientId?.trim() || config.oauth.clientId;
|
||||||
|
const clientSecret = options.clientSecret?.trim() || config.oauth.clientSecret;
|
||||||
|
const refreshToken = options.refreshToken?.trim() || config.oauth.refreshToken;
|
||||||
|
const accessToken = options.accessToken?.trim() || config.oauth.accessToken;
|
||||||
|
const expiresAt = parseOptionalNumber(options.expiresAt) ?? config.oauth.expiresAt;
|
||||||
|
const checks: OAuthDoctorCheck[] = [];
|
||||||
|
|
||||||
|
const hasRefreshConfig = Boolean(clientId && refreshToken);
|
||||||
|
const hasAccessConfig = Boolean(accessToken);
|
||||||
|
if (!hasRefreshConfig && !hasAccessConfig) {
|
||||||
|
checks.push({
|
||||||
|
id: "oauth-config",
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"Missing Google Meet OAuth credentials. Configure oauth.clientId and oauth.refreshToken, or pass --client-id and --refresh-token.",
|
||||||
|
});
|
||||||
|
return { ok: false, configured: false, checks };
|
||||||
|
}
|
||||||
|
|
||||||
|
checks.push({
|
||||||
|
id: "oauth-config",
|
||||||
|
ok: true,
|
||||||
|
message: hasRefreshConfig
|
||||||
|
? "Google Meet OAuth refresh credentials are configured"
|
||||||
|
: "Google Meet cached access token is configured",
|
||||||
|
});
|
||||||
|
|
||||||
|
let token: Awaited<ReturnType<typeof resolveGoogleMeetAccessToken>>;
|
||||||
|
try {
|
||||||
|
token = await resolveGoogleMeetAccessToken({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
refreshToken,
|
||||||
|
accessToken,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
checks.push({
|
||||||
|
id: "oauth-token",
|
||||||
|
ok: true,
|
||||||
|
message: token.refreshed
|
||||||
|
? "Refresh token minted an access token"
|
||||||
|
: "Cached access token is still valid",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
checks.push({
|
||||||
|
id: "oauth-token",
|
||||||
|
ok: false,
|
||||||
|
message: sanitizeOAuthErrorMessage(error),
|
||||||
|
});
|
||||||
|
return { ok: false, configured: true, checks };
|
||||||
|
}
|
||||||
|
|
||||||
|
const report: OAuthDoctorReport = {
|
||||||
|
ok: true,
|
||||||
|
configured: true,
|
||||||
|
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||||
|
expiresAt: token.expiresAt,
|
||||||
|
checks,
|
||||||
|
};
|
||||||
|
|
||||||
|
const meeting = options.meeting?.trim();
|
||||||
|
if (meeting) {
|
||||||
|
try {
|
||||||
|
const space = await fetchGoogleMeetSpace({ accessToken: token.accessToken, meeting });
|
||||||
|
checks.push({
|
||||||
|
id: "meet-spaces-get",
|
||||||
|
ok: true,
|
||||||
|
message: `Resolved ${space.name}`,
|
||||||
|
});
|
||||||
|
report.meetingUri = space.meetingUri;
|
||||||
|
} catch (error) {
|
||||||
|
checks.push({
|
||||||
|
id: "meet-spaces-get",
|
||||||
|
ok: false,
|
||||||
|
message: sanitizeOAuthErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.createSpace) {
|
||||||
|
try {
|
||||||
|
const created = await createGoogleMeetSpace({ accessToken: token.accessToken });
|
||||||
|
checks.push({
|
||||||
|
id: "meet-spaces-create",
|
||||||
|
ok: true,
|
||||||
|
message: `Created ${created.space.name}`,
|
||||||
|
});
|
||||||
|
report.createdSpace = created.space.name;
|
||||||
|
report.meetingUri = created.meetingUri;
|
||||||
|
} catch (error) {
|
||||||
|
checks.push({
|
||||||
|
id: "meet-spaces-create",
|
||||||
|
ok: false,
|
||||||
|
message: sanitizeOAuthErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.ok = checks.every((check) => check.ok);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeOAuthDoctorReport(report: OAuthDoctorReport): void {
|
||||||
|
writeStdoutLine("Google Meet OAuth: %s", report.ok ? "OK" : "needs attention");
|
||||||
|
writeStdoutLine("configured: %s", report.configured ? "yes" : "no");
|
||||||
|
if (report.tokenSource) {
|
||||||
|
writeStdoutLine("token source: %s", report.tokenSource);
|
||||||
|
}
|
||||||
|
if (report.meetingUri) {
|
||||||
|
writeStdoutLine("meeting uri: %s", report.meetingUri);
|
||||||
|
}
|
||||||
|
for (const check of report.checks) {
|
||||||
|
writeStdoutLine("[%s] %s: %s", check.ok ? "ok" : "fail", check.id, check.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function writeRecoverCurrentTabResult(
|
function writeRecoverCurrentTabResult(
|
||||||
result: Awaited<ReturnType<GoogleMeetRuntime["recoverCurrentTab"]>>,
|
result: Awaited<ReturnType<GoogleMeetRuntime["recoverCurrentTab"]>>,
|
||||||
): void {
|
): void {
|
||||||
@@ -754,8 +911,25 @@ export function registerGoogleMeetCli(params: {
|
|||||||
.command("doctor")
|
.command("doctor")
|
||||||
.description("Show human-readable Meet session/browser/realtime health")
|
.description("Show human-readable Meet session/browser/realtime health")
|
||||||
.argument("[session-id]", "Meet session ID")
|
.argument("[session-id]", "Meet session ID")
|
||||||
|
.option("--oauth", "Verify Google Meet OAuth token refresh without printing secrets", false)
|
||||||
|
.option("--meeting <value>", "Also verify spaces.get for a Meet URL, code, or spaces/{id}")
|
||||||
|
.option("--create-space", "Also verify spaces.create by creating a throwaway Meet space", false)
|
||||||
|
.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)
|
.option("--json", "Print JSON output", false)
|
||||||
.action(async (sessionId: string | undefined, options: JsonOptions) => {
|
.action(async (sessionId: string | undefined, options: DoctorOptions) => {
|
||||||
|
if (options.oauth) {
|
||||||
|
const report = await buildOAuthDoctorReport(params.config, options);
|
||||||
|
if (options.json) {
|
||||||
|
writeStdoutJson(report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeOAuthDoctorReport(report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const rt = await params.ensureRuntime();
|
const rt = await params.ensureRuntime();
|
||||||
const status = rt.status(sessionId);
|
const status = rt.status(sessionId);
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
|
|||||||
Reference in New Issue
Block a user