chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3CopyObjectTool: ToolConfig = {
|
||||
id: 's3_copy_object',
|
||||
name: 'S3 Copy Object',
|
||||
description: 'Copy an object within or between AWS S3 buckets',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
sourceBucket: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source bucket name (e.g., my-bucket)',
|
||||
},
|
||||
sourceKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source object key/path (e.g., folder/file.txt)',
|
||||
},
|
||||
destinationBucket: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Destination bucket name (e.g., my-other-bucket)',
|
||||
},
|
||||
destinationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Destination object key/path (e.g., backup/file.txt)',
|
||||
},
|
||||
acl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Access control list for the copied object (e.g., private, public-read)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/copy-object',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
sourceBucket: params.sourceBucket,
|
||||
sourceKey: params.sourceKey,
|
||||
destinationBucket: params.destinationBucket,
|
||||
destinationKey: params.destinationKey,
|
||||
acl: params.acl,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
url: '',
|
||||
metadata: {
|
||||
error: data.error || 'Failed to copy object',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
url: data.output.url,
|
||||
uri: data.output.uri,
|
||||
metadata: {
|
||||
copySourceVersionId: data.output.copySourceVersionId,
|
||||
versionId: data.output.versionId,
|
||||
etag: data.output.etag,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the copied S3 object',
|
||||
},
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'S3 URI of the copied object (s3://bucket/key)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Copy operation metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3CreateBucketTool: ToolConfig = {
|
||||
id: 's3_create_bucket',
|
||||
name: 'S3 Create Bucket',
|
||||
description: 'Create a new AWS S3 bucket in the specified region',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region to create the bucket in (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name for the new S3 bucket (must be globally unique)',
|
||||
},
|
||||
acl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Canned ACL for the bucket (e.g., private, public-read)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/create-bucket',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
acl: params.acl,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
metadata: {
|
||||
error: data.error || 'Failed to create bucket',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metadata: {
|
||||
bucket: data.output.bucket,
|
||||
location: data.output.location ?? null,
|
||||
bucketArn: data.output.bucketArn ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Created bucket metadata including name and location',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3DeleteBucketTool: ToolConfig = {
|
||||
id: 's3_delete_bucket',
|
||||
name: 'S3 Delete Bucket',
|
||||
description: 'Delete an empty 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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region where the bucket is located (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the S3 bucket to delete (must be empty)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/delete-bucket',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
deleted: false,
|
||||
metadata: {
|
||||
error: data.error || 'Failed to delete bucket',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
metadata: {
|
||||
bucket: data.output.bucket,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the bucket was successfully deleted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Deletion metadata including bucket name',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3DeleteObjectTool: ToolConfig = {
|
||||
id: 's3_delete_object',
|
||||
name: 'S3 Delete Object',
|
||||
description: 'Delete 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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
objectKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Object key/path to delete (e.g., folder/file.txt)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/delete-object',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
objectKey: params.objectKey,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
deleted: false,
|
||||
metadata: {
|
||||
error: data.error || 'Failed to delete object',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
metadata: {
|
||||
key: data.output.key,
|
||||
deleteMarker: data.output.deleteMarker,
|
||||
versionId: data.output.versionId,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the object was successfully deleted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Deletion metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3DeleteObjectsTool: ToolConfig = {
|
||||
id: 's3_delete_objects',
|
||||
name: 'S3 Delete Objects',
|
||||
description: 'Delete multiple objects from an AWS S3 bucket in a single batch request',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
keys: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of object keys to delete (e.g., ["a.txt", "folder/b.txt"]). Max 1000.',
|
||||
},
|
||||
quiet: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return only deletion errors, omitting successfully deleted keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/delete-objects',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
keys: params.keys,
|
||||
quiet: params.quiet,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
deleted: [],
|
||||
errors: [],
|
||||
metadata: {
|
||||
error: data.error || 'Failed to delete objects',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: data.output.deleted || [],
|
||||
errors: data.output.errors || [],
|
||||
metadata: {
|
||||
deletedCount: (data.output.deleted || []).length,
|
||||
errorCount: (data.output.errors || []).length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'array',
|
||||
description: 'Objects that were successfully deleted',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Deleted object key' },
|
||||
versionId: { type: 'string', description: 'Version ID of the deleted object' },
|
||||
deleteMarker: { type: 'boolean', description: 'Whether a delete marker was created' },
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
type: 'array',
|
||||
description: 'Objects that failed to delete',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Object key that failed' },
|
||||
code: { type: 'string', description: 'Error code' },
|
||||
message: { type: 'string', description: 'Error message' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Batch deletion summary including counts',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3HeadObjectTool: ToolConfig = {
|
||||
id: 's3_head_object',
|
||||
name: 'S3 Head Object',
|
||||
description: 'Retrieve metadata for an S3 object without downloading its body',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
objectKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Object key/path to inspect (e.g., folder/file.txt)',
|
||||
},
|
||||
versionId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Specific object version ID to inspect (for versioned buckets)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/head-object',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
objectKey: params.objectKey,
|
||||
versionId: params.versionId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
exists: false,
|
||||
metadata: {
|
||||
error: data.error || 'Failed to retrieve object metadata',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
exists: data.output.exists,
|
||||
metadata: {
|
||||
size: data.output.contentLength ?? null,
|
||||
fileType: data.output.contentType ?? null,
|
||||
etag: data.output.etag ?? null,
|
||||
lastModified: data.output.lastModified ?? null,
|
||||
versionId: data.output.versionId ?? null,
|
||||
storageClass: data.output.storageClass ?? null,
|
||||
serverSideEncryption: data.output.serverSideEncryption ?? null,
|
||||
deleteMarker: data.output.deleteMarker ?? null,
|
||||
userMetadata: data.output.metadata ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
exists: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the object exists and was reachable',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Object metadata including size, content type, ETag, and last modified date',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { s3CopyObjectTool } from '@/tools/s3/copy_object'
|
||||
import { s3CreateBucketTool } from '@/tools/s3/create_bucket'
|
||||
import { s3DeleteBucketTool } from '@/tools/s3/delete_bucket'
|
||||
import { s3DeleteObjectTool } from '@/tools/s3/delete_object'
|
||||
import { s3DeleteObjectsTool } from '@/tools/s3/delete_objects'
|
||||
import { s3GetObjectTool } from '@/tools/s3/get_object'
|
||||
import { s3HeadObjectTool } from '@/tools/s3/head_object'
|
||||
import { s3ListBucketsTool } from '@/tools/s3/list_buckets'
|
||||
import { s3ListObjectsTool } from '@/tools/s3/list_objects'
|
||||
import { s3PresignedUrlTool } from '@/tools/s3/presigned_url'
|
||||
import { s3PutObjectTool } from '@/tools/s3/put_object'
|
||||
|
||||
export {
|
||||
s3GetObjectTool,
|
||||
s3PutObjectTool,
|
||||
s3ListObjectsTool,
|
||||
s3DeleteObjectTool,
|
||||
s3CopyObjectTool,
|
||||
s3ListBucketsTool,
|
||||
s3HeadObjectTool,
|
||||
s3CreateBucketTool,
|
||||
s3DeleteBucketTool,
|
||||
s3PresignedUrlTool,
|
||||
s3DeleteObjectsTool,
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3ListBucketsTool: ToolConfig = {
|
||||
id: 's3_list_buckets',
|
||||
name: 'S3 List Buckets',
|
||||
description: 'List the S3 buckets owned by the authenticated AWS account',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region to address the request to (e.g., us-east-1)',
|
||||
},
|
||||
prefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Limit the response to bucket names that begin with this prefix',
|
||||
},
|
||||
maxBuckets: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of buckets to return (1-10000)',
|
||||
},
|
||||
continuationToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Token for pagination from a previous list buckets response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/list-buckets',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
prefix: params.prefix,
|
||||
maxBuckets: params.maxBuckets !== undefined ? Number(params.maxBuckets) : undefined,
|
||||
continuationToken: params.continuationToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
buckets: [],
|
||||
metadata: {
|
||||
error: data.error || 'Failed to list buckets',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
buckets: data.output.buckets || [],
|
||||
metadata: {
|
||||
owner: data.output.owner ?? null,
|
||||
continuationToken: data.output.continuationToken ?? null,
|
||||
prefix: data.output.prefix ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
buckets: {
|
||||
type: 'array',
|
||||
description: 'List of S3 buckets owned by the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Bucket name' },
|
||||
creationDate: { type: 'string', description: 'Bucket creation timestamp' },
|
||||
region: { type: 'string', description: 'AWS region where the bucket is located' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Listing metadata including owner and pagination info',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3ListObjectsTool: ToolConfig = {
|
||||
id: 's3_list_objects',
|
||||
name: 'S3 List Objects',
|
||||
description: 'List objects in 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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
prefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Prefix to filter objects (e.g., folder/, images/2024/)',
|
||||
},
|
||||
maxKeys: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of objects to return (default: 1000)',
|
||||
},
|
||||
continuationToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Token for pagination from previous list response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/list-objects',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
prefix: params.prefix,
|
||||
maxKeys: params.maxKeys !== undefined ? Number(params.maxKeys) : undefined,
|
||||
continuationToken: params.continuationToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
objects: [],
|
||||
metadata: {
|
||||
error: data.error || 'Failed to list objects',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objects: data.output.objects || [],
|
||||
metadata: {
|
||||
isTruncated: data.output.isTruncated,
|
||||
nextContinuationToken: data.output.nextContinuationToken,
|
||||
keyCount: data.output.keyCount,
|
||||
prefix: data.output.prefix,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
objects: {
|
||||
type: 'array',
|
||||
description: 'List of S3 objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Object key' },
|
||||
size: { type: 'number', description: 'Object size in bytes' },
|
||||
lastModified: { type: 'string', description: 'Last modified timestamp' },
|
||||
etag: { type: 'string', description: 'Entity tag' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Listing metadata including pagination info',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3PresignedUrlTool: ToolConfig = {
|
||||
id: 's3_presigned_url',
|
||||
name: 'S3 Presigned URL',
|
||||
description: 'Generate a time-limited presigned URL to download or upload an S3 object',
|
||||
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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region where the bucket is located (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
objectKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Object key/path for the presigned URL (e.g., folder/file.txt)',
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Operation the URL grants: get (download) or put (upload)',
|
||||
},
|
||||
expiresIn: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL validity in seconds (1-604800, default 3600)',
|
||||
},
|
||||
contentType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content-Type the upload must use (only applies to put URLs)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/presigned-url',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
objectKey: params.objectKey,
|
||||
method: params.method,
|
||||
expiresIn: params.expiresIn !== undefined ? Number(params.expiresIn) : 3600,
|
||||
contentType: params.contentType,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
url: '',
|
||||
metadata: {
|
||||
error: data.error || 'Failed to generate presigned URL',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
url: data.output.url,
|
||||
metadata: {
|
||||
method: data.output.method,
|
||||
expiresIn: data.output.expiresIn,
|
||||
expiresAt: data.output.expiresAt,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'The generated presigned URL',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Presigned URL metadata including method and expiration',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const s3PutObjectTool: ToolConfig = {
|
||||
id: 's3_put_object',
|
||||
name: 'S3 Put Object',
|
||||
description: 'Upload a file to 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: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
bucketName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'S3 bucket name (e.g., my-bucket)',
|
||||
},
|
||||
objectKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Object key/path in S3 (e.g., folder/filename.ext)',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'File to upload',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Text content to upload (alternative to file)',
|
||||
},
|
||||
contentType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Content-Type header (auto-detected from file if not provided)',
|
||||
},
|
||||
acl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Access control list (e.g., private, public-read)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/s3/put-object',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region: params.region,
|
||||
bucketName: params.bucketName,
|
||||
objectKey: params.objectKey,
|
||||
file: params.file,
|
||||
content: params.content,
|
||||
contentType: params.contentType,
|
||||
acl: params.acl,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
url: '',
|
||||
metadata: {
|
||||
error: data.error || 'Failed to upload object',
|
||||
},
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
url: data.output.url,
|
||||
uri: data.output.uri,
|
||||
metadata: {
|
||||
etag: data.output.etag,
|
||||
location: data.output.location,
|
||||
key: data.output.key,
|
||||
bucket: data.output.bucket,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the uploaded S3 object',
|
||||
},
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'S3 URI of the uploaded object (s3://bucket/key)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Upload metadata including ETag and location',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface S3Response extends ToolResponse {
|
||||
output: {
|
||||
url?: string
|
||||
uri?: string
|
||||
file?: UserFile
|
||||
objects?: Array<{
|
||||
key: string
|
||||
size: number
|
||||
lastModified: string
|
||||
etag: string
|
||||
}>
|
||||
buckets?: Array<{
|
||||
name: string
|
||||
creationDate: string | null
|
||||
region: string | null
|
||||
}>
|
||||
deleted?:
|
||||
| boolean
|
||||
| Array<{ key: string | null; versionId: string | null; deleteMarker: boolean | null }>
|
||||
errors?: Array<{ key: string | null; code: string | null; message: string | null }>
|
||||
exists?: boolean
|
||||
metadata: {
|
||||
fileType?: string | null
|
||||
size?: number | null
|
||||
name?: string
|
||||
lastModified?: string | null
|
||||
etag?: string | null
|
||||
location?: string | null
|
||||
key?: string
|
||||
bucket?: string
|
||||
isTruncated?: boolean
|
||||
nextContinuationToken?: string | null
|
||||
keyCount?: number
|
||||
prefix?: string | null
|
||||
deleteMarker?: boolean | null
|
||||
versionId?: string | null
|
||||
copySourceVersionId?: string
|
||||
storageClass?: string | null
|
||||
serverSideEncryption?: string | null
|
||||
userMetadata?: Record<string, string>
|
||||
owner?: { displayName: string | null; id: string | null } | null
|
||||
continuationToken?: string | null
|
||||
bucketArn?: string | null
|
||||
method?: string
|
||||
expiresIn?: number
|
||||
expiresAt?: string
|
||||
deletedCount?: number
|
||||
errorCount?: number
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function encodeS3PathComponent(pathComponent: string): string {
|
||||
return encodeURIComponent(pathComponent).replace(/%2F/g, '/')
|
||||
}
|
||||
|
||||
export function getSignatureKey(
|
||||
key: string,
|
||||
dateStamp: string,
|
||||
regionName: string,
|
||||
serviceName: string
|
||||
): Buffer {
|
||||
if (!key || typeof key !== 'string') {
|
||||
throw new Error('Invalid key provided to getSignatureKey')
|
||||
}
|
||||
const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest()
|
||||
const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest()
|
||||
const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest()
|
||||
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
||||
return kSigning
|
||||
}
|
||||
|
||||
export function parseS3Uri(
|
||||
s3Uri: string,
|
||||
fallbackRegion?: string
|
||||
): {
|
||||
bucketName: string
|
||||
region: string
|
||||
objectKey: string
|
||||
} {
|
||||
try {
|
||||
const url = new URL(s3Uri)
|
||||
const hostname = url.hostname
|
||||
const normalizedPath = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname
|
||||
|
||||
const virtualHostedDualstackMatch = hostname.match(
|
||||
/^(.+)\.s3\.dualstack\.([^.]+)\.amazonaws\.com(?:\.cn)?$/
|
||||
)
|
||||
const virtualHostedRegionalMatch = hostname.match(
|
||||
/^(.+)\.s3[.-]([^.]+)\.amazonaws\.com(?:\.cn)?$/
|
||||
)
|
||||
const virtualHostedGlobalMatch = hostname.match(/^(.+)\.s3\.amazonaws\.com(?:\.cn)?$/)
|
||||
|
||||
const pathStyleDualstackMatch = hostname.match(
|
||||
/^s3\.dualstack\.([^.]+)\.amazonaws\.com(?:\.cn)?$/
|
||||
)
|
||||
const pathStyleRegionalMatch = hostname.match(/^s3[.-]([^.]+)\.amazonaws\.com(?:\.cn)?$/)
|
||||
const pathStyleGlobalMatch = hostname.match(/^s3\.amazonaws\.com(?:\.cn)?$/)
|
||||
|
||||
const isPathStyleHost = Boolean(
|
||||
pathStyleDualstackMatch || pathStyleRegionalMatch || pathStyleGlobalMatch
|
||||
)
|
||||
|
||||
const firstSlashIndex = normalizedPath.indexOf('/')
|
||||
const pathStyleBucketName =
|
||||
firstSlashIndex === -1 ? normalizedPath : normalizedPath.slice(0, firstSlashIndex)
|
||||
const pathStyleObjectKey =
|
||||
firstSlashIndex === -1 ? '' : normalizedPath.slice(firstSlashIndex + 1)
|
||||
|
||||
const bucketName = isPathStyleHost
|
||||
? pathStyleBucketName
|
||||
: (virtualHostedDualstackMatch?.[1] ??
|
||||
virtualHostedRegionalMatch?.[1] ??
|
||||
virtualHostedGlobalMatch?.[1] ??
|
||||
'')
|
||||
|
||||
const rawObjectKey = isPathStyleHost ? pathStyleObjectKey : normalizedPath
|
||||
const objectKey = (() => {
|
||||
try {
|
||||
return decodeURIComponent(rawObjectKey)
|
||||
} catch {
|
||||
return rawObjectKey
|
||||
}
|
||||
})()
|
||||
|
||||
const normalizedFallbackRegion = fallbackRegion?.trim()
|
||||
const regionFromHost =
|
||||
virtualHostedDualstackMatch?.[2] ??
|
||||
virtualHostedRegionalMatch?.[2] ??
|
||||
pathStyleDualstackMatch?.[1] ??
|
||||
pathStyleRegionalMatch?.[1]
|
||||
const region = regionFromHost || normalizedFallbackRegion || 'us-east-1'
|
||||
|
||||
if (!bucketName || !objectKey) {
|
||||
throw new Error('Invalid S3 URI format')
|
||||
}
|
||||
|
||||
return { bucketName, region, objectKey }
|
||||
} catch (_error) {
|
||||
throw new Error(
|
||||
'Invalid S3 Object URL format. Expected S3 virtual-hosted or path-style URL with object key.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePresignedUrl(params: any, expiresIn = 3600): string {
|
||||
const date = new Date()
|
||||
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
||||
const dateStamp = amzDate.slice(0, 8)
|
||||
const encodedPath = encodeS3PathComponent(params.objectKey)
|
||||
|
||||
const _expires = Math.floor(Date.now() / 1000) + expiresIn
|
||||
|
||||
const method = 'GET'
|
||||
const canonicalUri = `/${encodedPath}`
|
||||
const canonicalQueryString = `X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${encodeURIComponent(`${params.accessKeyId}/${dateStamp}/${params.region}/s3/aws4_request`)}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host`
|
||||
const canonicalHeaders = `host:${params.bucketName}.s3.${params.region}.amazonaws.com\n`
|
||||
const signedHeaders = 'host'
|
||||
const payloadHash = 'UNSIGNED-PAYLOAD'
|
||||
|
||||
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')
|
||||
|
||||
return `https://${params.bucketName}.s3.${params.region}.amazonaws.com/${encodedPath}?${canonicalQueryString}&X-Amz-Signature=${signature}`
|
||||
}
|
||||
Reference in New Issue
Block a user