import { normalizeGoogleDiscoveryUrl } from "./discovery";import { compactGoogleOAuthScopes } from "./oauth-scopes";import { googleOAuthConsentScopesForPreset } from "./service-policy";import type { HealthCheckSpec, IntegrationPreset } from "@executor-js/sdk/core"; export { googleOAuthConsentScopes, googleOAuthConsentScopesForPreset } from "./service-policy"; export interface GooglePreset { readonly id: string; readonly name: string; readonly summary: string; readonly url?: string; readonly icon?: string; readonly featured?: boolean;} export type GoogleOpenApiOAuthAudience = | "standard-user" | "advanced-user" | "workspace-admin" | "unsupported-user"; export type GoogleOpenApiPreset = GooglePreset & { readonly oauthAudience: GoogleOpenApiOAuthAudience;}; const gd = (service: string, version: string) => `https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`; const GOOGLE_G = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg";export const GOOGLE_BUNDLE_PRESET_ID = "google";export const GOOGLE_PHOTOS_PRESET_ID = "google-photos";export const GOOGLE_PHOTOS_ICON = "https://www.gstatic.com/images/branding/product/2x/photos_96dp.png"; export const googleOpenApiBundlePreset: GooglePreset = { id: GOOGLE_BUNDLE_PRESET_ID, name: "Google", summary: "Bundle Gmail, Calendar, Drive, Docs, and other Google APIs into one integration.", icon: GOOGLE_G, featured: true,}; export const googlePhotosOpenApiBundlePreset: GooglePreset = { id: GOOGLE_PHOTOS_PRESET_ID, name: "Google Photos", summary: "Albums, uploads, app-created media, and user-selected picker media.", icon: GOOGLE_PHOTOS_ICON, featured: true,}; export const googleOpenApiPresets: readonly GoogleOpenApiPreset[] = [ { id: "google-calendar", name: "Google Calendar", summary: "Calendars, events, ACLs, and scheduling.", url: gd("calendar", "v3"), icon: "https://fonts.gstatic.com/s/i/productlogos/calendar_2020q4/v8/192px.svg", featured: true, oauthAudience: "standard-user", }, { id: "google-meet", name: "Google Meet", summary: "Meeting spaces, conference records, participants, recordings, and transcripts.", url: "https://meet.googleapis.com/$discovery/rest?version=v2", icon: "https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v8/192px.svg", featured: true, oauthAudience: "standard-user", }, { id: "google-gmail", name: "Gmail", summary: "Messages, threads, labels, and drafts.", url: gd("gmail", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/web-96dp/logo_gmail_2020q4_color_2x_web_96dp.png", featured: true, oauthAudience: "standard-user", }, { id: "google-sheets", name: "Google Sheets", summary: "Spreadsheets, values, ranges, and formatting.", url: gd("sheets", "v4"), icon: "https://fonts.gstatic.com/s/i/productlogos/sheets_2020q4/v8/192px.svg", featured: true, oauthAudience: "standard-user", }, { id: "google-drive", name: "Google Drive", summary: "Files, folders, permissions, and shared drives.", url: gd("drive", "v3"), icon: "https://fonts.gstatic.com/s/i/productlogos/drive_2020q4/v8/192px.svg", featured: true, oauthAudience: "standard-user", }, { id: "google-docs", name: "Google Docs", summary: "Documents, structural edits, and formatting.", url: gd("docs", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/docs_2020q4/v12/192px.svg", featured: true, oauthAudience: "standard-user", }, { id: "google-slides", name: "Google Slides", summary: "Presentations, slides, page elements, and deck updates.", url: gd("slides", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/slides_2020q4/v12/192px.svg", oauthAudience: "standard-user", }, { id: "google-forms", name: "Google Forms", summary: "Forms, questions, responses, and quizzes.", url: "https://forms.googleapis.com/$discovery/rest?version=v1", icon: "https://fonts.gstatic.com/s/i/productlogos/forms_2020q4/v6/192px.svg", oauthAudience: "standard-user", }, { id: "google-tasks", name: "Google Tasks", summary: "Task lists, task items, notes, and due dates.", url: gd("tasks", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/tasks/v5/192px.svg", oauthAudience: "standard-user", }, { id: "google-people", name: "Google People", summary: "Contacts, profiles, directory people, and contact groups.", url: gd("people", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/contacts_2022/v2/192px.svg", oauthAudience: "standard-user", }, { id: "google-photos-library", name: "Google Photos Library", summary: "Albums, uploads, and app-created media through Google Photos.", url: gd("photoslibrary", "v1"), icon: GOOGLE_PHOTOS_ICON, oauthAudience: "advanced-user", }, { id: "google-photos-picker", name: "Google Photos Picker", summary: "Picker sessions and user-selected Google Photos media items.", url: "https://photospicker.googleapis.com/$discovery/rest?version=v1", icon: GOOGLE_PHOTOS_ICON, oauthAudience: "advanced-user", }, { id: "google-chat", name: "Google Chat", summary: "Spaces, messages, members, reactions, and chat workflows.", url: gd("chat", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/chat_2020q4/v8/192px.svg", oauthAudience: "advanced-user", }, { id: "google-keep", name: "Google Keep", summary: "Create, list, delete, and share notes; download attachments.", url: "https://keep.googleapis.com/$discovery/rest?version=v1", icon: "https://fonts.gstatic.com/s/i/productlogos/keep_2020q4/v8/192px.svg", oauthAudience: "unsupported-user", }, { id: "google-youtube-data", name: "YouTube Data", summary: "Channels, playlists, videos, comments, and uploads.", url: gd("youtube", "v3"), icon: "https://fonts.gstatic.com/s/i/productlogos/youtube/v9/192px.svg", oauthAudience: "advanced-user", }, { id: "google-search-console", name: "Google Search Console", summary: "Sites, sitemaps, URL inspection, and search performance.", url: gd("searchconsole", "v1"), icon: GOOGLE_G, oauthAudience: "standard-user", }, { id: "google-classroom", name: "Google Classroom", summary: "Courses, rosters, coursework, and grading.", url: gd("classroom", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/classroom/v7/192px.svg", oauthAudience: "advanced-user", }, { id: "google-admin-directory", name: "Google Admin Directory", summary: "Users, groups, org units, roles, and domain resources.", url: "https://admin.googleapis.com/$discovery/rest?version=directory_v1", icon: "https://fonts.gstatic.com/s/i/productlogos/admin_2020q4/v6/192px.svg", oauthAudience: "workspace-admin", }, { id: "google-admin-reports", name: "Google Admin Reports", summary: "Audit events, usage reports, and admin activity logs.", url: "https://admin.googleapis.com/$discovery/rest?version=reports_v1", icon: "https://fonts.gstatic.com/s/i/productlogos/admin_2020q4/v6/192px.svg", oauthAudience: "workspace-admin", }, { id: "google-apps-script", name: "Google Apps Script", summary: "Projects, deployments, versions, processes, and metrics.", url: gd("script", "v1"), icon: "https://fonts.gstatic.com/s/i/productlogos/apps_script/v10/192px.svg", oauthAudience: "advanced-user", }, { id: "google-bigquery", name: "Google BigQuery", summary: "Datasets, tables, jobs, and analytical queries.", url: gd("bigquery", "v2"), icon: "https://fonts.gstatic.com/s/i/productlogos/google_cloud/v6/192px.svg", oauthAudience: "advanced-user", }, { id: "google-cloud-resource-manager", name: "Google Cloud Resource Manager", summary: "Projects, folders, organizations, and IAM hierarchy.", url: "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v3", icon: "https://fonts.gstatic.com/s/i/productlogos/google_cloud/v6/192px.svg", oauthAudience: "advanced-user", },]; export const googleStandardUserOAuthPresets = googleOpenApiPresets.filter( (preset) => preset.oauthAudience === "standard-user",); export const googlePhotosPresetIds: readonly string[] = [ "google-photos-library", "google-photos-picker",]; export const googlePhotosOpenApiPresets: readonly GoogleOpenApiPreset[] = googleOpenApiPresets.filter((preset) => googlePhotosPresetIds.includes(preset.id)); // ---------------------------------------------------------------------------// Representative consent scopes per preset.//// The picker shows the OAuth consent a user is about to grant BEFORE connecting// (the "View scopes" panel), but the authoritative scope list only exists in// each API's live Discovery document, which the add flow fetches lazily at// provider add time. To preview consent without N network round-trips, each preset// declares the broad top-level scope(s) a full integration grants. These flow// through `googleOAuthConsentBatches` (which compacts sub-scopes under their// broad parent), so the previewed grant matches the unioned scopes the bundle// converter ultimately stores. Grounded against each API's published Discovery// `auth.oauth2.scopes`.// --------------------------------------------------------------------------- export const googleServiceSlug = (presetId: string): string => presetId.replaceAll("-", "_"); const GOOGLE_OAUTH_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth";const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2";const GOOGLE_IDENTITY_SCOPES: readonly string[] = ["openid", "email", "profile"];const GOOGLE_HEALTH_CHECKS: Readonly<Record<string, HealthCheckSpec>> = { "google-calendar": { operation: "calendar.calendarList.list" }, "google-meet": { operation: "meet.conferenceRecords.list" }, "google-gmail": { operation: "gmail.users.labels.list", args: { userId: "me" }, }, "google-drive": { operation: "drive.about.get", args: { fields: "user" }, }, "google-tasks": { operation: "tasks.tasklists.list", args: { maxResults: 1 }, }, "google-people": { operation: "people.people.get", args: { resourceName: "people/me", personFields: "emailAddresses", identityField: "emailAddresses.0.value", }, }, "google-photos-library": { operation: "photoslibrary.albums.list" }, "google-chat": { operation: "chat.spaces.list" }, "google-keep": { operation: "keep.notes.list" }, "google-youtube-data": { operation: "youtube.channels.list", args: { part: "id", mine: true }, }, "google-search-console": { operation: "webmasters.sites.list" }, "google-classroom": { operation: "classroom.courses.list" }, "google-admin-directory": { operation: "directory.users.list", args: { customer: "my_customer", maxResults: 1 }, }, "google-admin-reports": { operation: "reports.activities.list", args: { userKey: "all", applicationName: "login", maxResults: 1 }, }, "google-apps-script": { operation: "script.processes.list" }, "google-bigquery": { operation: "bigquery.projects.list", args: { maxResults: 1 }, }, "google-cloud-resource-manager": { operation: "cloudresourcemanager.projects.search", args: { pageSize: 1 }, },}; /** Complete Google OAuth scope set requested by a catalog preset, including * identity scopes used to label and distinguish connected accounts. */export const googleCatalogOAuthScopesForPreset = (presetId: string): readonly string[] => compactGoogleOAuthScopes([ ...GOOGLE_IDENTITY_SCOPES, ...googleOAuthConsentScopesForPreset(presetId), ]); const googleCatalogAuthTemplate = (presetId: string) => [ { slug: GOOGLE_OAUTH_SECURITY_SCHEME, kind: "oauth2" as const, authorizationUrl: GOOGLE_OAUTH_AUTHORIZATION_URL, tokenUrl: GOOGLE_OAUTH_TOKEN_URL, scopes: googleCatalogOAuthScopesForPreset(presetId), },]; export const googleCatalog: readonly IntegrationPreset[] = googleOpenApiPresets .filter((preset) => preset.oauthAudience !== "unsupported-user") .map((preset) => ({ id: preset.id, name: preset.name, summary: preset.summary, ...(preset.url ? { url: preset.url } : {}), ...(preset.icon ? { icon: preset.icon } : {}), ...(preset.featured ? { featured: preset.featured } : {}), family: "google", specFormat: "google-discovery", defaultSlug: googleServiceSlug(preset.id), authTemplate: googleCatalogAuthTemplate(preset.id), ...(GOOGLE_HEALTH_CHECKS[preset.id] ? { healthCheck: GOOGLE_HEALTH_CHECKS[preset.id] } : {}), })); // ---------------------------------------------------------------------------// Resolve a stored/normalized Discovery URL back to its preset, so a bundled// `google` integration can surface each selected API's `oauthAudience` (e.g. a// caution on a connection's auth method when admin-only or unsupported-consent// APIs are part of the bundle).// --------------------------------------------------------------------------- const normalizeGooglePresetUrl = (url: string): string => { const discoveryUrl = normalizeGoogleDiscoveryUrl(url); if (discoveryUrl) return discoveryUrl; const trimmed = url.trim(); if (!URL.canParse(trimmed)) return trimmed.replace(/\/$/, ""); const parsed = new URL(trimmed); parsed.hash = ""; parsed.searchParams.sort(); return parsed.toString().replace(/\/$/, "");}; const googlePresetsByNormalizedUrl: ReadonlyMap<string, GoogleOpenApiPreset> = new Map( googleOpenApiPresets.flatMap((preset) => preset.url ? [[normalizeGooglePresetUrl(preset.url), preset] as const] : [], ),); export const googlePresetForDiscoveryUrl = (url: string): GoogleOpenApiPreset | undefined => googlePresetsByNormalizedUrl.get(normalizeGooglePresetUrl(url)); /** The distinct caution-tier audiences (`workspace-admin`, `unsupported-user`) * among the supplied Discovery URLs - the ones whose consent the user should be * warned about. Returns `[]` when every URL is a standard/advanced API. */export const googleAudienceWarningsForUrls = ( urls: readonly string[],): readonly GoogleOpenApiOAuthAudience[] => { const seen = new Set<GoogleOpenApiOAuthAudience>(); for (const url of urls) { const audience = googlePresetForDiscoveryUrl(url)?.oauthAudience; if (audience === "workspace-admin" || audience === "unsupported-user") seen.add(audience); } return [...seen];};
import { normalizeGoogleDiscoveryUrl } from "./discovery";
import { compactGoogleOAuthScopes } from "./oauth-scopes";
import { googleOAuthConsentScopesForPreset } from "./service-policy";
import type { HealthCheckSpec, IntegrationPreset } from "@executor-js/sdk/core";
export { googleOAuthConsentScopes, googleOAuthConsentScopesForPreset } from "./service-policy";
export interface GooglePreset {
readonly id: string;
readonly name: string;
readonly summary: string;
readonly url?: string;
readonly icon?: string;
readonly featured?: boolean;
}
export type GoogleOpenApiOAuthAudience =
| "standard-user"
| "advanced-user"
| "workspace-admin"
| "unsupported-user";
export type GoogleOpenApiPreset = GooglePreset & {
readonly oauthAudience: GoogleOpenApiOAuthAudience;
};
const gd = (service: string, version: string) =>
`https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`;
const GOOGLE_G = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg";
export const GOOGLE_BUNDLE_PRESET_ID = "google";
export const GOOGLE_PHOTOS_PRESET_ID = "google-photos";
export const GOOGLE_PHOTOS_ICON =
"https://www.gstatic.com/images/branding/product/2x/photos_96dp.png";
export const googleOpenApiBundlePreset: GooglePreset = {
id: GOOGLE_BUNDLE_PRESET_ID,
name: "Google",
summary: "Bundle Gmail, Calendar, Drive, Docs, and other Google APIs into one integration.",
icon: GOOGLE_G,
featured: true,
};
export const googlePhotosOpenApiBundlePreset: GooglePreset = {
id: GOOGLE_PHOTOS_PRESET_ID,
name: "Google Photos",
summary: "Albums, uploads, app-created media, and user-selected picker media.",
icon: GOOGLE_PHOTOS_ICON,
featured: true,
};
export const googleOpenApiPresets: readonly GoogleOpenApiPreset[] = [
{
id: "google-calendar",
name: "Google Calendar",
summary: "Calendars, events, ACLs, and scheduling.",
url: gd("calendar", "v3"),
icon: "https://fonts.gstatic.com/s/i/productlogos/calendar_2020q4/v8/192px.svg",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-meet",
name: "Google Meet",
summary: "Meeting spaces, conference records, participants, recordings, and transcripts.",
url: "https://meet.googleapis.com/$discovery/rest?version=v2",
icon: "https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v8/192px.svg",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-gmail",
name: "Gmail",
summary: "Messages, threads, labels, and drafts.",
url: gd("gmail", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/web-96dp/logo_gmail_2020q4_color_2x_web_96dp.png",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-sheets",
name: "Google Sheets",
summary: "Spreadsheets, values, ranges, and formatting.",
url: gd("sheets", "v4"),
icon: "https://fonts.gstatic.com/s/i/productlogos/sheets_2020q4/v8/192px.svg",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-drive",
name: "Google Drive",
summary: "Files, folders, permissions, and shared drives.",
url: gd("drive", "v3"),
icon: "https://fonts.gstatic.com/s/i/productlogos/drive_2020q4/v8/192px.svg",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-docs",
name: "Google Docs",
summary: "Documents, structural edits, and formatting.",
url: gd("docs", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/docs_2020q4/v12/192px.svg",
featured: true,
oauthAudience: "standard-user",
},
{
id: "google-slides",
name: "Google Slides",
summary: "Presentations, slides, page elements, and deck updates.",
url: gd("slides", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/slides_2020q4/v12/192px.svg",
oauthAudience: "standard-user",
},
{
id: "google-forms",
name: "Google Forms",
summary: "Forms, questions, responses, and quizzes.",
url: "https://forms.googleapis.com/$discovery/rest?version=v1",
icon: "https://fonts.gstatic.com/s/i/productlogos/forms_2020q4/v6/192px.svg",
oauthAudience: "standard-user",
},
{
id: "google-tasks",
name: "Google Tasks",
summary: "Task lists, task items, notes, and due dates.",
url: gd("tasks", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/tasks/v5/192px.svg",
oauthAudience: "standard-user",
},
{
id: "google-people",
name: "Google People",
summary: "Contacts, profiles, directory people, and contact groups.",
url: gd("people", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/contacts_2022/v2/192px.svg",
oauthAudience: "standard-user",
},
{
id: "google-photos-library",
name: "Google Photos Library",
summary: "Albums, uploads, and app-created media through Google Photos.",
url: gd("photoslibrary", "v1"),
icon: GOOGLE_PHOTOS_ICON,
oauthAudience: "advanced-user",
},
{
id: "google-photos-picker",
name: "Google Photos Picker",
summary: "Picker sessions and user-selected Google Photos media items.",
url: "https://photospicker.googleapis.com/$discovery/rest?version=v1",
icon: GOOGLE_PHOTOS_ICON,
oauthAudience: "advanced-user",
},
{
id: "google-chat",
name: "Google Chat",
summary: "Spaces, messages, members, reactions, and chat workflows.",
url: gd("chat", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/chat_2020q4/v8/192px.svg",
oauthAudience: "advanced-user",
},
{
id: "google-keep",
name: "Google Keep",
summary: "Create, list, delete, and share notes; download attachments.",
url: "https://keep.googleapis.com/$discovery/rest?version=v1",
icon: "https://fonts.gstatic.com/s/i/productlogos/keep_2020q4/v8/192px.svg",
oauthAudience: "unsupported-user",
},
{
id: "google-youtube-data",
name: "YouTube Data",
summary: "Channels, playlists, videos, comments, and uploads.",
url: gd("youtube", "v3"),
icon: "https://fonts.gstatic.com/s/i/productlogos/youtube/v9/192px.svg",
oauthAudience: "advanced-user",
},
{
id: "google-search-console",
name: "Google Search Console",
summary: "Sites, sitemaps, URL inspection, and search performance.",
url: gd("searchconsole", "v1"),
icon: GOOGLE_G,
oauthAudience: "standard-user",
},
{
id: "google-classroom",
name: "Google Classroom",
summary: "Courses, rosters, coursework, and grading.",
url: gd("classroom", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/classroom/v7/192px.svg",
oauthAudience: "advanced-user",
},
{
id: "google-admin-directory",
name: "Google Admin Directory",
summary: "Users, groups, org units, roles, and domain resources.",
url: "https://admin.googleapis.com/$discovery/rest?version=directory_v1",
icon: "https://fonts.gstatic.com/s/i/productlogos/admin_2020q4/v6/192px.svg",
oauthAudience: "workspace-admin",
},
{
id: "google-admin-reports",
name: "Google Admin Reports",
summary: "Audit events, usage reports, and admin activity logs.",
url: "https://admin.googleapis.com/$discovery/rest?version=reports_v1",
icon: "https://fonts.gstatic.com/s/i/productlogos/admin_2020q4/v6/192px.svg",
oauthAudience: "workspace-admin",
},
{
id: "google-apps-script",
name: "Google Apps Script",
summary: "Projects, deployments, versions, processes, and metrics.",
url: gd("script", "v1"),
icon: "https://fonts.gstatic.com/s/i/productlogos/apps_script/v10/192px.svg",
oauthAudience: "advanced-user",
},
{
id: "google-bigquery",
name: "Google BigQuery",
summary: "Datasets, tables, jobs, and analytical queries.",
url: gd("bigquery", "v2"),
icon: "https://fonts.gstatic.com/s/i/productlogos/google_cloud/v6/192px.svg",
oauthAudience: "advanced-user",
},
{
id: "google-cloud-resource-manager",
name: "Google Cloud Resource Manager",
summary: "Projects, folders, organizations, and IAM hierarchy.",
url: "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v3",
icon: "https://fonts.gstatic.com/s/i/productlogos/google_cloud/v6/192px.svg",
oauthAudience: "advanced-user",
},
];
export const googleStandardUserOAuthPresets = googleOpenApiPresets.filter(
(preset) => preset.oauthAudience === "standard-user",
);
export const googlePhotosPresetIds: readonly string[] = [
"google-photos-library",
"google-photos-picker",
];
export const googlePhotosOpenApiPresets: readonly GoogleOpenApiPreset[] =
googleOpenApiPresets.filter((preset) => googlePhotosPresetIds.includes(preset.id));
// ---------------------------------------------------------------------------
// Representative consent scopes per preset.
//
// The picker shows the OAuth consent a user is about to grant BEFORE connecting
// (the "View scopes" panel), but the authoritative scope list only exists in
// each API's live Discovery document, which the add flow fetches lazily at
// provider add time. To preview consent without N network round-trips, each preset
// declares the broad top-level scope(s) a full integration grants. These flow
// through `googleOAuthConsentBatches` (which compacts sub-scopes under their
// broad parent), so the previewed grant matches the unioned scopes the bundle
// converter ultimately stores. Grounded against each API's published Discovery
// `auth.oauth2.scopes`.
// ---------------------------------------------------------------------------
export const googleServiceSlug = (presetId: string): string => presetId.replaceAll("-", "_");
const GOOGLE_OAUTH_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2";
const GOOGLE_IDENTITY_SCOPES: readonly string[] = ["openid", "email", "profile"];
const GOOGLE_HEALTH_CHECKS: Readonly<Record<string, HealthCheckSpec>> = {
"google-calendar": { operation: "calendar.calendarList.list" },
"google-meet": { operation: "meet.conferenceRecords.list" },
"google-gmail": {
operation: "gmail.users.labels.list",
args: { userId: "me" },
},
"google-drive": {
operation: "drive.about.get",
args: { fields: "user" },
},
"google-tasks": {
operation: "tasks.tasklists.list",
args: { maxResults: 1 },
},
"google-people": {
operation: "people.people.get",
args: {
resourceName: "people/me",
personFields: "emailAddresses",
identityField: "emailAddresses.0.value",
},
},
"google-photos-library": { operation: "photoslibrary.albums.list" },
"google-chat": { operation: "chat.spaces.list" },
"google-keep": { operation: "keep.notes.list" },
"google-youtube-data": {
operation: "youtube.channels.list",
args: { part: "id", mine: true },
},
"google-search-console": { operation: "webmasters.sites.list" },
"google-classroom": { operation: "classroom.courses.list" },
"google-admin-directory": {
operation: "directory.users.list",
args: { customer: "my_customer", maxResults: 1 },
},
"google-admin-reports": {
operation: "reports.activities.list",
args: { userKey: "all", applicationName: "login", maxResults: 1 },
},
"google-apps-script": { operation: "script.processes.list" },
"google-bigquery": {
operation: "bigquery.projects.list",
args: { maxResults: 1 },
},
"google-cloud-resource-manager": {
operation: "cloudresourcemanager.projects.search",
args: { pageSize: 1 },
},
};
/** Complete Google OAuth scope set requested by a catalog preset, including
* identity scopes used to label and distinguish connected accounts. */
export const googleCatalogOAuthScopesForPreset = (presetId: string): readonly string[] =>
compactGoogleOAuthScopes([
...GOOGLE_IDENTITY_SCOPES,
...googleOAuthConsentScopesForPreset(presetId),
]);
const googleCatalogAuthTemplate = (presetId: string) => [
{
slug: GOOGLE_OAUTH_SECURITY_SCHEME,
kind: "oauth2" as const,
authorizationUrl: GOOGLE_OAUTH_AUTHORIZATION_URL,
tokenUrl: GOOGLE_OAUTH_TOKEN_URL,
scopes: googleCatalogOAuthScopesForPreset(presetId),
},
];
export const googleCatalog: readonly IntegrationPreset[] = googleOpenApiPresets
.filter((preset) => preset.oauthAudience !== "unsupported-user")
.map((preset) => ({
id: preset.id,
name: preset.name,
summary: preset.summary,
...(preset.url ? { url: preset.url } : {}),
...(preset.icon ? { icon: preset.icon } : {}),
...(preset.featured ? { featured: preset.featured } : {}),
family: "google",
specFormat: "google-discovery",
defaultSlug: googleServiceSlug(preset.id),
authTemplate: googleCatalogAuthTemplate(preset.id),
...(GOOGLE_HEALTH_CHECKS[preset.id] ? { healthCheck: GOOGLE_HEALTH_CHECKS[preset.id] } : {}),
}));
// ---------------------------------------------------------------------------
// Resolve a stored/normalized Discovery URL back to its preset, so a bundled
// `google` integration can surface each selected API's `oauthAudience` (e.g. a
// caution on a connection's auth method when admin-only or unsupported-consent
// APIs are part of the bundle).
// ---------------------------------------------------------------------------
const normalizeGooglePresetUrl = (url: string): string => {
const discoveryUrl = normalizeGoogleDiscoveryUrl(url);
if (discoveryUrl) return discoveryUrl;
const trimmed = url.trim();
if (!URL.canParse(trimmed)) return trimmed.replace(/\/$/, "");
const parsed = new URL(trimmed);
parsed.hash = "";
parsed.searchParams.sort();
return parsed.toString().replace(/\/$/, "");
};
const googlePresetsByNormalizedUrl: ReadonlyMap<string, GoogleOpenApiPreset> = new Map(
googleOpenApiPresets.flatMap((preset) =>
preset.url ? [[normalizeGooglePresetUrl(preset.url), preset] as const] : [],
),
);
export const googlePresetForDiscoveryUrl = (url: string): GoogleOpenApiPreset | undefined =>
googlePresetsByNormalizedUrl.get(normalizeGooglePresetUrl(url));
/** The distinct caution-tier audiences (`workspace-admin`, `unsupported-user`)
* among the supplied Discovery URLs - the ones whose consent the user should be
* warned about. Returns `[]` when every URL is a standard/advanced API. */
export const googleAudienceWarningsForUrls = (
urls: readonly string[],
): readonly GoogleOpenApiOAuthAudience[] => {
const seen = new Set<GoogleOpenApiOAuthAudience>();
for (const url of urls) {
const audience = googlePresetForDiscoveryUrl(url)?.oauthAudience;
if (audience === "workspace-admin" || audience === "unsupported-user") seen.add(audience);
}
return [...seen];
};