@kodelyth/google-meet 2026.5.39 → 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.
@@ -0,0 +1,965 @@
1
+ import { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
2
+ import { sleep } from "klaw/plugin-sdk/runtime-env";
3
+ //#region extensions/google-meet/src/google-api-errors.ts
4
+ const REAUTH_HINT = "Re-run `klaw googlemeet auth login` and store the refreshed oauth block.";
5
+ function scopeText(scopes) {
6
+ return scopes.map((scope) => `\`${scope}\``).join(", ");
7
+ }
8
+ async function googleApiError(params) {
9
+ const scopeHint = params.scopes && params.scopes.length > 0 ? ` Required OAuth scope: ${scopeText(params.scopes)}. ${REAUTH_HINT}` : "";
10
+ return /* @__PURE__ */ new Error(`${params.prefix} failed (${params.response.status}): ${params.detail}${scopeHint}`);
11
+ }
12
+ //#endregion
13
+ //#region extensions/google-meet/src/drive.ts
14
+ const GOOGLE_DRIVE_API_BASE_URL = "https://www.googleapis.com/drive/v3";
15
+ const GOOGLE_DRIVE_API_HOST = "www.googleapis.com";
16
+ const GOOGLE_DRIVE_MEET_SCOPE = "https://www.googleapis.com/auth/drive.meet.readonly";
17
+ const TEXT_PLAIN_MIME = "text/plain";
18
+ function appendQuery$1(url, query) {
19
+ const parsed = new URL(url);
20
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) parsed.searchParams.set(key, value);
21
+ return parsed.toString();
22
+ }
23
+ function extractGoogleDriveDocumentId(value) {
24
+ if (typeof value !== "string") return;
25
+ const trimmed = value.trim();
26
+ if (!trimmed) return;
27
+ if (/^https?:\/\//i.test(trimmed)) try {
28
+ return new URL(trimmed).pathname.match(/\/document\/d\/([^/]+)/)?.[1];
29
+ } catch {
30
+ return;
31
+ }
32
+ return trimmed.split("/").filter(Boolean).at(-1);
33
+ }
34
+ async function exportGoogleDriveDocumentText(params) {
35
+ const { response, release } = await fetchWithSsrFGuard({
36
+ url: appendQuery$1(`${GOOGLE_DRIVE_API_BASE_URL}/files/${encodeURIComponent(params.documentId)}/export`, { mimeType: TEXT_PLAIN_MIME }),
37
+ init: { headers: {
38
+ Authorization: `Bearer ${params.accessToken}`,
39
+ Accept: TEXT_PLAIN_MIME
40
+ } },
41
+ policy: { allowedHostnames: [GOOGLE_DRIVE_API_HOST] },
42
+ auditContext: "google-meet.drive.files.export"
43
+ });
44
+ try {
45
+ if (!response.ok) throw await googleApiError({
46
+ response,
47
+ detail: await response.text(),
48
+ prefix: "Google Drive files.export",
49
+ scopes: [GOOGLE_DRIVE_MEET_SCOPE]
50
+ });
51
+ return await response.text();
52
+ } finally {
53
+ await release();
54
+ }
55
+ }
56
+ //#endregion
57
+ //#region extensions/google-meet/src/meet.ts
58
+ const GOOGLE_MEET_API_BASE_URL = `https://meet.googleapis.com/v2`;
59
+ const GOOGLE_MEET_URL_HOST = "meet.google.com";
60
+ const GOOGLE_MEET_API_HOST = "meet.googleapis.com";
61
+ const GOOGLE_MEET_MEDIA_SCOPE = "https://www.googleapis.com/auth/meetings.conference.media.readonly";
62
+ const GOOGLE_MEET_SPACE_SCOPE = "https://www.googleapis.com/auth/meetings.space.readonly";
63
+ const GOOGLE_MEET_SPACE_CREATED_SCOPE = "https://www.googleapis.com/auth/meetings.space.created";
64
+ const GOOGLE_MEET_SPACE_SETTINGS_SCOPE = "https://www.googleapis.com/auth/meetings.space.settings";
65
+ function normalizeGoogleMeetSpaceName(input) {
66
+ const trimmed = input.trim();
67
+ if (!trimmed) throw new Error("Meeting input is required");
68
+ if (trimmed.startsWith("spaces/")) {
69
+ const suffix = trimmed.slice(7).trim();
70
+ if (!suffix) throw new Error("spaces/ input must include a meeting code or space id");
71
+ return `spaces/${suffix}`;
72
+ }
73
+ if (/^https?:\/\//i.test(trimmed)) {
74
+ const url = new URL(trimmed);
75
+ if (url.hostname !== GOOGLE_MEET_URL_HOST) throw new Error(`Expected a ${GOOGLE_MEET_URL_HOST} URL, received ${url.hostname}`);
76
+ const firstSegment = url.pathname.split("/").map((segment) => segment.trim()).find(Boolean);
77
+ if (!firstSegment) throw new Error("Google Meet URL did not include a meeting code");
78
+ return `spaces/${firstSegment}`;
79
+ }
80
+ return `spaces/${trimmed}`;
81
+ }
82
+ function encodeSpaceNameForPath(name) {
83
+ return name.split("/").map(encodeURIComponent).join("/");
84
+ }
85
+ function encodeResourceNameForPath(name) {
86
+ const trimmed = name.trim();
87
+ if (!trimmed) throw new Error("Google Meet resource name is required");
88
+ return trimmed.split("/").map(encodeURIComponent).join("/");
89
+ }
90
+ function normalizeConferenceRecordName(input) {
91
+ const trimmed = input.trim();
92
+ if (!trimmed) throw new Error("Conference record is required");
93
+ return trimmed.startsWith("conferenceRecords/") ? trimmed : `conferenceRecords/${trimmed}`;
94
+ }
95
+ function appendQuery(url, query) {
96
+ if (!query) return url;
97
+ const parsed = new URL(url);
98
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) parsed.searchParams.set(key, String(value));
99
+ return parsed.toString();
100
+ }
101
+ function assertResourceArray(value, key, context) {
102
+ if (value === void 0) return [];
103
+ if (!Array.isArray(value)) throw new Error(`Google Meet ${context} response had non-array ${key}`);
104
+ const resources = value;
105
+ for (const resource of resources) if (!resource.name?.trim()) throw new Error(`Google Meet ${context} response included a resource without name`);
106
+ return resources;
107
+ }
108
+ function getErrorMessage(error) {
109
+ return error instanceof Error ? error.message : String(error);
110
+ }
111
+ async function fetchGoogleMeetJson(params) {
112
+ const { response, release } = await fetchWithSsrFGuard({
113
+ url: appendQuery(`${GOOGLE_MEET_API_BASE_URL}/${params.path}`, params.query),
114
+ init: { headers: {
115
+ Authorization: `Bearer ${params.accessToken}`,
116
+ Accept: "application/json"
117
+ } },
118
+ policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
119
+ auditContext: params.auditContext
120
+ });
121
+ try {
122
+ if (!response.ok) throw await googleApiError({
123
+ response,
124
+ detail: await response.text(),
125
+ prefix: params.errorPrefix,
126
+ scopes: [GOOGLE_MEET_MEDIA_SCOPE]
127
+ });
128
+ return await response.json();
129
+ } finally {
130
+ await release();
131
+ }
132
+ }
133
+ async function listGoogleMeetCollection(params) {
134
+ const items = [];
135
+ let pageToken;
136
+ do {
137
+ const payload = await fetchGoogleMeetJson({
138
+ accessToken: params.accessToken,
139
+ path: params.path,
140
+ query: {
141
+ ...params.query,
142
+ pageToken
143
+ },
144
+ auditContext: params.auditContext,
145
+ errorPrefix: params.errorPrefix
146
+ });
147
+ const pageItems = assertResourceArray(payload[params.collectionKey], params.collectionKey, params.errorPrefix);
148
+ const remaining = typeof params.maxItems === "number" ? Math.max(params.maxItems - items.length, 0) : void 0;
149
+ items.push(...remaining === void 0 ? pageItems : pageItems.slice(0, remaining));
150
+ if (typeof params.maxItems === "number" && items.length >= params.maxItems) break;
151
+ pageToken = typeof payload.nextPageToken === "string" ? payload.nextPageToken : void 0;
152
+ } while (pageToken);
153
+ return items;
154
+ }
155
+ async function fetchGoogleMeetSpace(params) {
156
+ const { response, release } = await fetchWithSsrFGuard({
157
+ url: `${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(normalizeGoogleMeetSpaceName(params.meeting))}`,
158
+ init: { headers: {
159
+ Authorization: `Bearer ${params.accessToken}`,
160
+ Accept: "application/json"
161
+ } },
162
+ policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
163
+ auditContext: "google-meet.spaces.get"
164
+ });
165
+ try {
166
+ if (!response.ok) throw await googleApiError({
167
+ response,
168
+ detail: await response.text(),
169
+ prefix: "Google Meet spaces.get",
170
+ scopes: [GOOGLE_MEET_SPACE_SCOPE]
171
+ });
172
+ const payload = await response.json();
173
+ if (!payload.name?.trim()) throw new Error("Google Meet spaces.get response was missing name");
174
+ return payload;
175
+ } finally {
176
+ await release();
177
+ }
178
+ }
179
+ async function createGoogleMeetSpace(params) {
180
+ const body = params.config && Object.keys(params.config).length > 0 ? JSON.stringify({ config: params.config }) : "{}";
181
+ const { response, release } = await fetchWithSsrFGuard({
182
+ url: `${GOOGLE_MEET_API_BASE_URL}/spaces`,
183
+ init: {
184
+ method: "POST",
185
+ headers: {
186
+ Authorization: `Bearer ${params.accessToken}`,
187
+ Accept: "application/json",
188
+ "Content-Type": "application/json"
189
+ },
190
+ body
191
+ },
192
+ policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
193
+ auditContext: "google-meet.spaces.create"
194
+ });
195
+ try {
196
+ if (!response.ok) throw await googleApiError({
197
+ response,
198
+ detail: await response.text(),
199
+ prefix: "Google Meet spaces.create",
200
+ scopes: params.config && Object.keys(params.config).length > 0 ? [GOOGLE_MEET_SPACE_CREATED_SCOPE, GOOGLE_MEET_SPACE_SETTINGS_SCOPE] : [GOOGLE_MEET_SPACE_CREATED_SCOPE]
201
+ });
202
+ const payload = await response.json();
203
+ if (!payload.name?.trim()) throw new Error("Google Meet spaces.create response was missing name");
204
+ const meetingUri = payload.meetingUri?.trim();
205
+ if (!meetingUri) throw new Error("Google Meet spaces.create response was missing meetingUri");
206
+ return {
207
+ space: payload,
208
+ meetingUri
209
+ };
210
+ } finally {
211
+ await release();
212
+ }
213
+ }
214
+ async function endGoogleMeetActiveConference(params) {
215
+ const space = (await fetchGoogleMeetSpace({
216
+ accessToken: params.accessToken,
217
+ meeting: params.meeting
218
+ })).name;
219
+ const { response, release } = await fetchWithSsrFGuard({
220
+ url: `${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(space)}:endActiveConference`,
221
+ init: {
222
+ method: "POST",
223
+ headers: {
224
+ Authorization: `Bearer ${params.accessToken}`,
225
+ Accept: "application/json",
226
+ "Content-Type": "application/json"
227
+ },
228
+ body: "{}"
229
+ },
230
+ policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
231
+ auditContext: "google-meet.spaces.endActiveConference"
232
+ });
233
+ try {
234
+ if (!response.ok) throw await googleApiError({
235
+ response,
236
+ detail: await response.text(),
237
+ prefix: "Google Meet spaces.endActiveConference",
238
+ scopes: [GOOGLE_MEET_SPACE_CREATED_SCOPE]
239
+ });
240
+ return {
241
+ space,
242
+ ended: true
243
+ };
244
+ } finally {
245
+ await release();
246
+ }
247
+ }
248
+ async function fetchGoogleMeetConferenceRecord(params) {
249
+ const name = normalizeConferenceRecordName(params.conferenceRecord);
250
+ const payload = await fetchGoogleMeetJson({
251
+ accessToken: params.accessToken,
252
+ path: encodeResourceNameForPath(name),
253
+ auditContext: "google-meet.conferenceRecords.get",
254
+ errorPrefix: "Google Meet conferenceRecords.get"
255
+ });
256
+ if (!payload.name?.trim()) throw new Error("Google Meet conferenceRecords.get response was missing name");
257
+ return payload;
258
+ }
259
+ async function listGoogleMeetConferenceRecords(params) {
260
+ const filter = params.meeting ? `space.name = "${normalizeGoogleMeetSpaceName(params.meeting)}"` : void 0;
261
+ return listGoogleMeetCollection({
262
+ accessToken: params.accessToken,
263
+ path: "conferenceRecords",
264
+ collectionKey: "conferenceRecords",
265
+ query: {
266
+ pageSize: params.pageSize,
267
+ filter
268
+ },
269
+ maxItems: params.maxItems,
270
+ auditContext: "google-meet.conferenceRecords.list",
271
+ errorPrefix: "Google Meet conferenceRecords.list"
272
+ });
273
+ }
274
+ async function fetchLatestGoogleMeetConferenceRecord(params) {
275
+ const space = await fetchGoogleMeetSpace({
276
+ accessToken: params.accessToken,
277
+ meeting: params.meeting
278
+ });
279
+ const [conferenceRecord] = await listGoogleMeetConferenceRecords({
280
+ accessToken: params.accessToken,
281
+ meeting: space.name,
282
+ pageSize: 1,
283
+ maxItems: 1
284
+ });
285
+ return {
286
+ input: params.meeting,
287
+ space,
288
+ ...conferenceRecord ? { conferenceRecord } : {}
289
+ };
290
+ }
291
+ async function listGoogleMeetParticipants(params) {
292
+ const parent = normalizeConferenceRecordName(params.conferenceRecord);
293
+ return listGoogleMeetCollection({
294
+ accessToken: params.accessToken,
295
+ path: `${encodeResourceNameForPath(parent)}/participants`,
296
+ collectionKey: "participants",
297
+ query: { pageSize: params.pageSize },
298
+ auditContext: "google-meet.conferenceRecords.participants.list",
299
+ errorPrefix: "Google Meet conferenceRecords.participants.list"
300
+ });
301
+ }
302
+ async function listGoogleMeetParticipantSessions(params) {
303
+ return listGoogleMeetCollection({
304
+ accessToken: params.accessToken,
305
+ path: `${encodeResourceNameForPath(params.participant)}/participantSessions`,
306
+ collectionKey: "participantSessions",
307
+ query: { pageSize: params.pageSize },
308
+ auditContext: "google-meet.conferenceRecords.participants.participantSessions.list",
309
+ errorPrefix: "Google Meet conferenceRecords.participants.participantSessions.list"
310
+ });
311
+ }
312
+ async function listGoogleMeetRecordings(params) {
313
+ const parent = normalizeConferenceRecordName(params.conferenceRecord);
314
+ return listGoogleMeetCollection({
315
+ accessToken: params.accessToken,
316
+ path: `${encodeResourceNameForPath(parent)}/recordings`,
317
+ collectionKey: "recordings",
318
+ query: { pageSize: params.pageSize },
319
+ auditContext: "google-meet.conferenceRecords.recordings.list",
320
+ errorPrefix: "Google Meet conferenceRecords.recordings.list"
321
+ });
322
+ }
323
+ async function listGoogleMeetTranscripts(params) {
324
+ const parent = normalizeConferenceRecordName(params.conferenceRecord);
325
+ return listGoogleMeetCollection({
326
+ accessToken: params.accessToken,
327
+ path: `${encodeResourceNameForPath(parent)}/transcripts`,
328
+ collectionKey: "transcripts",
329
+ query: { pageSize: params.pageSize },
330
+ auditContext: "google-meet.conferenceRecords.transcripts.list",
331
+ errorPrefix: "Google Meet conferenceRecords.transcripts.list"
332
+ });
333
+ }
334
+ async function listGoogleMeetTranscriptEntries(params) {
335
+ return listGoogleMeetCollection({
336
+ accessToken: params.accessToken,
337
+ path: `${encodeResourceNameForPath(params.transcript)}/entries`,
338
+ collectionKey: "transcriptEntries",
339
+ query: { pageSize: params.pageSize },
340
+ auditContext: "google-meet.conferenceRecords.transcripts.entries.list",
341
+ errorPrefix: "Google Meet conferenceRecords.transcripts.entries.list"
342
+ });
343
+ }
344
+ async function listGoogleMeetSmartNotes(params) {
345
+ const parent = normalizeConferenceRecordName(params.conferenceRecord);
346
+ return listGoogleMeetCollection({
347
+ accessToken: params.accessToken,
348
+ path: `${encodeResourceNameForPath(parent)}/smartNotes`,
349
+ collectionKey: "smartNotes",
350
+ query: { pageSize: params.pageSize },
351
+ auditContext: "google-meet.conferenceRecords.smartNotes.list",
352
+ errorPrefix: "Google Meet conferenceRecords.smartNotes.list"
353
+ });
354
+ }
355
+ function getParticipantDisplayName(participant) {
356
+ return participant.signedinUser?.displayName ?? participant.anonymousUser?.displayName ?? participant.phoneUser?.displayName;
357
+ }
358
+ function getParticipantUser(participant) {
359
+ return participant.signedinUser?.user;
360
+ }
361
+ function getDocsDestinationDocumentId(destination) {
362
+ return extractGoogleDriveDocumentId(destination?.document) ?? extractGoogleDriveDocumentId(destination?.documentId) ?? extractGoogleDriveDocumentId(destination?.file);
363
+ }
364
+ async function attachDocumentText(params) {
365
+ const documentId = getDocsDestinationDocumentId(params.resource.docsDestination);
366
+ if (!documentId) return params.resource;
367
+ try {
368
+ return {
369
+ ...params.resource,
370
+ documentText: await exportGoogleDriveDocumentText({
371
+ accessToken: params.accessToken,
372
+ documentId
373
+ })
374
+ };
375
+ } catch (error) {
376
+ return {
377
+ ...params.resource,
378
+ documentTextError: getErrorMessage(error)
379
+ };
380
+ }
381
+ }
382
+ function parseGoogleMeetTimestamp(value) {
383
+ if (!value?.trim()) return;
384
+ const parsed = Date.parse(value);
385
+ return Number.isFinite(parsed) ? parsed : void 0;
386
+ }
387
+ function isoFromMs(value) {
388
+ return typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : void 0;
389
+ }
390
+ function minTimestamp(values) {
391
+ const parsed = values.map(parseGoogleMeetTimestamp).filter((value) => typeof value === "number");
392
+ return parsed.length > 0 ? isoFromMs(Math.min(...parsed)) : void 0;
393
+ }
394
+ function maxTimestamp(values) {
395
+ const parsed = values.map(parseGoogleMeetTimestamp).filter((value) => typeof value === "number");
396
+ return parsed.length > 0 ? isoFromMs(Math.max(...parsed)) : void 0;
397
+ }
398
+ function sumSessionDurationMs(sessions, fallbackStart, fallbackEnd) {
399
+ const sessionTotal = sessions.reduce((total, session) => {
400
+ const startMs = parseGoogleMeetTimestamp(session.startTime);
401
+ const endMs = parseGoogleMeetTimestamp(session.endTime);
402
+ return startMs !== void 0 && endMs !== void 0 && endMs > startMs ? total + (endMs - startMs) : total;
403
+ }, 0);
404
+ if (sessionTotal > 0) return sessionTotal;
405
+ const startMs = parseGoogleMeetTimestamp(fallbackStart);
406
+ const endMs = parseGoogleMeetTimestamp(fallbackEnd);
407
+ return startMs !== void 0 && endMs !== void 0 && endMs > startMs ? endMs - startMs : void 0;
408
+ }
409
+ function attendanceMergeKey(row) {
410
+ return (row.user ?? row.displayName ?? row.participant).trim().toLocaleLowerCase();
411
+ }
412
+ function sortSessions(sessions) {
413
+ return sessions.toSorted((left, right) => (parseGoogleMeetTimestamp(left.startTime) ?? 0) - (parseGoogleMeetTimestamp(right.startTime) ?? 0));
414
+ }
415
+ function decorateAttendanceRow(row, conferenceRecord, params) {
416
+ const sessions = sortSessions(row.sessions);
417
+ const firstJoinTime = minTimestamp([row.earliestStartTime, ...sessions.map((session) => session.startTime)]);
418
+ const lastLeaveTime = maxTimestamp([row.latestEndTime, ...sessions.map((session) => session.endTime)]);
419
+ const durationMs = sumSessionDurationMs(sessions, firstJoinTime, lastLeaveTime);
420
+ const conferenceStartMs = parseGoogleMeetTimestamp(conferenceRecord.startTime);
421
+ const conferenceEndMs = parseGoogleMeetTimestamp(conferenceRecord.endTime);
422
+ const firstJoinMs = parseGoogleMeetTimestamp(firstJoinTime);
423
+ const lastLeaveMs = parseGoogleMeetTimestamp(lastLeaveTime);
424
+ const lateGraceMs = (params.lateAfterMinutes ?? 5) * 6e4;
425
+ const earlyGraceMs = (params.earlyBeforeMinutes ?? 5) * 6e4;
426
+ const lateByMs = conferenceStartMs !== void 0 && firstJoinMs !== void 0 ? Math.max(firstJoinMs - conferenceStartMs, 0) : void 0;
427
+ const earlyLeaveByMs = conferenceEndMs !== void 0 && lastLeaveMs !== void 0 ? Math.max(conferenceEndMs - lastLeaveMs, 0) : void 0;
428
+ const decorated = {
429
+ ...row,
430
+ sessions,
431
+ participants: row.participants ?? [row.participant]
432
+ };
433
+ decorated.earliestStartTime = firstJoinTime ?? row.earliestStartTime;
434
+ decorated.latestEndTime = lastLeaveTime ?? row.latestEndTime;
435
+ if (firstJoinTime) decorated.firstJoinTime = firstJoinTime;
436
+ if (lastLeaveTime) decorated.lastLeaveTime = lastLeaveTime;
437
+ if (durationMs !== void 0) decorated.durationMs = durationMs;
438
+ if (lateByMs !== void 0) {
439
+ decorated.late = lateByMs > lateGraceMs;
440
+ if (decorated.late) decorated.lateByMs = lateByMs;
441
+ }
442
+ if (earlyLeaveByMs !== void 0) {
443
+ decorated.earlyLeave = earlyLeaveByMs > earlyGraceMs;
444
+ if (decorated.earlyLeave) decorated.earlyLeaveByMs = earlyLeaveByMs;
445
+ }
446
+ return decorated;
447
+ }
448
+ function mergeAttendanceRows(rows, conferenceRecord, params) {
449
+ if (params.mergeDuplicateParticipants === false) return rows.map((row) => decorateAttendanceRow(row, conferenceRecord, params));
450
+ const grouped = /* @__PURE__ */ new Map();
451
+ for (const row of rows) {
452
+ const key = attendanceMergeKey(row);
453
+ const existing = grouped.get(key);
454
+ if (!existing) {
455
+ grouped.set(key, {
456
+ ...row,
457
+ participants: [row.participant]
458
+ });
459
+ continue;
460
+ }
461
+ existing.participants = [...new Set([...existing.participants ?? [existing.participant], row.participant])];
462
+ existing.sessions.push(...row.sessions);
463
+ existing.displayName ??= row.displayName;
464
+ existing.user ??= row.user;
465
+ existing.earliestStartTime = minTimestamp([existing.earliestStartTime, row.earliestStartTime]);
466
+ existing.latestEndTime = maxTimestamp([existing.latestEndTime, row.latestEndTime]);
467
+ }
468
+ return [...grouped.values()].map((row) => decorateAttendanceRow(row, conferenceRecord, params));
469
+ }
470
+ async function resolveConferenceRecordQuery(params) {
471
+ if (params.conferenceRecord?.trim()) {
472
+ const conferenceRecord = await fetchGoogleMeetConferenceRecord({
473
+ accessToken: params.accessToken,
474
+ conferenceRecord: params.conferenceRecord
475
+ });
476
+ return {
477
+ input: params.conferenceRecord.trim(),
478
+ conferenceRecords: [conferenceRecord]
479
+ };
480
+ }
481
+ if (!params.meeting?.trim()) throw new Error("Meeting input or conference record is required");
482
+ const space = await fetchGoogleMeetSpace({
483
+ accessToken: params.accessToken,
484
+ meeting: params.meeting
485
+ });
486
+ const conferenceRecords = await listGoogleMeetConferenceRecords({
487
+ accessToken: params.accessToken,
488
+ meeting: space.name,
489
+ pageSize: params.allConferenceRecords ? params.pageSize : 1,
490
+ maxItems: params.allConferenceRecords ? void 0 : 1
491
+ });
492
+ return {
493
+ input: params.meeting,
494
+ space,
495
+ conferenceRecords
496
+ };
497
+ }
498
+ async function fetchGoogleMeetArtifacts(params) {
499
+ const resolved = await resolveConferenceRecordQuery(params);
500
+ const artifacts = await Promise.all(resolved.conferenceRecords.map(async (conferenceRecord) => {
501
+ const [participants, recordings, transcripts, smartNotesResult] = await Promise.all([
502
+ listGoogleMeetParticipants({
503
+ accessToken: params.accessToken,
504
+ conferenceRecord: conferenceRecord.name,
505
+ pageSize: params.pageSize
506
+ }),
507
+ listGoogleMeetRecordings({
508
+ accessToken: params.accessToken,
509
+ conferenceRecord: conferenceRecord.name,
510
+ pageSize: params.pageSize
511
+ }),
512
+ listGoogleMeetTranscripts({
513
+ accessToken: params.accessToken,
514
+ conferenceRecord: conferenceRecord.name,
515
+ pageSize: params.pageSize
516
+ }),
517
+ listGoogleMeetSmartNotes({
518
+ accessToken: params.accessToken,
519
+ conferenceRecord: conferenceRecord.name,
520
+ pageSize: params.pageSize
521
+ }).then((smartNotes) => ({ smartNotes })).catch((error) => ({
522
+ smartNotes: [],
523
+ smartNotesError: getErrorMessage(error)
524
+ }))
525
+ ]);
526
+ const transcriptEntries = params.includeTranscriptEntries === false ? [] : await Promise.all(transcripts.map(async (transcript) => {
527
+ try {
528
+ return {
529
+ transcript: transcript.name,
530
+ entries: await listGoogleMeetTranscriptEntries({
531
+ accessToken: params.accessToken,
532
+ transcript: transcript.name,
533
+ pageSize: params.pageSize
534
+ })
535
+ };
536
+ } catch (error) {
537
+ return {
538
+ transcript: transcript.name,
539
+ entries: [],
540
+ entriesError: getErrorMessage(error)
541
+ };
542
+ }
543
+ }));
544
+ return {
545
+ conferenceRecord,
546
+ participants,
547
+ recordings,
548
+ transcripts: params.includeDocumentBodies === true ? await Promise.all(transcripts.map((transcript) => attachDocumentText({
549
+ accessToken: params.accessToken,
550
+ resource: transcript
551
+ }))) : transcripts,
552
+ transcriptEntries,
553
+ smartNotes: params.includeDocumentBodies === true ? await Promise.all(smartNotesResult.smartNotes.map((smartNote) => attachDocumentText({
554
+ accessToken: params.accessToken,
555
+ resource: smartNote
556
+ }))) : smartNotesResult.smartNotes,
557
+ ...smartNotesResult.smartNotesError ? { smartNotesError: smartNotesResult.smartNotesError } : {}
558
+ };
559
+ }));
560
+ return {
561
+ input: resolved.input,
562
+ space: resolved.space,
563
+ conferenceRecords: resolved.conferenceRecords,
564
+ artifacts
565
+ };
566
+ }
567
+ async function fetchGoogleMeetAttendance(params) {
568
+ const resolved = await resolveConferenceRecordQuery(params);
569
+ const nestedRows = await Promise.all(resolved.conferenceRecords.map(async (conferenceRecord) => {
570
+ const participants = await listGoogleMeetParticipants({
571
+ accessToken: params.accessToken,
572
+ conferenceRecord: conferenceRecord.name,
573
+ pageSize: params.pageSize
574
+ });
575
+ return mergeAttendanceRows(await Promise.all(participants.map(async (participant) => ({
576
+ conferenceRecord: conferenceRecord.name,
577
+ participant: participant.name,
578
+ displayName: getParticipantDisplayName(participant),
579
+ user: getParticipantUser(participant),
580
+ earliestStartTime: participant.earliestStartTime,
581
+ latestEndTime: participant.latestEndTime,
582
+ sessions: await listGoogleMeetParticipantSessions({
583
+ accessToken: params.accessToken,
584
+ participant: participant.name,
585
+ pageSize: params.pageSize
586
+ })
587
+ }))), conferenceRecord, params);
588
+ }));
589
+ return {
590
+ input: resolved.input,
591
+ space: resolved.space,
592
+ conferenceRecords: resolved.conferenceRecords,
593
+ attendance: nestedRows.flat()
594
+ };
595
+ }
596
+ function buildGoogleMeetPreflightReport(params) {
597
+ const blockers = [];
598
+ if (!params.previewAcknowledged) blockers.push("Set preview.enrollmentAcknowledged=true after confirming your Cloud project, OAuth principal, and meeting participants are enrolled in the Google Workspace Developer Preview Program.");
599
+ return {
600
+ input: params.input,
601
+ resolvedSpaceName: params.space.name,
602
+ meetingCode: params.space.meetingCode,
603
+ meetingUri: params.space.meetingUri,
604
+ hasActiveConference: Boolean(params.space.activeConference),
605
+ previewAcknowledged: params.previewAcknowledged,
606
+ tokenSource: params.tokenSource,
607
+ blockers
608
+ };
609
+ }
610
+ //#endregion
611
+ //#region extensions/google-meet/src/transports/chrome-browser-proxy.ts
612
+ function normalizeMeetUrlForReuse(url) {
613
+ if (!url) return;
614
+ try {
615
+ const parsed = new URL(url);
616
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "meet.google.com") return;
617
+ const match = parsed.pathname.match(/^\/(new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:\/)?$/i);
618
+ if (!match?.[1]) return;
619
+ return `https://meet.google.com/${match[1].toLowerCase()}`;
620
+ } catch {
621
+ return;
622
+ }
623
+ }
624
+ function isSameMeetUrlForReuse(a, b) {
625
+ const normalizedA = normalizeMeetUrlForReuse(a);
626
+ const normalizedB = normalizeMeetUrlForReuse(b);
627
+ return Boolean(normalizedA && normalizedB && normalizedA === normalizedB);
628
+ }
629
+ function isGoogleMeetNode(node) {
630
+ const commands = Array.isArray(node.commands) ? node.commands : [];
631
+ const caps = Array.isArray(node.caps) ? node.caps : [];
632
+ return node.connected === true && commands.includes("googlemeet.chrome") && (commands.includes("browser.proxy") || caps.includes("browser"));
633
+ }
634
+ function matchesRequestedNode(node, requested) {
635
+ return [
636
+ node.nodeId,
637
+ node.displayName,
638
+ node.remoteIp
639
+ ].some((value) => value === requested);
640
+ }
641
+ function formatNodeLabel(node) {
642
+ const parts = [
643
+ node.displayName,
644
+ node.nodeId,
645
+ node.remoteIp
646
+ ].filter(Boolean);
647
+ return parts.length > 0 ? parts.join(" / ") : "unknown node";
648
+ }
649
+ function describeNodeUsabilityIssues(node) {
650
+ const commands = Array.isArray(node.commands) ? node.commands : [];
651
+ const caps = Array.isArray(node.caps) ? node.caps : [];
652
+ const issues = [];
653
+ if (node.connected !== true) issues.push("offline");
654
+ if (!commands.includes("googlemeet.chrome")) issues.push("missing googlemeet.chrome");
655
+ if (!commands.includes("browser.proxy") && !caps.includes("browser")) issues.push("missing browser.proxy/browser capability");
656
+ return issues;
657
+ }
658
+ async function listGoogleMeetNodes(runtime, params) {
659
+ try {
660
+ return params ? await runtime.nodes.list(params) : await runtime.nodes.list();
661
+ } catch (error) {
662
+ throw new Error("Google Meet node inventory unavailable", { cause: error });
663
+ }
664
+ }
665
+ async function resolveChromeNodeInfo(params) {
666
+ const requested = params.requestedNode?.trim();
667
+ if (requested) {
668
+ const matches = (await listGoogleMeetNodes(params.runtime)).nodes.filter((node) => matchesRequestedNode(node, requested));
669
+ if (matches.length === 1) {
670
+ const [node] = matches;
671
+ if (isGoogleMeetNode(node)) return node;
672
+ throw new Error(`Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`klaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`);
673
+ }
674
+ if (matches.length > 1) throw new Error(`Configured Google Meet node ${requested} is ambiguous (${matches.length} matches). Pin chromeNode.node to a unique node id, display name, or remote IP.`);
675
+ throw new Error(`Configured Google Meet node ${requested} was not found. Run \`klaw nodes status\` and start or approve the Chrome node.`);
676
+ }
677
+ const nodes = (await listGoogleMeetNodes(params.runtime, { connected: true })).nodes.filter(isGoogleMeetNode);
678
+ if (nodes.length === 0) throw new Error("No connected Google Meet-capable node with browser proxy. Run `klaw node run` on the Chrome host with browser proxy enabled, approve pairing, and allow googlemeet.chrome plus browser.proxy.");
679
+ if (nodes.length === 1) return nodes[0];
680
+ throw new Error("Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.");
681
+ }
682
+ async function resolveChromeNode(params) {
683
+ const node = await resolveChromeNodeInfo(params);
684
+ if (!node.nodeId) throw new Error("Google Meet node did not include a node id.");
685
+ return node.nodeId;
686
+ }
687
+ function unwrapNodeInvokePayload(raw) {
688
+ const record = raw && typeof raw === "object" ? raw : {};
689
+ if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) try {
690
+ return JSON.parse(record.payloadJSON);
691
+ } catch (error) {
692
+ throw new Error("Google Meet browser proxy returned malformed payloadJSON.", { cause: error });
693
+ }
694
+ if ("payload" in record) return record.payload;
695
+ return raw;
696
+ }
697
+ function parseBrowserProxyResult(raw) {
698
+ const payload = unwrapNodeInvokePayload(raw);
699
+ const proxy = payload && typeof payload === "object" ? payload : void 0;
700
+ if (!proxy || !("result" in proxy)) throw new Error("Google Meet browser proxy returned an invalid result.");
701
+ return proxy.result;
702
+ }
703
+ async function callBrowserProxyOnNode(params) {
704
+ return parseBrowserProxyResult(await params.runtime.nodes.invoke({
705
+ nodeId: params.nodeId,
706
+ command: "browser.proxy",
707
+ params: {
708
+ method: params.method,
709
+ path: params.path,
710
+ body: params.body,
711
+ timeoutMs: params.timeoutMs
712
+ },
713
+ timeoutMs: params.timeoutMs + 5e3
714
+ }));
715
+ }
716
+ function asBrowserTabs(result) {
717
+ const record = result && typeof result === "object" ? result : {};
718
+ return Array.isArray(record.tabs) ? record.tabs : [];
719
+ }
720
+ function readBrowserTab(result) {
721
+ return result && typeof result === "object" ? result : void 0;
722
+ }
723
+ //#endregion
724
+ //#region extensions/google-meet/src/transports/chrome-create.ts
725
+ const GOOGLE_MEET_NEW_URL = "https://meet.google.com/new";
726
+ const GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS = 6e4;
727
+ const GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS = 1e4;
728
+ const GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS = 1e3;
729
+ const GOOGLE_MEET_BROWSER_POLL_MS = 500;
730
+ var GoogleMeetBrowserManualActionError = class extends Error {
731
+ constructor(payload) {
732
+ const prefix = payload.manualActionReason ? `${payload.manualActionReason}: ` : "";
733
+ super(`${prefix}${payload.manualActionMessage}`);
734
+ this.name = "GoogleMeetBrowserManualActionError";
735
+ this.payload = {
736
+ source: "browser",
737
+ error: this.message,
738
+ ...payload
739
+ };
740
+ }
741
+ };
742
+ function isGoogleMeetBrowserManualActionError(error) {
743
+ return error instanceof GoogleMeetBrowserManualActionError;
744
+ }
745
+ function formatBrowserAutomationError(error) {
746
+ if (error instanceof Error) return error.message;
747
+ try {
748
+ return JSON.stringify(error);
749
+ } catch {
750
+ return "unknown error";
751
+ }
752
+ }
753
+ function isBrowserNavigationInterruption(error) {
754
+ return /execution context was destroyed|navigation|target closed/i.test(formatBrowserAutomationError(error));
755
+ }
756
+ function isGoogleMeetCreateTab(tab) {
757
+ const url = tab.url ?? "";
758
+ if (/^https:\/\/meet\.google\.com\/(?:new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.test(url)) return true;
759
+ return url.startsWith("https://accounts.google.com/") && /sign in|google accounts|meet/i.test(tab.title ?? "");
760
+ }
761
+ async function findGoogleMeetCreateTab(params) {
762
+ return asBrowserTabs(await callBrowserProxyOnNode({
763
+ runtime: params.runtime,
764
+ nodeId: params.nodeId,
765
+ method: "GET",
766
+ path: "/tabs",
767
+ timeoutMs: params.timeoutMs
768
+ })).find(isGoogleMeetCreateTab);
769
+ }
770
+ async function focusBrowserTab(params) {
771
+ await callBrowserProxyOnNode({
772
+ runtime: params.runtime,
773
+ nodeId: params.nodeId,
774
+ method: "POST",
775
+ path: "/tabs/focus",
776
+ body: { targetId: params.targetId },
777
+ timeoutMs: params.timeoutMs
778
+ });
779
+ }
780
+ function readStringArray(value) {
781
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : void 0;
782
+ }
783
+ function readBrowserCreateResult(result) {
784
+ const record = result && typeof result === "object" ? result : {};
785
+ const nested = record.result && typeof record.result === "object" ? record.result : record;
786
+ return {
787
+ meetingUri: typeof nested.meetingUri === "string" ? nested.meetingUri : void 0,
788
+ browserUrl: typeof nested.browserUrl === "string" ? nested.browserUrl : void 0,
789
+ browserTitle: typeof nested.browserTitle === "string" ? nested.browserTitle : void 0,
790
+ manualAction: typeof nested.manualAction === "string" ? nested.manualAction : void 0,
791
+ manualActionReason: typeof nested.manualActionReason === "string" ? nested.manualActionReason : void 0,
792
+ notes: readStringArray(nested.notes),
793
+ retryAfterMs: typeof nested.retryAfterMs === "number" && Number.isFinite(nested.retryAfterMs) ? nested.retryAfterMs : void 0
794
+ };
795
+ }
796
+ const CREATE_MEET_FROM_BROWSER_SCRIPT = `async () => {
797
+ const meetUrlPattern = /^https:\\/\\/meet\\.google\\.com\\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i;
798
+ const text = (node) => (node?.innerText || node?.textContent || "").trim();
799
+ const current = () => location.href;
800
+ const notes = [];
801
+ const findButton = (pattern) =>
802
+ [...document.querySelectorAll("button")].find((button) => {
803
+ const label = [
804
+ button.getAttribute("aria-label"),
805
+ button.getAttribute("data-tooltip"),
806
+ text(button),
807
+ ]
808
+ .filter(Boolean)
809
+ .join(" ");
810
+ return pattern.test(label) && !button.disabled;
811
+ });
812
+ const clickButton = (pattern, note) => {
813
+ const button = findButton(pattern);
814
+ if (!button) {
815
+ return false;
816
+ }
817
+ button.click();
818
+ notes.push(note);
819
+ return true;
820
+ };
821
+ if (!current().startsWith("https://meet.google.com/")) {
822
+ return {
823
+ manualActionReason: "google-login-required",
824
+ manualAction: "Sign in to Google in the Klaw browser profile, then retry meeting creation.",
825
+ browserUrl: current(),
826
+ browserTitle: document.title,
827
+ notes,
828
+ };
829
+ }
830
+ const href = current();
831
+ if (meetUrlPattern.test(href)) {
832
+ return { meetingUri: href, browserUrl: href, browserTitle: document.title, notes };
833
+ }
834
+ const pageText = text(document.body);
835
+ if (clickButton(/\\buse microphone\\b/i, "Accepted Meet microphone prompt with browser automation.")) {
836
+ return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
837
+ }
838
+ if (
839
+ clickButton(
840
+ /continue without microphone/i,
841
+ "Continued through Meet microphone prompt with browser automation.",
842
+ )
843
+ ) {
844
+ return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
845
+ }
846
+ if (/do you want people to hear you in the meeting/i.test(pageText)) {
847
+ return {
848
+ manualActionReason: "meet-audio-choice-required",
849
+ manualAction: "Meet is showing the microphone choice. Click Use microphone in the Klaw browser profile, then retry meeting creation.",
850
+ browserUrl: href,
851
+ browserTitle: document.title,
852
+ notes,
853
+ };
854
+ }
855
+ if (/allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera)/i.test(pageText)) {
856
+ return {
857
+ manualActionReason: "meet-permission-required",
858
+ manualAction: "Allow microphone/camera permissions for Meet in the Klaw browser profile, then retry meeting creation.",
859
+ browserUrl: href,
860
+ browserTitle: document.title,
861
+ notes,
862
+ };
863
+ }
864
+ if (/couldn't create|unable to create/i.test(pageText)) {
865
+ return {
866
+ manualAction: "Resolve the Google Meet page prompt in the Klaw browser profile, then retry meeting creation.",
867
+ browserUrl: href,
868
+ browserTitle: document.title,
869
+ notes,
870
+ };
871
+ }
872
+ if (location.hostname.toLowerCase() === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText)) {
873
+ return {
874
+ manualActionReason: "google-login-required",
875
+ manualAction: "Sign in to Google in the Klaw browser profile, then retry meeting creation.",
876
+ browserUrl: href,
877
+ browserTitle: document.title,
878
+ notes,
879
+ };
880
+ }
881
+ return {
882
+ retryAfterMs: 500,
883
+ browserUrl: current(),
884
+ browserTitle: document.title,
885
+ notes,
886
+ };
887
+ }`;
888
+ async function createMeetWithBrowserProxyOnNode(params) {
889
+ const nodeId = await resolveChromeNode({
890
+ runtime: params.runtime,
891
+ requestedNode: params.config.chromeNode.node
892
+ });
893
+ const timeoutMs = Math.max(GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS, params.config.chrome.joinTimeoutMs);
894
+ const stepTimeoutMs = Math.min(timeoutMs, GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS);
895
+ let tab = await findGoogleMeetCreateTab({
896
+ runtime: params.runtime,
897
+ nodeId,
898
+ timeoutMs: stepTimeoutMs
899
+ });
900
+ if (tab?.targetId) await focusBrowserTab({
901
+ runtime: params.runtime,
902
+ nodeId,
903
+ targetId: tab.targetId,
904
+ timeoutMs: stepTimeoutMs
905
+ });
906
+ else tab = readBrowserTab(await callBrowserProxyOnNode({
907
+ runtime: params.runtime,
908
+ nodeId,
909
+ method: "POST",
910
+ path: "/tabs/open",
911
+ body: { url: GOOGLE_MEET_NEW_URL },
912
+ timeoutMs: stepTimeoutMs
913
+ }));
914
+ const targetId = tab?.targetId;
915
+ if (!targetId) throw new Error("Browser fallback opened Google Meet but did not return a targetId.");
916
+ const notes = /* @__PURE__ */ new Set();
917
+ let lastResult;
918
+ let lastError;
919
+ const deadline = Date.now() + timeoutMs;
920
+ while (Date.now() <= deadline) try {
921
+ const result = readBrowserCreateResult(await callBrowserProxyOnNode({
922
+ runtime: params.runtime,
923
+ nodeId,
924
+ method: "POST",
925
+ path: "/act",
926
+ body: {
927
+ kind: "evaluate",
928
+ targetId,
929
+ fn: CREATE_MEET_FROM_BROWSER_SCRIPT
930
+ },
931
+ timeoutMs: stepTimeoutMs
932
+ }));
933
+ lastResult = result;
934
+ for (const note of result.notes ?? []) notes.add(note);
935
+ if (result.meetingUri) return {
936
+ source: "browser",
937
+ nodeId,
938
+ targetId,
939
+ meetingUri: result.meetingUri,
940
+ browserUrl: result.browserUrl,
941
+ browserTitle: result.browserTitle,
942
+ notes: [...notes]
943
+ };
944
+ if (result.manualAction) throw new GoogleMeetBrowserManualActionError({
945
+ manualActionRequired: true,
946
+ manualActionReason: result.manualActionReason,
947
+ manualActionMessage: result.manualAction,
948
+ browser: {
949
+ nodeId,
950
+ targetId,
951
+ browserUrl: result.browserUrl,
952
+ browserTitle: result.browserTitle,
953
+ notes: [...notes]
954
+ }
955
+ });
956
+ await sleep(result.retryAfterMs ?? GOOGLE_MEET_BROWSER_POLL_MS);
957
+ } catch (error) {
958
+ lastError = error;
959
+ if (!isBrowserNavigationInterruption(error)) throw error;
960
+ await sleep(GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS);
961
+ }
962
+ throw new Error(lastResult?.manualAction ?? `Google Meet did not return a meeting URL from the browser create flow before timeout.${lastError ? ` Last browser automation error: ${formatBrowserAutomationError(lastError)}` : ""}`);
963
+ }
964
+ //#endregion
965
+ export { googleApiError as _, isSameMeetUrlForReuse as a, resolveChromeNode as c, createGoogleMeetSpace as d, endGoogleMeetActiveConference as f, fetchLatestGoogleMeetConferenceRecord as g, fetchGoogleMeetSpace as h, callBrowserProxyOnNode as i, resolveChromeNodeInfo as l, fetchGoogleMeetAttendance as m, isGoogleMeetBrowserManualActionError as n, normalizeMeetUrlForReuse as o, fetchGoogleMeetArtifacts as p, asBrowserTabs as r, readBrowserTab as s, createMeetWithBrowserProxyOnNode as t, buildGoogleMeetPreflightReport as u };