diff --git a/apps/docs/content/docs/en/tools/google_vault.mdx b/apps/docs/content/docs/en/tools/google_vault.mdx new file mode 100644 index 0000000000..30402c3ee3 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_vault.mdx @@ -0,0 +1,177 @@ +--- +title: Google Vault +description: Search, export, and manage holds/exports for Vault matters +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + `} +/> + +## Usage Instructions + +Connect Google Vault to create exports, list exports, and manage holds within matters. + + + +## Tools + +### `google_vault_create_matters_export` + +Create an export in a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | No description | +| `exportName` | string | Yes | No description | +| `corpus` | string | Yes | Data corpus to export \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | +| `accountEmails` | string | No | Comma-separated list of user emails to scope export | +| `orgUnitId` | string | No | Organization unit ID to scope export \(alternative to emails\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + +### `google_vault_list_matters_export` + +List exports for a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | No description | +| `pageSize` | number | No | No description | +| `pageToken` | string | No | No description | +| `exportId` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + +### `google_vault_download_export_file` + +Download a single file from a Google Vault export (GCS object) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | No description | +| `bucketName` | string | Yes | No description | +| `objectName` | string | Yes | No description | +| `fileName` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Downloaded Vault export file stored in execution files | + +### `google_vault_create_matters_holds` + +Create a hold in a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | No description | +| `holdName` | string | Yes | No description | +| `corpus` | string | Yes | Data corpus to hold \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | +| `accountEmails` | string | No | Comma-separated list of user emails to put on hold | +| `orgUnitId` | string | No | Organization unit ID to put on hold \(alternative to accounts\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + +### `google_vault_list_matters_holds` + +List holds for a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | No description | +| `pageSize` | number | No | No description | +| `pageToken` | string | No | No description | +| `holdId` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + +### `google_vault_create_matters` + +Create a new matter in Google Vault + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | No description | +| `description` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + +### `google_vault_list_matters` + +List matters, or get a specific matter if matterId is provided + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `pageSize` | number | No | No description | +| `pageToken` | string | No | No description | +| `matterId` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `output` | json | Vault API response data | +| `file` | json | Downloaded export file \(UserFile\) from execution files | + + + +## Notes + +- Category: `tools` +- Type: `google_vault` diff --git a/apps/sim/app/api/proxy/route.ts b/apps/sim/app/api/proxy/route.ts index 4d4f9bbeab..c207c7bc55 100644 --- a/apps/sim/app/api/proxy/route.ts +++ b/apps/sim/app/api/proxy/route.ts @@ -1,7 +1,9 @@ import { NextResponse } from 'next/server' +import { generateInternalToken } from '@/lib/auth/internal' import { isDev } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import { validateProxyUrl } from '@/lib/security/url-validation' +import { getBaseUrl } from '@/lib/urls/utils' import { generateRequestId } from '@/lib/utils' import { executeTool } from '@/tools' import { getTool, validateRequiredParametersAfterMerge } from '@/tools/utils' @@ -77,6 +79,79 @@ export async function GET(request: Request) { const targetUrl = url.searchParams.get('url') const requestId = generateRequestId() + // Vault download proxy: /api/proxy?vaultDownload=1&bucket=...&object=...&credentialId=... + const vaultDownload = url.searchParams.get('vaultDownload') + if (vaultDownload === '1') { + try { + const bucket = url.searchParams.get('bucket') + const objectParam = url.searchParams.get('object') + const credentialId = url.searchParams.get('credentialId') + + if (!bucket || !objectParam || !credentialId) { + return createErrorResponse('Missing bucket, object, or credentialId', 400) + } + + // Fetch access token using existing token API + const baseUrl = new URL(getBaseUrl()) + const tokenUrl = new URL('/api/auth/oauth/token', baseUrl) + + // Build headers: forward session cookies if present; include internal auth for server-side + const tokenHeaders: Record = { 'Content-Type': 'application/json' } + const incomingCookie = request.headers.get('cookie') + if (incomingCookie) tokenHeaders.Cookie = incomingCookie + try { + const internalToken = await generateInternalToken() + tokenHeaders.Authorization = `Bearer ${internalToken}` + } catch (_e) { + // best-effort internal auth + } + + // Optional workflow context for collaboration auth + const workflowId = url.searchParams.get('workflowId') || undefined + + const tokenRes = await fetch(tokenUrl.toString(), { + method: 'POST', + headers: tokenHeaders, + body: JSON.stringify({ credentialId, workflowId }), + }) + + if (!tokenRes.ok) { + const err = await tokenRes.text() + return createErrorResponse(`Failed to fetch access token: ${err}`, 401) + } + + const tokenJson = await tokenRes.json() + const accessToken = tokenJson.accessToken + if (!accessToken) { + return createErrorResponse('No access token available', 401) + } + + // Avoid double-encoding: incoming object may already be percent-encoded + const objectDecoded = decodeURIComponent(objectParam) + const gcsUrl = `https://storage.googleapis.com/storage/v1/b/${encodeURIComponent( + bucket + )}/o/${encodeURIComponent(objectDecoded)}?alt=media` + + const fileRes = await fetch(gcsUrl, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + if (!fileRes.ok) { + const errText = await fileRes.text() + return createErrorResponse(errText || 'Failed to download file', fileRes.status) + } + + const headers = new Headers() + fileRes.headers.forEach((v, k) => headers.set(k, v)) + return new NextResponse(fileRes.body, { status: 200, headers }) + } catch (error: any) { + logger.error(`[${requestId}] Vault download proxy failed`, { + error: error instanceof Error ? error.message : String(error), + }) + return createErrorResponse('Vault download failed', 500) + } + } + if (!targetUrl) { logger.error(`[${requestId}] Missing 'url' parameter`) return createErrorResponse("Missing 'url' parameter", 400) diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts new file mode 100644 index 0000000000..b2556a9093 --- /dev/null +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -0,0 +1,284 @@ +import { GoogleVaultIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const GoogleVaultBlock: BlockConfig = { + type: 'google_vault', + name: 'Google Vault', + description: 'Search, export, and manage holds/exports for Vault matters', + authMode: AuthMode.OAuth, + longDescription: + 'Connect Google Vault to create exports, list exports, and manage holds within matters.', + docsLink: 'https://developers.google.com/vault', + category: 'tools', + bgColor: '#E8F0FE', + icon: GoogleVaultIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Create Export', id: 'create_matters_export' }, + { label: 'List Exports', id: 'list_matters_export' }, + { label: 'Download Export File', id: 'download_export_file' }, + { label: 'Create Hold', id: 'create_matters_holds' }, + { label: 'List Holds', id: 'list_matters_holds' }, + { label: 'Create Matter', id: 'create_matters' }, + { label: 'List Matters', id: 'list_matters' }, + ], + value: () => 'list_matters_export', + }, + + { + id: 'credential', + title: 'Google Vault Account', + type: 'oauth-input', + layout: 'full', + required: true, + provider: 'google-vault', + serviceId: 'google-vault', + requiredScopes: [ + 'https://www.googleapis.com/auth/ediscovery', + 'https://www.googleapis.com/auth/devstorage.read_only', + ], + placeholder: 'Select Google Vault account', + }, + // Create Hold inputs + { + id: 'matterId', + title: 'Matter ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Matter ID', + condition: () => ({ + field: 'operation', + value: [ + 'create_matters_export', + 'list_matters_export', + 'download_export_file', + 'create_matters_holds', + 'list_matters_holds', + ], + }), + }, + // Download Export File inputs + { + id: 'bucketName', + title: 'Bucket Name', + type: 'short-input', + layout: 'full', + placeholder: 'Vault export bucket (from cloudStorageSink.files.bucketName)', + condition: { field: 'operation', value: 'download_export_file' }, + required: true, + }, + { + id: 'objectName', + title: 'Object Name', + type: 'long-input', + layout: 'full', + placeholder: 'Vault export object (from cloudStorageSink.files.objectName)', + condition: { field: 'operation', value: 'download_export_file' }, + required: true, + }, + { + id: 'fileName', + title: 'File Name (optional)', + type: 'short-input', + layout: 'full', + placeholder: 'Override filename used for storage/display', + condition: { field: 'operation', value: 'download_export_file' }, + }, + { + id: 'exportName', + title: 'Export Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name for the export', + condition: { field: 'operation', value: 'create_matters_export' }, + required: true, + }, + { + id: 'holdName', + title: 'Hold Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name of the hold', + condition: { field: 'operation', value: 'create_matters_holds' }, + required: true, + }, + { + id: 'corpus', + title: 'Corpus', + type: 'dropdown', + layout: 'half', + options: [ + { id: 'MAIL', label: 'MAIL' }, + { id: 'DRIVE', label: 'DRIVE' }, + { id: 'GROUPS', label: 'GROUPS' }, + { id: 'HANGOUTS_CHAT', label: 'HANGOUTS_CHAT' }, + { id: 'VOICE', label: 'VOICE' }, + ], + condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + required: true, + }, + { + id: 'accountEmails', + title: 'Account Emails', + type: 'long-input', + layout: 'full', + placeholder: 'Comma-separated emails (alternative to Org Unit)', + condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + }, + { + id: 'orgUnitId', + title: 'Org Unit ID', + type: 'short-input', + layout: 'half', + placeholder: 'Org Unit ID (alternative to emails)', + condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + }, + { + id: 'exportId', + title: 'Export ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Export ID (optional to fetch a specific export)', + condition: { field: 'operation', value: 'list_matters_export' }, + }, + { + id: 'holdId', + title: 'Hold ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Hold ID (optional to fetch a specific hold)', + condition: { field: 'operation', value: 'list_matters_holds' }, + }, + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + layout: 'half', + placeholder: 'Number of items to return', + condition: { + field: 'operation', + value: ['list_matters_export', 'list_matters_holds', 'list_matters'], + }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + layout: 'half', + placeholder: 'Pagination token', + condition: { + field: 'operation', + value: ['list_matters_export', 'list_matters_holds', 'list_matters'], + }, + }, + + { + id: 'name', + title: 'Matter Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Matter name', + condition: { field: 'operation', value: 'create_matters' }, + required: true, + }, + { + id: 'description', + title: 'Description', + type: 'short-input', + layout: 'full', + placeholder: 'Optional description for the matter', + condition: { field: 'operation', value: 'create_matters' }, + }, + // Optional get specific matter by ID + { + id: 'matterId', + title: 'Matter ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Matter ID (optional to fetch a specific matter)', + condition: { field: 'operation', value: 'list_matters' }, + }, + ], + tools: { + access: [ + 'google_vault_create_matters_export', + 'google_vault_list_matters_export', + 'google_vault_download_export_file', + 'google_vault_create_matters_holds', + 'google_vault_list_matters_holds', + 'google_vault_create_matters', + 'google_vault_list_matters', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'create_matters_export': + return 'google_vault_create_matters_export' + case 'list_matters_export': + return 'google_vault_list_matters_export' + case 'download_export_file': + return 'google_vault_download_export_file' + case 'create_matters_holds': + return 'google_vault_create_matters_holds' + case 'list_matters_holds': + return 'google_vault_list_matters_holds' + case 'create_matters': + return 'google_vault_create_matters' + case 'list_matters': + return 'google_vault_list_matters' + default: + throw new Error(`Invalid Google Vault operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, ...rest } = params + return { + ...rest, + credential, + } + }, + }, + }, + inputs: { + // Core inputs + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Google Vault OAuth credential' }, + matterId: { type: 'string', description: 'Matter ID' }, + + // Create export inputs + exportName: { type: 'string', description: 'Name for the export' }, + corpus: { type: 'string', description: 'Data corpus (MAIL, DRIVE, GROUPS, etc.)' }, + accountEmails: { type: 'string', description: 'Comma-separated account emails' }, + orgUnitId: { type: 'string', description: 'Organization unit ID' }, + + // Create hold inputs + holdName: { type: 'string', description: 'Name for the hold' }, + + // Download export file inputs + bucketName: { type: 'string', description: 'GCS bucket name from export' }, + objectName: { type: 'string', description: 'GCS object name from export' }, + fileName: { type: 'string', description: 'Optional filename override' }, + + // List operations inputs + exportId: { type: 'string', description: 'Specific export ID to fetch' }, + holdId: { type: 'string', description: 'Specific hold ID to fetch' }, + pageSize: { type: 'number', description: 'Number of items per page' }, + pageToken: { type: 'string', description: 'Pagination token' }, + + // Create matter inputs + name: { type: 'string', description: 'Matter name' }, + description: { type: 'string', description: 'Matter description' }, + }, + outputs: { + // Common outputs + output: { type: 'json', description: 'Vault API response data' }, + // Download export file output + file: { type: 'json', description: 'Downloaded export file (UserFile) from execution files' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f225a1c3f4..1f30aea2c2 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -29,6 +29,7 @@ import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_form' import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets' +import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' import { HuggingFaceBlock } from '@/blocks/blocks/huggingface' import { HunterBlock } from '@/blocks/blocks/hunter' import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator' @@ -113,6 +114,7 @@ export const registry: Record = { google_forms: GoogleFormsBlock, google_search: GoogleSearchBlock, google_sheets: GoogleSheetsBlock, + google_vault: GoogleVaultBlock, huggingface: HuggingFaceBlock, hunter: HunterBlock, image_generator: ImageGeneratorBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 3ba23138e7..39f356cc61 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3711,3 +3711,18 @@ export const ResendIcon = (props: SVGProps) => ( /> ) + +export const GoogleVaultIcon = (props: SVGProps) => ( + + + + +) diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 9e22128230..680877a815 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -469,6 +469,22 @@ export const auth = betterAuth({ redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-forms`, }, + { + providerId: 'google-vault', + clientId: env.GOOGLE_CLIENT_ID as string, + clientSecret: env.GOOGLE_CLIENT_SECRET as string, + discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration', + accessType: 'offline', + scopes: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/ediscovery', + 'https://www.googleapis.com/auth/devstorage.read_only', + ], + prompt: 'consent', + redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-vault`, + }, + { providerId: 'microsoft-teams', clientId: env.MICROSOFT_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 7774ba1d51..803100b752 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -56,6 +56,7 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-vault' | 'google-forms' | 'github' | 'x' @@ -162,6 +163,18 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: (props) => GoogleIcon(props), scopes: ['https://www.googleapis.com/auth/calendar'], }, + 'google-vault': { + id: 'google-vault', + name: 'Google Vault', + description: 'Search, export, and manage matters/holds via Google Vault.', + providerId: 'google-vault', + icon: (props) => GoogleIcon(props), + baseProviderIcon: (props) => GoogleIcon(props), + scopes: [ + 'https://www.googleapis.com/auth/ediscovery', + 'https://www.googleapis.com/auth/devstorage.read_only', + ], + }, }, defaultService: 'gmail', }, @@ -534,6 +547,9 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] if (scopes.some((scope) => scope.includes('forms'))) { return 'google-forms' } + if (scopes.some((scope) => scope.includes('ediscovery'))) { + return 'google-vault' + } } else if (provider === 'microsoft-teams') { return 'microsoft-teams' } else if (provider === 'outlook') { @@ -947,7 +963,6 @@ export async function refreshOAuthToken( status: response.status, error: errorText, parsedError: errorData, - provider, providerId, }) throw new Error(`Failed to refresh token: ${response.status} ${errorText}`) diff --git a/apps/sim/tools/google_vault/create_matters.ts b/apps/sim/tools/google_vault/create_matters.ts new file mode 100644 index 0000000000..a903626466 --- /dev/null +++ b/apps/sim/tools/google_vault/create_matters.ts @@ -0,0 +1,46 @@ +import type { ToolConfig } from '@/tools/types' + +export interface GoogleVaultCreateMattersParams { + accessToken: string + name: string + description?: string +} + +// matters.create +// POST https://vault.googleapis.com/v1/matters +export const createMattersTool: ToolConfig = { + id: 'create_matters', + name: 'Vault Create Matter', + description: 'Create a new matter in Google Vault', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + name: { type: 'string', required: true, visibility: 'user-only' }, + description: { type: 'string', required: false, visibility: 'user-only' }, + }, + + request: { + url: () => `https://vault.googleapis.com/v1/matters`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ name: params.name, description: params.description }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to create matter') + } + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/create_matters_export.ts b/apps/sim/tools/google_vault/create_matters_export.ts new file mode 100644 index 0000000000..215eefad90 --- /dev/null +++ b/apps/sim/tools/google_vault/create_matters_export.ts @@ -0,0 +1,97 @@ +import type { GoogleVaultCreateMattersExportParams } from '@/tools/google_vault/types' +import type { ToolConfig } from '@/tools/types' + +// matters.exports.create +// POST https://vault.googleapis.com/v1/matters/{matterId}/exports +export const createMattersExportTool: ToolConfig = { + id: 'create_matters_export', + name: 'Vault Create Export (by Matter)', + description: 'Create an export in a matter', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + matterId: { type: 'string', required: true, visibility: 'user-only' }, + exportName: { type: 'string', required: true, visibility: 'user-only' }, + corpus: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Data corpus to export (MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE)', + }, + accountEmails: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of user emails to scope export', + }, + orgUnitId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization unit ID to scope export (alternative to emails)', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId}/exports`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + // Handle accountEmails - can be string (comma-separated) or array + let emails: string[] = [] + if (params.accountEmails) { + if (Array.isArray(params.accountEmails)) { + emails = params.accountEmails + } else if (typeof params.accountEmails === 'string') { + emails = params.accountEmails + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + } + } + + const scope = + emails.length > 0 + ? { accountInfo: { emails } } + : params.orgUnitId + ? { orgUnitInfo: { orgUnitId: params.orgUnitId } } + : {} + + const searchMethod = emails.length > 0 ? 'ACCOUNT' : params.orgUnitId ? 'ORG_UNIT' : undefined + + const query: any = { + corpus: params.corpus, + dataScope: 'ALL_DATA', + searchMethod: searchMethod, + terms: params.terms || undefined, + startTime: params.startTime || undefined, + endTime: params.endTime || undefined, + timeZone: params.timeZone || undefined, + ...scope, + } + + return { + name: params.exportName, + query, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to create export') + } + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/create_matters_holds.ts b/apps/sim/tools/google_vault/create_matters_holds.ts new file mode 100644 index 0000000000..e80a9b3890 --- /dev/null +++ b/apps/sim/tools/google_vault/create_matters_holds.ts @@ -0,0 +1,87 @@ +import type { GoogleVaultCreateMattersHoldsParams } from '@/tools/google_vault/types' +import type { ToolConfig } from '@/tools/types' + +// matters.holds.create +// POST https://vault.googleapis.com/v1/matters/{matterId}/holds +export const createMattersHoldsTool: ToolConfig = { + id: 'create_matters_holds', + name: 'Vault Create Hold (by Matter)', + description: 'Create a hold in a matter', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + matterId: { type: 'string', required: true, visibility: 'user-only' }, + holdName: { type: 'string', required: true, visibility: 'user-only' }, + corpus: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Data corpus to hold (MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE)', + }, + accountEmails: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of user emails to put on hold', + }, + orgUnitId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization unit ID to put on hold (alternative to accounts)', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId}/holds`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + // Build Hold body. One of accounts or orgUnit must be provided. + const body: any = { + name: params.holdName, + corpus: params.corpus, + } + + // Handle accountEmails - can be string (comma-separated) or array + let emails: string[] = [] + if (params.accountEmails) { + if (Array.isArray(params.accountEmails)) { + emails = params.accountEmails + } else if (typeof params.accountEmails === 'string') { + emails = params.accountEmails + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + } + } + + if (emails.length > 0) { + // Google Vault expects HeldAccount objects with 'email' or 'accountId'. Use 'email' here. + body.accounts = emails.map((email: string) => ({ email })) + } else if (params.orgUnitId) { + body.orgUnit = { orgUnitId: params.orgUnitId } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to create hold') + } + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/download_export_file.ts b/apps/sim/tools/google_vault/download_export_file.ts new file mode 100644 index 0000000000..9a386f2caf --- /dev/null +++ b/apps/sim/tools/google_vault/download_export_file.ts @@ -0,0 +1,132 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleVaultDownloadExportFileTool') + +interface DownloadParams { + accessToken: string + matterId: string + bucketName: string + objectName: string + fileName?: string +} + +export const downloadExportFileTool: ToolConfig = { + id: 'google_vault_download_export_file', + name: 'Vault Download Export File', + description: 'Download a single file from a Google Vault export (GCS object)', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: [ + 'https://www.googleapis.com/auth/ediscovery', + // Required to fetch the object bytes from the Cloud Storage bucket that Vault uses + 'https://www.googleapis.com/auth/devstorage.read_only', + ], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + matterId: { type: 'string', required: true, visibility: 'user-only' }, + bucketName: { type: 'string', required: true, visibility: 'user-only' }, + objectName: { type: 'string', required: true, visibility: 'user-only' }, + fileName: { type: 'string', required: false, visibility: 'user-only' }, + }, + + request: { + url: (params) => { + const bucket = encodeURIComponent(params.bucketName) + const object = encodeURIComponent(params.objectName) + // Use GCS media endpoint directly; framework will prefetch token and inject accessToken + return `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media` + }, + method: 'GET', + headers: (params) => ({ + // Access token is injected by the tools framework when 'credential' is present + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: DownloadParams) => { + if (!response.ok) { + let details: any + try { + details = await response.json() + } catch { + try { + const text = await response.text() + details = { error: text } + } catch { + details = undefined + } + } + throw new Error(details?.error || `Failed to download Vault export file (${response.status})`) + } + + // Since we're just doing a HEAD request to verify access, we need to fetch the actual file + if (!params?.accessToken || !params?.bucketName || !params?.objectName) { + throw new Error('Missing required parameters for download') + } + + const bucket = encodeURIComponent(params.bucketName) + const object = encodeURIComponent(params.objectName) + const downloadUrl = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media` + + // Fetch the actual file content + const downloadResponse = await fetch(downloadUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${params.accessToken}`, + }, + }) + + if (!downloadResponse.ok) { + const errorText = await downloadResponse.text().catch(() => '') + throw new Error(`Failed to download file: ${errorText || downloadResponse.statusText}`) + } + + const contentType = downloadResponse.headers.get('content-type') || 'application/octet-stream' + const disposition = downloadResponse.headers.get('content-disposition') || '' + const match = disposition.match(/filename\*=UTF-8''([^;]+)|filename="([^"]+)"/) + + let resolvedName = params.fileName + if (!resolvedName) { + if (match?.[1]) { + try { + resolvedName = decodeURIComponent(match[1]) + } catch { + resolvedName = match[1] + } + } else if (match?.[2]) { + resolvedName = match[2] + } else if (params.objectName) { + const parts = params.objectName.split('/') + resolvedName = parts[parts.length - 1] || 'vault-export.bin' + } else { + resolvedName = 'vault-export.bin' + } + } + + // Get the file as an array buffer and convert to Buffer + const arrayBuffer = await downloadResponse.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + return { + success: true, + output: { + file: { + name: resolvedName, + mimeType: contentType, + data: buffer, + size: buffer.length, + }, + }, + } + }, + + outputs: { + file: { type: 'file', description: 'Downloaded Vault export file stored in execution files' }, + }, +} diff --git a/apps/sim/tools/google_vault/index.ts b/apps/sim/tools/google_vault/index.ts new file mode 100644 index 0000000000..9eddf354af --- /dev/null +++ b/apps/sim/tools/google_vault/index.ts @@ -0,0 +1,7 @@ +export { createMattersTool } from '@/tools/google_vault/create_matters' +export { createMattersExportTool } from '@/tools/google_vault/create_matters_export' +export { createMattersHoldsTool } from '@/tools/google_vault/create_matters_holds' +export { downloadExportFileTool } from '@/tools/google_vault/download_export_file' +export { listMattersTool } from '@/tools/google_vault/list_matters' +export { listMattersExportTool } from '@/tools/google_vault/list_matters_export' +export { listMattersHoldsTool } from '@/tools/google_vault/list_matters_holds' diff --git a/apps/sim/tools/google_vault/list_matters.ts b/apps/sim/tools/google_vault/list_matters.ts new file mode 100644 index 0000000000..0dd625c39c --- /dev/null +++ b/apps/sim/tools/google_vault/list_matters.ts @@ -0,0 +1,57 @@ +import type { ToolConfig } from '@/tools/types' + +export interface GoogleVaultListMattersParams { + accessToken: string + pageSize?: number + pageToken?: string + matterId?: string // Optional get for a specific matter +} + +export const listMattersTool: ToolConfig = { + id: 'list_matters', + name: 'Vault List Matters', + description: 'List matters, or get a specific matter if matterId is provided', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + pageSize: { type: 'number', required: false, visibility: 'user-only' }, + pageToken: { type: 'string', required: false, visibility: 'hidden' }, + matterId: { type: 'string', required: false, visibility: 'user-only' }, + }, + + request: { + url: (params) => { + if (params.matterId) { + return `https://vault.googleapis.com/v1/matters/${params.matterId}` + } + const url = new URL('https://vault.googleapis.com/v1/matters') + // Handle pageSize - convert to number if needed + if (params.pageSize !== undefined && params.pageSize !== null) { + const pageSize = Number(params.pageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + url.searchParams.set('pageSize', String(pageSize)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + // Default BASIC view implicitly by omitting 'view' and 'state' params + return url.toString() + }, + method: 'GET', + headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list matters') + } + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/list_matters_export.ts b/apps/sim/tools/google_vault/list_matters_export.ts new file mode 100644 index 0000000000..348bc843cb --- /dev/null +++ b/apps/sim/tools/google_vault/list_matters_export.ts @@ -0,0 +1,55 @@ +import type { GoogleVaultListMattersExportParams } from '@/tools/google_vault/types' +import type { ToolConfig } from '@/tools/types' + +// matters.exports.list +// GET https://vault.googleapis.com/v1/matters/{matterId}/exports +export const listMattersExportTool: ToolConfig = { + id: 'list_matters_export', + name: 'Vault List Exports (by Matter)', + description: 'List exports for a matter', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + matterId: { type: 'string', required: true, visibility: 'user-only' }, + pageSize: { type: 'number', required: false, visibility: 'user-only' }, + pageToken: { type: 'string', required: false, visibility: 'hidden' }, + exportId: { type: 'string', required: false, visibility: 'user-only' }, + }, + + request: { + url: (params) => { + if (params.exportId) { + return `https://vault.googleapis.com/v1/matters/${params.matterId}/exports/${params.exportId}` + } + const url = new URL(`https://vault.googleapis.com/v1/matters/${params.matterId}/exports`) + // Handle pageSize - convert to number if needed + if (params.pageSize !== undefined && params.pageSize !== null) { + const pageSize = Number(params.pageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + url.searchParams.set('pageSize', String(pageSize)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list exports') + } + + // Return the raw API response without modifications + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/list_matters_holds.ts b/apps/sim/tools/google_vault/list_matters_holds.ts new file mode 100644 index 0000000000..e0c4378cee --- /dev/null +++ b/apps/sim/tools/google_vault/list_matters_holds.ts @@ -0,0 +1,52 @@ +import type { GoogleVaultListMattersHoldsParams } from '@/tools/google_vault/types' +import type { ToolConfig } from '@/tools/types' + +export const listMattersHoldsTool: ToolConfig = { + id: 'list_matters_holds', + name: 'Vault List Holds (by Matter)', + description: 'List holds for a matter', + version: '1.0', + + oauth: { + required: true, + provider: 'google-vault', + additionalScopes: ['https://www.googleapis.com/auth/ediscovery'], + }, + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden' }, + matterId: { type: 'string', required: true, visibility: 'user-only' }, + pageSize: { type: 'number', required: false, visibility: 'user-only' }, + pageToken: { type: 'string', required: false, visibility: 'hidden' }, + holdId: { type: 'string', required: false, visibility: 'user-only' }, + }, + + request: { + url: (params) => { + if (params.holdId) { + return `https://vault.googleapis.com/v1/matters/${params.matterId}/holds/${params.holdId}` + } + const url = new URL(`https://vault.googleapis.com/v1/matters/${params.matterId}/holds`) + // Handle pageSize - convert to number if needed + if (params.pageSize !== undefined && params.pageSize !== null) { + const pageSize = Number(params.pageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + url.searchParams.set('pageSize', String(pageSize)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + // Default BASIC_HOLD implicitly by omitting 'view' + return url.toString() + }, + method: 'GET', + headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list holds') + } + return { success: true, output: data } + }, +} diff --git a/apps/sim/tools/google_vault/types.ts b/apps/sim/tools/google_vault/types.ts new file mode 100644 index 0000000000..dff9aa42fa --- /dev/null +++ b/apps/sim/tools/google_vault/types.ts @@ -0,0 +1,52 @@ +import type { ToolResponse } from '@/tools/types' + +export interface GoogleVaultCommonParams { + accessToken: string + matterId: string +} + +// Exports +export interface GoogleVaultCreateMattersExportParams extends GoogleVaultCommonParams { + exportName: string + corpus: GoogleVaultCorpus + accountEmails?: string // Comma-separated list or array handled in the tool + orgUnitId?: string + terms?: string + startTime?: string + endTime?: string + timeZone?: string + includeSharedDrives?: boolean +} + +export interface GoogleVaultListMattersExportParams extends GoogleVaultCommonParams { + pageSize?: number + pageToken?: string + exportId?: string // Short input to fetch a specific export +} + +export interface GoogleVaultListMattersExportResponse extends ToolResponse { + output: any +} + +// Holds +// Simplified: default to BASIC_HOLD by omission in requests +export type GoogleVaultHoldView = 'BASIC_HOLD' | 'FULL_HOLD' + +export type GoogleVaultCorpus = 'MAIL' | 'DRIVE' | 'GROUPS' | 'HANGOUTS_CHAT' | 'VOICE' + +export interface GoogleVaultCreateMattersHoldsParams extends GoogleVaultCommonParams { + holdName: string + corpus: GoogleVaultCorpus + accountEmails?: string // Comma-separated list or array handled in the tool + orgUnitId?: string +} + +export interface GoogleVaultListMattersHoldsParams extends GoogleVaultCommonParams { + pageSize?: number + pageToken?: string + holdId?: string // Short input to fetch a specific hold +} + +export interface GoogleVaultListMattersHoldsResponse extends ToolResponse { + output: any +} diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index ed964a69d4..8f2b3d2946 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -236,6 +236,14 @@ export async function executeTool( `[${requestId}] Successfully got access token for ${toolId}, length: ${data.accessToken?.length || 0}` ) + // Preserve credential for downstream transforms while removing it from request payload + // so we don't leak it to external services. + if (contextParams.credential) { + ;(contextParams as any)._credentialId = contextParams.credential + } + if (workflowId) { + ;(contextParams as any)._workflowId = workflowId + } // Clean up params we don't need to pass to the actual tool contextParams.credential = undefined if (contextParams.workflowId) contextParams.workflowId = undefined diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 40a5329ffb..9fc727f15a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -55,6 +55,15 @@ import { googleSheetsUpdateTool, googleSheetsWriteTool, } from '@/tools/google_sheets' +import { + createMattersExportTool, + createMattersHoldsTool, + createMattersTool, + downloadExportFileTool, + listMattersExportTool, + listMattersHoldsTool, + listMattersTool, +} from '@/tools/google_vault' import { requestTool as httpRequest } from '@/tools/http' import { huggingfaceChatTool } from '@/tools/huggingface' import { @@ -356,6 +365,13 @@ export const tools: Record = { wikipedia_search: wikipediaSearchTool, wikipedia_content: wikipediaPageContentTool, wikipedia_random: wikipediaRandomPageTool, + google_vault_create_matters_export: createMattersExportTool, + google_vault_list_matters_export: listMattersExportTool, + google_vault_create_matters_holds: createMattersHoldsTool, + google_vault_list_matters_holds: listMattersHoldsTool, + google_vault_create_matters: createMattersTool, + google_vault_list_matters: listMattersTool, + google_vault_download_export_file: downloadExportFileTool, qdrant_fetch_points: qdrantFetchTool, qdrant_search_vector: qdrantSearchTool, qdrant_upsert_points: qdrantUpsertTool,