import crypto from 'crypto' import { getErrorMessage } from '@sim/utils/errors' import { encodeS3PathComponent, generatePresignedUrl, getSignatureKey, parseS3Uri, } from '@/tools/s3/utils' import type { ToolConfig } from '@/tools/types' export const s3GetObjectTool: ToolConfig = { id: 's3_get_object', name: 'S3 Get Object', description: 'Retrieve an object from an AWS S3 bucket', version: '1.0.0', params: { accessKeyId: { type: 'string', required: true, visibility: 'user-only', description: 'Your AWS Access Key ID', }, secretAccessKey: { type: 'string', required: true, visibility: 'user-only', description: 'Your AWS Secret Access Key', }, region: { type: 'string', required: false, visibility: 'user-only', description: 'Optional region override when URL does not include region (e.g., us-east-1, eu-west-1)', }, s3Uri: { type: 'string', required: true, visibility: 'user-or-llm', description: 'S3 Object URL (e.g., https://bucket.s3.region.amazonaws.com/path/to/file)', }, }, request: { url: (params) => { try { const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region params.objectKey = objectKey return `https://${bucketName}.s3.${region}.amazonaws.com/${encodeS3PathComponent(objectKey)}` } catch (_error) { throw new Error( 'Invalid S3 Object URL. Use a valid S3 URL and optionally provide region if the URL omits it.' ) } }, method: 'GET', headers: (params) => { try { // Parse S3 URI if not already parsed if (!params.bucketName || !params.region || !params.objectKey) { const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region params.objectKey = objectKey } // Use UTC time explicitly const date = new Date() const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '') const dateStamp = amzDate.slice(0, 8) const method = 'GET' const encodedPath = encodeS3PathComponent(params.objectKey) const canonicalUri = `/${encodedPath}` const canonicalQueryString = '' const payloadHash = crypto.createHash('sha256').update('').digest('hex') const host = `${params.bucketName}.s3.${params.region}.amazonaws.com` const canonicalHeaders = `host:${host}\n` + `x-amz-content-sha256:${payloadHash}\n` + `x-amz-date:${amzDate}\n` const signedHeaders = 'host;x-amz-content-sha256;x-amz-date' const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}` const algorithm = 'AWS4-HMAC-SHA256' const credentialScope = `${dateStamp}/${params.region}/s3/aws4_request` const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${crypto.createHash('sha256').update(canonicalRequest).digest('hex')}` const signingKey = getSignatureKey(params.secretAccessKey, dateStamp, params.region, 's3') const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex') const authorizationHeader = `${algorithm} Credential=${params.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}` return { Host: host, 'X-Amz-Content-Sha256': payloadHash, 'X-Amz-Date': amzDate, Authorization: authorizationHeader, } } catch (error) { const errorMessage = getErrorMessage(error, 'Unknown error') throw new Error(`Failed to generate request headers: ${errorMessage}`) } }, }, transformResponse: async (response: Response, params) => { // Parse S3 URI if not already parsed if (!params.bucketName || !params.region || !params.objectKey) { const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region params.objectKey = objectKey } if (!response.ok) { const errorText = await response.text() throw new Error( `Failed to download S3 object: ${response.status} ${response.statusText} ${errorText}` ) } const contentType = response.headers.get('content-type') || 'application/octet-stream' const lastModified = response.headers.get('last-modified') || new Date().toISOString() const fileName = params.objectKey.split('/').pop() || params.objectKey const arrayBuffer = await response.arrayBuffer() const buffer = Buffer.from(arrayBuffer) // Generate pre-signed URL for download const url = generatePresignedUrl(params, 3600) return { success: true, output: { url, file: { name: fileName, mimeType: contentType, data: buffer.toString('base64'), size: buffer.length, }, metadata: { fileType: contentType, size: buffer.length, name: fileName, lastModified: lastModified, }, }, } }, outputs: { url: { type: 'string', description: 'Pre-signed URL for downloading the S3 object', }, file: { type: 'file', description: 'Downloaded file stored in execution files', }, metadata: { type: 'object', description: 'File metadata including type, size, name, and last modified date', }, }, }