@gakr-gakr/google-meet 0.1.0

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/create.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { AutoBotPluginApi } from "autobot/plugin-sdk/plugin-entry";
2
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
3
+ import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
4
+ import {
5
+ createGoogleMeetSpace,
6
+ type GoogleMeetAccessType,
7
+ type GoogleMeetEntryPointAccess,
8
+ type GoogleMeetSpaceConfig,
9
+ } from "./meet.js";
10
+ import { resolveGoogleMeetAccessToken } from "./oauth.js";
11
+ import type { GoogleMeetRuntime } from "./runtime.js";
12
+ import { createMeetWithBrowserProxyOnNode } from "./transports/chrome-create.js";
13
+
14
+ function normalizeTransport(value: unknown): GoogleMeetTransport | undefined {
15
+ return value === "chrome" || value === "chrome-node" || value === "twilio" ? value : undefined;
16
+ }
17
+
18
+ function normalizeMode(value: unknown): GoogleMeetMode | undefined {
19
+ if (value === "realtime") {
20
+ return "agent";
21
+ }
22
+ return value === "agent" || value === "bidi" || value === "transcribe" ? value : undefined;
23
+ }
24
+
25
+ function normalizeGoogleMeetAccessType(value: unknown): GoogleMeetAccessType | undefined {
26
+ const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
27
+ return normalized === "OPEN" || normalized === "TRUSTED" || normalized === "RESTRICTED"
28
+ ? normalized
29
+ : undefined;
30
+ }
31
+
32
+ function normalizeGoogleMeetEntryPointAccess(
33
+ value: unknown,
34
+ ): GoogleMeetEntryPointAccess | undefined {
35
+ const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
36
+ return normalized === "ALL" || normalized === "CREATOR_APP_ONLY" ? normalized : undefined;
37
+ }
38
+
39
+ export function resolveCreateSpaceConfig(
40
+ raw: Record<string, unknown>,
41
+ ): GoogleMeetSpaceConfig | undefined {
42
+ const rawAccessType = normalizeOptionalString(raw.accessType);
43
+ const rawEntryPointAccess = normalizeOptionalString(raw.entryPointAccess);
44
+ const accessType = normalizeGoogleMeetAccessType(raw.accessType);
45
+ const entryPointAccess = normalizeGoogleMeetEntryPointAccess(raw.entryPointAccess);
46
+ if (rawAccessType !== undefined && !accessType) {
47
+ throw new Error("Invalid Google Meet accessType. Expected OPEN, TRUSTED, or RESTRICTED.");
48
+ }
49
+ if (rawEntryPointAccess !== undefined && !entryPointAccess) {
50
+ throw new Error("Invalid Google Meet entryPointAccess. Expected ALL or CREATOR_APP_ONLY.");
51
+ }
52
+ const config = {
53
+ ...(accessType ? { accessType } : {}),
54
+ ...(entryPointAccess ? { entryPointAccess } : {}),
55
+ };
56
+ return Object.keys(config).length > 0 ? config : undefined;
57
+ }
58
+
59
+ export function hasCreateSpaceConfigInput(raw: Record<string, unknown>): boolean {
60
+ return (
61
+ normalizeOptionalString(raw.accessType) !== undefined ||
62
+ normalizeOptionalString(raw.entryPointAccess) !== undefined
63
+ );
64
+ }
65
+
66
+ async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
67
+ const token = await resolveGoogleMeetAccessToken({
68
+ clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
69
+ clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
70
+ refreshToken: normalizeOptionalString(raw.refreshToken) ?? config.oauth.refreshToken,
71
+ accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
72
+ expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt,
73
+ });
74
+ const result = await createGoogleMeetSpace({
75
+ accessToken: token.accessToken,
76
+ config: resolveCreateSpaceConfig(raw),
77
+ });
78
+ return { source: "api" as const, token, ...result };
79
+ }
80
+
81
+ function hasGoogleMeetOAuth(config: GoogleMeetConfig, raw: Record<string, unknown>): boolean {
82
+ return Boolean(
83
+ normalizeOptionalString(raw.accessToken) ??
84
+ normalizeOptionalString(raw.refreshToken) ??
85
+ config.oauth.accessToken ??
86
+ config.oauth.refreshToken,
87
+ );
88
+ }
89
+
90
+ export async function createMeetFromParams(params: {
91
+ config: GoogleMeetConfig;
92
+ runtime: AutoBotPluginApi["runtime"];
93
+ raw: Record<string, unknown>;
94
+ }) {
95
+ if (hasGoogleMeetOAuth(params.config, params.raw)) {
96
+ const { token: _token, ...result } = await createSpaceFromParams(params.config, params.raw);
97
+ return {
98
+ ...result,
99
+ joined: false,
100
+ nextAction:
101
+ "URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
102
+ };
103
+ }
104
+ if (hasCreateSpaceConfigInput(params.raw)) {
105
+ throw new Error(
106
+ "Google Meet access policy options require OAuth/API room creation. Configure Google Meet OAuth or remove accessType/entryPointAccess.",
107
+ );
108
+ }
109
+ const browser = await createMeetWithBrowserProxyOnNode({
110
+ runtime: params.runtime,
111
+ config: params.config,
112
+ });
113
+ return {
114
+ source: browser.source,
115
+ meetingUri: browser.meetingUri,
116
+ joined: false,
117
+ nextAction:
118
+ "URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
119
+ space: {
120
+ name: `browser/${browser.meetingUri.split("/").pop()}`,
121
+ meetingUri: browser.meetingUri,
122
+ },
123
+ browser: {
124
+ nodeId: browser.nodeId,
125
+ targetId: browser.targetId,
126
+ browserUrl: browser.browserUrl,
127
+ browserTitle: browser.browserTitle,
128
+ notes: browser.notes,
129
+ },
130
+ };
131
+ }
132
+
133
+ export async function createAndJoinMeetFromParams(params: {
134
+ config: GoogleMeetConfig;
135
+ runtime: AutoBotPluginApi["runtime"];
136
+ raw: Record<string, unknown>;
137
+ ensureRuntime: () => Promise<GoogleMeetRuntime>;
138
+ }) {
139
+ const created = await createMeetFromParams(params);
140
+ const rt = await params.ensureRuntime();
141
+ const join = await rt.join({
142
+ url: created.meetingUri,
143
+ transport: normalizeTransport(params.raw.transport),
144
+ mode: normalizeMode(params.raw.mode),
145
+ dialInNumber: normalizeOptionalString(params.raw.dialInNumber),
146
+ pin: normalizeOptionalString(params.raw.pin),
147
+ dtmfSequence: normalizeOptionalString(params.raw.dtmfSequence),
148
+ message: normalizeOptionalString(params.raw.message),
149
+ requesterSessionKey: normalizeOptionalString(params.raw.requesterSessionKey),
150
+ });
151
+ return {
152
+ ...created,
153
+ joined: true,
154
+ nextAction: "Share meetingUri with participants; the AutoBot agent has started the join flow.",
155
+ join,
156
+ };
157
+ }
package/src/drive.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { fetchWithSsrFGuard } from "autobot/plugin-sdk/ssrf-runtime";
2
+ import { googleApiError } from "./google-api-errors.js";
3
+
4
+ const GOOGLE_DRIVE_API_BASE_URL = "https://www.googleapis.com/drive/v3";
5
+ const GOOGLE_DRIVE_API_HOST = "www.googleapis.com";
6
+ const GOOGLE_DRIVE_MEET_SCOPE = "https://www.googleapis.com/auth/drive.meet.readonly";
7
+ const TEXT_PLAIN_MIME = "text/plain";
8
+
9
+ function appendQuery(url: string, query: Record<string, string | undefined>) {
10
+ const parsed = new URL(url);
11
+ for (const [key, value] of Object.entries(query)) {
12
+ if (value !== undefined) {
13
+ parsed.searchParams.set(key, value);
14
+ }
15
+ }
16
+ return parsed.toString();
17
+ }
18
+
19
+ export function extractGoogleDriveDocumentId(value: unknown): string | undefined {
20
+ if (typeof value !== "string") {
21
+ return undefined;
22
+ }
23
+ const trimmed = value.trim();
24
+ if (!trimmed) {
25
+ return undefined;
26
+ }
27
+ if (/^https?:\/\//i.test(trimmed)) {
28
+ try {
29
+ const url = new URL(trimmed);
30
+ const documentMatch = url.pathname.match(/\/document\/d\/([^/]+)/);
31
+ return documentMatch?.[1];
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ const segments = trimmed.split("/").filter(Boolean);
37
+ return segments.at(-1);
38
+ }
39
+
40
+ export async function exportGoogleDriveDocumentText(params: {
41
+ accessToken: string;
42
+ documentId: string;
43
+ }): Promise<string> {
44
+ const { response, release } = await fetchWithSsrFGuard({
45
+ url: appendQuery(
46
+ `${GOOGLE_DRIVE_API_BASE_URL}/files/${encodeURIComponent(params.documentId)}/export`,
47
+ { mimeType: TEXT_PLAIN_MIME },
48
+ ),
49
+ init: {
50
+ headers: {
51
+ Authorization: `Bearer ${params.accessToken}`,
52
+ Accept: TEXT_PLAIN_MIME,
53
+ },
54
+ },
55
+ policy: { allowedHostnames: [GOOGLE_DRIVE_API_HOST] },
56
+ auditContext: "google-meet.drive.files.export",
57
+ });
58
+ try {
59
+ if (!response.ok) {
60
+ const detail = await response.text();
61
+ throw await googleApiError({
62
+ response,
63
+ detail,
64
+ prefix: "Google Drive files.export",
65
+ scopes: [GOOGLE_DRIVE_MEET_SCOPE],
66
+ });
67
+ }
68
+ return await response.text();
69
+ } finally {
70
+ await release();
71
+ }
72
+ }
@@ -0,0 +1,20 @@
1
+ const REAUTH_HINT = "Re-run `autobot googlemeet auth login` and store the refreshed oauth block.";
2
+
3
+ function scopeText(scopes: readonly string[]): string {
4
+ return scopes.map((scope) => `\`${scope}\``).join(", ");
5
+ }
6
+
7
+ export async function googleApiError(params: {
8
+ response: Response;
9
+ detail: string;
10
+ prefix: string;
11
+ scopes?: readonly string[];
12
+ }): Promise<Error> {
13
+ const scopeHint =
14
+ params.scopes && params.scopes.length > 0
15
+ ? ` Required OAuth scope: ${scopeText(params.scopes)}. ${REAUTH_HINT}`
16
+ : "";
17
+ return new Error(
18
+ `${params.prefix} failed (${params.response.status}): ${params.detail}${scopeHint}`,
19
+ );
20
+ }