Feat(google vault): added google vault tool (#1459)

* first push pre testing

* toosl working

* progress

* bun run lint

* added doc

* changed google client ID and secret back

* cleaned up oauth

* removed comment

* removed any and added manual content

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
Adam Gough
2025-09-26 11:13:58 -07:00
committed by GitHub
parent 6daeb77740
commit 97257ca49f
18 changed files with 1194 additions and 1 deletions

View File

@@ -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"
<BlockInfoCard
type="google_vault"
color="#E8F0FE"
icon={true}
iconSvg={`<svg className="block-icon" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
<path
xmlns='http://www.w3.org/2000/svg'
d='M58.0251 41.1399L63.8516 36.7242L69.0032 30.157C67.372 26.032 63.097 18.4149 63.097 18.4149C63.097 18.4149 55.6204 14.618 51.9923 13.193L45.3454 17.918L41.0563 24.0492L36.7626 17.8336L30.2001 13.1133C26.5391 14.5195 19.0063 18.7805 19.0063 18.7805C19.0063 18.7805 14.6423 25.9852 12.9923 30.157L18.1532 36.7336L23.9048 41.0742L17.7969 45.7289L12.8798 52.0195C14.3329 55.807 18.3501 63.0352 18.3501 63.0352C18.3501 63.0352 25.8735 67.6289 29.9188 69.232L36.1016 64.4649L41.0001 58.1649L45.3454 63.9258L52.072 69.2367C55.8595 67.7414 63.0595 63.6305 63.0595 63.6305C63.0595 63.6305 67.4798 56.1961 69.036 52.2258L63.9782 45.7102L58.0251 41.1399ZM41.0048 53.4633C34.2501 53.4633 28.7704 47.9883 28.7704 41.2289C28.7704 34.4742 34.2454 28.9945 41.0048 28.9945C47.7641 28.9945 53.2391 34.4695 53.2391 41.2289C53.2391 47.9883 47.7595 53.4633 41.0048 53.4633Z'
fill='#1967D2'
/>
<path
xmlns='http://www.w3.org/2000/svg'
d='M58.025 41.1401L69.0078 30.1573C70.3672 33.5839 71.1172 37.3198 71.1172 41.2292C71.1172 45.1104 70.3766 48.8183 69.036 52.2261L58.025 41.1401ZM12.9969 30.162C11.6422 33.5886 10.8875 37.3198 10.8875 41.2292C10.8875 45.0354 11.6 48.6683 12.8891 52.0198L23.9094 41.0745L12.9969 30.162ZM51.9969 13.1933C48.5891 11.8573 44.886 11.1167 41.0047 11.1167C37.1985 11.1167 33.561 11.8292 30.2047 13.1183L41.061 24.0495L51.9969 13.1933ZM29.9328 69.2323C33.3594 70.5917 37.0953 71.3417 41.0047 71.3417C44.9141 71.3417 48.6453 70.587 52.0719 69.2323L41 58.1651L29.9328 69.2323ZM69.036 52.2261C65.9891 59.9839 59.8203 66.1667 52.0719 69.2323L62.811 79.9714C64.2828 81.4433 66.6641 81.4433 68.136 79.9714L79.761 68.3464C81.2282 66.8792 81.2328 64.5026 79.7703 63.0308L69.036 52.2261ZM69.0078 30.1573L79.9766 19.1886C81.4485 17.7167 81.4485 15.3354 79.9766 13.8636L68.3516 2.23857C66.8844 0.771387 64.5078 0.766699 63.036 2.2292L51.9922 13.1933C59.7547 16.2401 65.9422 22.4089 69.0078 30.1573ZM12.8891 52.0198L2.03284 62.8011C0.551587 64.2683 0.551587 66.6589 2.02346 68.1354L13.8641 79.9761C15.336 81.4479 17.7172 81.4479 19.1891 79.9761L29.9328 69.2323C22.1141 66.1386 15.9078 59.8761 12.8891 52.0198ZM30.2047 13.1183L19.1985 2.03232C17.7313 0.551074 15.3406 0.551074 13.8641 2.02295L2.02346 13.8636C0.551587 15.3354 0.551587 17.7167 2.02346 19.1886L12.9969 30.162C16.0906 22.3479 22.3532 16.137 30.2047 13.1183Z'
fill='#4285F4'
/>
</svg>`}
/>
## 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`

View File

@@ -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<string, string> = { '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)

View File

@@ -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' },
},
}

View File

@@ -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<string, BlockConfig> = {
google_forms: GoogleFormsBlock,
google_search: GoogleSearchBlock,
google_sheets: GoogleSheetsBlock,
google_vault: GoogleVaultBlock,
huggingface: HuggingFaceBlock,
hunter: HunterBlock,
image_generator: ImageGeneratorBlock,

View File

@@ -3711,3 +3711,18 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
/>
</svg>
)
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
<path
xmlns='http://www.w3.org/2000/svg'
d='M58.0251 41.1399L63.8516 36.7242L69.0032 30.157C67.372 26.032 63.097 18.4149 63.097 18.4149C63.097 18.4149 55.6204 14.618 51.9923 13.193L45.3454 17.918L41.0563 24.0492L36.7626 17.8336L30.2001 13.1133C26.5391 14.5195 19.0063 18.7805 19.0063 18.7805C19.0063 18.7805 14.6423 25.9852 12.9923 30.157L18.1532 36.7336L23.9048 41.0742L17.7969 45.7289L12.8798 52.0195C14.3329 55.807 18.3501 63.0352 18.3501 63.0352C18.3501 63.0352 25.8735 67.6289 29.9188 69.232L36.1016 64.4649L41.0001 58.1649L45.3454 63.9258L52.072 69.2367C55.8595 67.7414 63.0595 63.6305 63.0595 63.6305C63.0595 63.6305 67.4798 56.1961 69.036 52.2258L63.9782 45.7102L58.0251 41.1399ZM41.0048 53.4633C34.2501 53.4633 28.7704 47.9883 28.7704 41.2289C28.7704 34.4742 34.2454 28.9945 41.0048 28.9945C47.7641 28.9945 53.2391 34.4695 53.2391 41.2289C53.2391 47.9883 47.7595 53.4633 41.0048 53.4633Z'
fill='#1967D2'
/>
<path
xmlns='http://www.w3.org/2000/svg'
d='M58.025 41.1401L69.0078 30.1573C70.3672 33.5839 71.1172 37.3198 71.1172 41.2292C71.1172 45.1104 70.3766 48.8183 69.036 52.2261L58.025 41.1401ZM12.9969 30.162C11.6422 33.5886 10.8875 37.3198 10.8875 41.2292C10.8875 45.0354 11.6 48.6683 12.8891 52.0198L23.9094 41.0745L12.9969 30.162ZM51.9969 13.1933C48.5891 11.8573 44.886 11.1167 41.0047 11.1167C37.1985 11.1167 33.561 11.8292 30.2047 13.1183L41.061 24.0495L51.9969 13.1933ZM29.9328 69.2323C33.3594 70.5917 37.0953 71.3417 41.0047 71.3417C44.9141 71.3417 48.6453 70.587 52.0719 69.2323L41 58.1651L29.9328 69.2323ZM69.036 52.2261C65.9891 59.9839 59.8203 66.1667 52.0719 69.2323L62.811 79.9714C64.2828 81.4433 66.6641 81.4433 68.136 79.9714L79.761 68.3464C81.2282 66.8792 81.2328 64.5026 79.7703 63.0308L69.036 52.2261ZM69.0078 30.1573L79.9766 19.1886C81.4485 17.7167 81.4485 15.3354 79.9766 13.8636L68.3516 2.23857C66.8844 0.771387 64.5078 0.766699 63.036 2.2292L51.9922 13.1933C59.7547 16.2401 65.9422 22.4089 69.0078 30.1573ZM12.8891 52.0198L2.03284 62.8011C0.551587 64.2683 0.551587 66.6589 2.02346 68.1354L13.8641 79.9761C15.336 81.4479 17.7172 81.4479 19.1891 79.9761L29.9328 69.2323C22.1141 66.1386 15.9078 59.8761 12.8891 52.0198ZM30.2047 13.1183L19.1985 2.03232C17.7313 0.551074 15.3406 0.551074 13.8641 2.02295L2.02346 13.8636C0.551587 15.3354 0.551587 17.7167 2.02346 19.1886L12.9969 30.162C16.0906 22.3479 22.3532 16.137 30.2047 13.1183Z'
fill='#4285F4'
/>
</svg>
)

View File

@@ -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,

View File

@@ -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<string, OAuthProviderConfig> = {
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}`)

View File

@@ -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<GoogleVaultCreateMattersParams> = {
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 }
},
}

View File

@@ -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<GoogleVaultCreateMattersExportParams> = {
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 }
},
}

View File

@@ -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<GoogleVaultCreateMattersHoldsParams> = {
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 }
},
}

View File

@@ -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<DownloadParams> = {
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' },
},
}

View File

@@ -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'

View File

@@ -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<GoogleVaultListMattersParams> = {
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 }
},
}

View File

@@ -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<GoogleVaultListMattersExportParams> = {
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 }
},
}

View File

@@ -0,0 +1,52 @@
import type { GoogleVaultListMattersHoldsParams } from '@/tools/google_vault/types'
import type { ToolConfig } from '@/tools/types'
export const listMattersHoldsTool: ToolConfig<GoogleVaultListMattersHoldsParams> = {
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 }
},
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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<string, ToolConfig> = {
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,