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,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 }
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user