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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+122
View File
@@ -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',
},
},
}
+90
View File
@@ -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',
},
},
}
+87
View File
@@ -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',
},
},
}
+96
View File
@@ -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',
},
},
}
+124
View File
@@ -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',
},
},
}
+169
View File
@@ -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',
},
},
}
+109
View File
@@ -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',
},
},
}
+25
View File
@@ -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,
}
+111
View File
@@ -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',
},
},
}
+120
View File
@@ -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',
},
},
}
+117
View File
@@ -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',
},
},
}
+130
View File
@@ -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',
},
},
}
+55
View File
@@ -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
}
}
}
+121
View File
@@ -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}`
}