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
@@ -0,0 +1,101 @@
import { CopyObjectCommand, type ObjectCannedACL, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3CopyObjectContract } from '@/lib/api/contracts/tools/aws/s3-copy-object'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3CopyObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated S3 copy object request via ${authResult.authType}`, {
userId: authResult.userId,
})
const parsed = await parseToolRequest(awsS3CopyObjectContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Copying S3 object`, {
source: `${validatedData.sourceBucket}/${validatedData.sourceKey}`,
destination: `${validatedData.destinationBucket}/${validatedData.destinationKey}`,
})
// Initialize S3 client
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
// Copy object (properly encode the source key for CopySource parameter)
const encodedSourceKey = validatedData.sourceKey.split('/').map(encodeURIComponent).join('/')
const copySource = `${validatedData.sourceBucket}/${encodedSourceKey}`
const copyCommand = new CopyObjectCommand({
Bucket: validatedData.destinationBucket,
Key: validatedData.destinationKey,
CopySource: copySource,
ACL: validatedData.acl as ObjectCannedACL | undefined,
})
const result = await s3Client.send(copyCommand)
logger.info(`[${requestId}] Object copied successfully`, {
source: copySource,
destination: `${validatedData.destinationBucket}/${validatedData.destinationKey}`,
etag: result.CopyObjectResult?.ETag,
})
// Generate public URL for destination (properly encode the destination key)
const encodedDestKey = validatedData.destinationKey.split('/').map(encodeURIComponent).join('/')
const url = `https://${validatedData.destinationBucket}.s3.${validatedData.region}.amazonaws.com/${encodedDestKey}`
const uri = `s3://${validatedData.destinationBucket}/${validatedData.destinationKey}`
return NextResponse.json({
success: true,
output: {
url,
uri,
copySourceVersionId: result.CopySourceVersionId,
versionId: result.VersionId,
etag: result.CopyObjectResult?.ETag,
},
})
} catch (error) {
logger.error(`[${requestId}] Error copying S3 object:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,99 @@
import {
type BucketCannedACL,
type BucketLocationConstraint,
CreateBucketCommand,
S3Client,
} from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3CreateBucketContract } from '@/lib/api/contracts/tools/aws/s3-create-bucket'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3CreateBucketAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 create bucket attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated S3 create bucket request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseToolRequest(awsS3CreateBucketContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Creating S3 bucket`, {
bucket: validatedData.bucketName,
region: validatedData.region,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const createCommand = new CreateBucketCommand({
Bucket: validatedData.bucketName,
ACL: (validatedData.acl as BucketCannedACL | undefined) || undefined,
CreateBucketConfiguration:
validatedData.region === 'us-east-1'
? undefined
: { LocationConstraint: validatedData.region as BucketLocationConstraint },
})
const result = await s3Client.send(createCommand)
logger.info(`[${requestId}] Bucket created successfully`, {
bucket: validatedData.bucketName,
location: result.Location,
})
return NextResponse.json({
success: true,
output: {
bucket: validatedData.bucketName,
location: result.Location ?? null,
bucketArn: result.BucketArn ?? null,
},
})
} catch (error) {
logger.error(`[${requestId}] Error creating S3 bucket:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,86 @@
import { DeleteBucketCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3DeleteBucketContract } from '@/lib/api/contracts/tools/aws/s3-delete-bucket'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3DeleteBucketAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 delete bucket attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated S3 delete bucket request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseToolRequest(awsS3DeleteBucketContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Deleting S3 bucket`, {
bucket: validatedData.bucketName,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const deleteCommand = new DeleteBucketCommand({
Bucket: validatedData.bucketName,
})
await s3Client.send(deleteCommand)
logger.info(`[${requestId}] Bucket deleted successfully`, {
bucket: validatedData.bucketName,
})
return NextResponse.json({
success: true,
output: {
deleted: true,
bucket: validatedData.bucketName,
},
})
} catch (error) {
logger.error(`[${requestId}] Error deleting S3 bucket:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,93 @@
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3DeleteObjectContract } from '@/lib/api/contracts/tools/aws/s3-delete-object'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3DeleteObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated S3 delete object request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseToolRequest(awsS3DeleteObjectContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Deleting S3 object`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
})
// Initialize S3 client
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
// Delete object
const deleteCommand = new DeleteObjectCommand({
Bucket: validatedData.bucketName,
Key: validatedData.objectKey,
})
const result = await s3Client.send(deleteCommand)
logger.info(`[${requestId}] Object deleted successfully`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
deleteMarker: result.DeleteMarker,
})
return NextResponse.json({
success: true,
output: {
key: validatedData.objectKey,
deleteMarker: result.DeleteMarker,
versionId: result.VersionId,
},
})
} catch (error) {
logger.error(`[${requestId}] Error deleting S3 object:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,105 @@
import { DeleteObjectsCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3DeleteObjectsContract } from '@/lib/api/contracts/tools/aws/s3-delete-objects'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3DeleteObjectsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 delete objects attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated S3 delete objects request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseToolRequest(awsS3DeleteObjectsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Deleting S3 objects`, {
bucket: validatedData.bucketName,
count: validatedData.keys.length,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const deleteCommand = new DeleteObjectsCommand({
Bucket: validatedData.bucketName,
Delete: {
Objects: validatedData.keys.map((key) => ({ Key: key })),
Quiet: validatedData.quiet ?? false,
},
})
const result = await s3Client.send(deleteCommand)
const deleted = (result.Deleted || []).map((obj) => ({
key: obj.Key ?? null,
versionId: obj.VersionId ?? null,
deleteMarker: obj.DeleteMarker ?? null,
}))
const errors = (result.Errors || []).map((err) => ({
key: err.Key ?? null,
code: err.Code ?? null,
message: err.Message ?? null,
}))
logger.info(`[${requestId}] Delete objects completed`, {
bucket: validatedData.bucketName,
deleted: deleted.length,
errors: errors.length,
})
return NextResponse.json({
success: true,
output: {
deleted,
errors,
},
})
} catch (error) {
logger.error(`[${requestId}] Error deleting S3 objects:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,114 @@
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3HeadObjectContract } from '@/lib/api/contracts/tools/aws/s3-head-object'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3HeadObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 head object attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated S3 head object request via ${authResult.authType}`, {
userId: authResult.userId,
})
const parsed = await parseToolRequest(awsS3HeadObjectContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Fetching S3 object metadata`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const headCommand = new HeadObjectCommand({
Bucket: validatedData.bucketName,
Key: validatedData.objectKey,
VersionId: validatedData.versionId || undefined,
})
const result = await s3Client.send(headCommand)
logger.info(`[${requestId}] Object metadata retrieved`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
})
return NextResponse.json({
success: true,
output: {
exists: true,
contentLength: result.ContentLength ?? null,
contentType: result.ContentType ?? null,
etag: result.ETag ?? null,
lastModified: result.LastModified?.toISOString() ?? null,
versionId: result.VersionId ?? null,
storageClass: result.StorageClass ?? null,
serverSideEncryption: result.ServerSideEncryption ?? null,
deleteMarker: result.DeleteMarker ?? null,
metadata: result.Metadata ?? {},
},
})
} catch (error) {
const metadata = error as { name?: string; $metadata?: { httpStatusCode?: number } }
if (metadata?.name === 'NotFound' || metadata?.$metadata?.httpStatusCode === 404) {
return NextResponse.json({
success: true,
output: {
exists: false,
contentLength: null,
contentType: null,
etag: null,
lastModified: null,
versionId: null,
storageClass: null,
serverSideEncryption: null,
deleteMarker: null,
metadata: {},
},
})
}
logger.error(`[${requestId}] Error fetching S3 object metadata:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { ListBucketsCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3ListBucketsContract } from '@/lib/api/contracts/tools/aws/s3-list-buckets'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3ListBucketsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 list buckets attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated S3 list buckets request via ${authResult.authType}`, {
userId: authResult.userId,
})
const parsed = await parseToolRequest(awsS3ListBucketsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Listing S3 buckets`, {
prefix: validatedData.prefix || '(none)',
maxBuckets: validatedData.maxBuckets || '(all)',
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const listCommand = new ListBucketsCommand({
Prefix: validatedData.prefix || undefined,
MaxBuckets: validatedData.maxBuckets || undefined,
ContinuationToken: validatedData.continuationToken || undefined,
})
const result = await s3Client.send(listCommand)
const buckets = (result.Buckets || []).map((bucket) => ({
name: bucket.Name || '',
creationDate: bucket.CreationDate?.toISOString() ?? null,
region: bucket.BucketRegion ?? null,
}))
logger.info(`[${requestId}] Listed ${buckets.length} buckets`)
return NextResponse.json({
success: true,
output: {
buckets,
owner: result.Owner
? {
displayName: result.Owner.DisplayName ?? null,
id: result.Owner.ID ?? null,
}
: null,
continuationToken: result.ContinuationToken ?? null,
prefix: result.Prefix ?? null,
},
})
} catch (error) {
logger.error(`[${requestId}] Error listing S3 buckets:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,101 @@
import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3ListObjectsContract } from '@/lib/api/contracts/tools/aws/s3-list-objects'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3ListObjectsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated S3 list objects request via ${authResult.authType}`, {
userId: authResult.userId,
})
const parsed = await parseToolRequest(awsS3ListObjectsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Listing S3 objects`, {
bucket: validatedData.bucketName,
prefix: validatedData.prefix || '(none)',
maxKeys: validatedData.maxKeys || 1000,
})
// Initialize S3 client
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
// List objects
const listCommand = new ListObjectsV2Command({
Bucket: validatedData.bucketName,
Prefix: validatedData.prefix || undefined,
MaxKeys: validatedData.maxKeys || undefined,
ContinuationToken: validatedData.continuationToken || undefined,
})
const result = await s3Client.send(listCommand)
const objects = (result.Contents || []).map((obj) => ({
key: obj.Key || '',
size: obj.Size || 0,
lastModified: obj.LastModified?.toISOString() || '',
etag: obj.ETag || '',
}))
logger.info(`[${requestId}] Listed ${objects.length} objects`, {
bucket: validatedData.bucketName,
isTruncated: result.IsTruncated,
})
return NextResponse.json({
success: true,
output: {
objects,
isTruncated: result.IsTruncated,
nextContinuationToken: result.NextContinuationToken,
keyCount: result.KeyCount,
prefix: validatedData.prefix,
},
})
} catch (error) {
logger.error(`[${requestId}] Error listing S3 objects:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,105 @@
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3PresignedUrlContract } from '@/lib/api/contracts/tools/aws/s3-presigned-url'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3PresignedUrlAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 presigned URL attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated S3 presigned URL request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseToolRequest(awsS3PresignedUrlContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Generating S3 presigned URL`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
method: validatedData.method,
expiresIn: validatedData.expiresIn,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
const command =
validatedData.method === 'put'
? new PutObjectCommand({
Bucket: validatedData.bucketName,
Key: validatedData.objectKey,
ContentType: validatedData.contentType || undefined,
})
: new GetObjectCommand({
Bucket: validatedData.bucketName,
Key: validatedData.objectKey,
})
const url = await getSignedUrl(s3Client, command, {
expiresIn: validatedData.expiresIn,
})
const expiresAt = new Date(Date.now() + validatedData.expiresIn * 1000).toISOString()
logger.info(`[${requestId}] Presigned URL generated`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
})
return NextResponse.json({
success: true,
output: {
url,
method: validatedData.method,
expiresIn: validatedData.expiresIn,
expiresAt,
},
})
} catch (error) {
logger.error(`[${requestId}] Error generating S3 presigned URL:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,159 @@
import { type ObjectCannedACL, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsS3PutObjectContract } from '@/lib/api/contracts/tools/aws/s3-put-object'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
const logger = createLogger('S3PutObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated S3 put object request via ${authResult.authType}`, {
userId: authResult.userId,
})
const parsed = await parseToolRequest(awsS3PutObjectContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Uploading to S3`, {
bucket: validatedData.bucketName,
key: validatedData.objectKey,
hasFile: !!validatedData.file,
hasContent: !!validatedData.content,
})
const s3Client = new S3Client({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
let uploadBody: Buffer | string
let uploadContentType: string | undefined
if (validatedData.file) {
const rawFile = validatedData.file
logger.info(`[${requestId}] Processing file upload: ${rawFile.name}`)
let userFile
try {
userFile = processSingleFileToUserFile(rawFile, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Failed to process file'),
},
{ status: 400 }
)
}
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
let downloadedContentType = ''
try {
const result = await downloadServableFileFromStorage(userFile, requestId, logger)
uploadBody = result.buffer
downloadedContentType = result.contentType
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
return NextResponse.json(
{ success: false, error: getErrorMessage(error, 'Failed to download file') },
{ status: 500 }
)
}
uploadContentType =
validatedData.contentType ||
downloadedContentType ||
userFile.type ||
'application/octet-stream'
} else if (validatedData.content) {
uploadBody = Buffer.from(validatedData.content, 'utf-8')
uploadContentType = validatedData.contentType || 'text/plain'
} else {
return NextResponse.json(
{
success: false,
error: 'Either file or content must be provided',
},
{ status: 400 }
)
}
const putCommand = new PutObjectCommand({
Bucket: validatedData.bucketName,
Key: validatedData.objectKey,
Body: uploadBody,
ContentType: uploadContentType,
ACL: validatedData.acl as ObjectCannedACL | undefined,
})
const result = await s3Client.send(putCommand)
logger.info(`[${requestId}] File uploaded successfully`, {
etag: result.ETag,
bucket: validatedData.bucketName,
key: validatedData.objectKey,
})
const encodedKey = validatedData.objectKey.split('/').map(encodeURIComponent).join('/')
const url = `https://${validatedData.bucketName}.s3.${validatedData.region}.amazonaws.com/${encodedKey}`
const uri = `s3://${validatedData.bucketName}/${validatedData.objectKey}`
return NextResponse.json({
success: true,
output: {
url,
uri,
etag: result.ETag,
location: url,
key: validatedData.objectKey,
bucket: validatedData.bucketName,
},
})
} catch (error) {
logger.error(`[${requestId}] Error uploading to S3:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Internal server error'),
},
{ status: 500 }
)
}
})