S3 Compatible Cloud Storage client for JavaScript/TypeScript
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
This document covers presigned URL generation and POST policy creation for secure, time-limited access to MinIO/S3 objects without exposing credentials to clients.
Presigned URLs allow clients to access objects directly without requiring AWS credentials. These URLs are signed with your credentials and have an expiration time.
const url = await client.presignedUrl(method, bucketName, objectName, expires?, reqParams?, requestDate?)
// Parameters
method: string // HTTP method ('GET', 'PUT', 'POST', 'DELETE')
bucketName: string // Bucket name
objectName: string // Object name/key
expires?: number // Expiry time in seconds (default: 604800 = 7 days)
reqParams?: PreSignRequestParams // Request parameters and headers
requestDate?: Date // Request date (default: current time)
// Returns: Promise<string> - Presigned URLinterface PreSignRequestParams {
// Query parameters to include in URL
[key: string]: string | undefined
// Common parameters
'response-content-type'?: string // Override Content-Type in response
'response-content-language'?: string // Override Content-Language in response
'response-expires'?: string // Override Expires in response
'response-cache-control'?: string // Override Cache-Control in response
'response-content-disposition'?: string // Override Content-Disposition in response
'response-content-encoding'?: string // Override Content-Encoding in response
// Version selection
'versionId'?: string // Specific version ID
}const url = await client.presignedGetObject(bucketName, objectName, expires?, respHeaders?, requestDate?)
// Parameters
bucketName: string // Bucket name
objectName: string // Object name/key
expires?: number // Expiry time in seconds (max: 604800)
respHeaders?: PreSignRequestParams // Response headers to override
requestDate?: Date // Request date (default: current time)
// Returns: Promise<string> - Presigned GET URLconst url = await client.presignedPutObject(bucketName, objectName, expires?)
// Parameters
bucketName: string // Bucket name
objectName: string // Object name/key
expires?: number // Expiry time in seconds (max: 604800)
// Returns: Promise<string> - Presigned PUT URL// Generate presigned GET URL (1 hour expiry)
const downloadUrl = await client.presignedGetObject('my-bucket', 'photo.jpg', 3600)
console.log('Download URL:', downloadUrl)
// Client can now download directly: fetch(downloadUrl)
// Generate presigned PUT URL (30 minutes expiry)
const uploadUrl = await client.presignedPutObject('my-bucket', 'upload.pdf', 1800)
console.log('Upload URL:', uploadUrl)
// Client can now upload: fetch(uploadUrl, { method: 'PUT', body: file })
// Presigned GET with custom response headers
const customUrl = await client.presignedGetObject('my-bucket', 'document.pdf', 3600, {
'response-content-type': 'application/octet-stream',
'response-content-disposition': 'attachment; filename="custom-name.pdf"'
})
// Downloaded file will have custom headers
// Presigned URL for specific version
const versionUrl = await client.presignedGetObject('versioned-bucket', 'file.txt', 3600, {
'versionId': 'version-123'
})
// Generic presigned URL for DELETE operation
const deleteUrl = await client.presignedUrl('DELETE', 'my-bucket', 'temp-file.txt', 600)
// Client can delete: fetch(deleteUrl, { method: 'DELETE' })
// Presigned URL with custom request date
const customDateUrl = await client.presignedUrl(
'GET',
'my-bucket',
'file.txt',
3600,
{},
new Date('2023-12-25T00:00:00Z') // Custom signing date
)POST policies allow secure, direct browser uploads to S3 with fine-grained control over upload conditions and metadata.
import { PostPolicy } from 'minio'
class PostPolicy {
// Properties
policy: {
conditions: (string | number)[][] // Policy conditions array
expiration?: string // Policy expiration timestamp
}
formData: Record<string, string> // Form data for POST request
// Methods
setExpires(date: Date): void
setKey(objectName: string): void
setKeyStartsWith(prefix: string): void
setBucket(bucketName: string): void
setContentType(type: string): void
setContentTypeStartsWith(prefix: string): void
setContentDisposition(value: string): void
setContentLengthRange(min: number, max: number): void
setUserMetaData(metaData: ObjectMetaData): void
}Creates a new PostPolicy instance for defining upload conditions and generating presigned POST policies.
const postPolicy = client.newPostPolicy()
// Returns: PostPolicy instance// Create new post policy
const postPolicy = client.newPostPolicy()
// Set policy conditions
postPolicy.setBucket('my-bucket')
postPolicy.setKey('uploads/file.jpg')
postPolicy.setExpires(new Date(Date.now() + 24 * 60 * 60 * 1000)) // 24 hours
postPolicy.setContentType('image/jpeg')
postPolicy.setContentLengthRange(1024, 5 * 1024 * 1024) // 1KB to 5MB
// Generate presigned policy
const presignedPostPolicy = await client.presignedPostPolicy(postPolicy)
// Alternative: Using PostPolicy constructor directly
import { PostPolicy } from 'minio'
const altPolicy = new PostPolicy()
altPolicy.setBucket('my-bucket')
// ... set conditionsconst result = await client.presignedPostPolicy(postPolicy)
// Parameters
postPolicy: PostPolicy // POST policy configuration
// Returns: Promise<PostPolicyResult>interface PostPolicyResult {
postURL: string // URL to POST to
formData: Record<string, string> // Form fields to include in POST
}import { PostPolicy } from 'minio'
// Basic POST policy for browser uploads
const policy = new PostPolicy()
// Set policy expiration (required)
const expires = new Date()
expires.setMinutes(expires.getMinutes() + 10) // 10 minutes from now
policy.setExpires(expires)
// Set bucket and object constraints
policy.setBucket('upload-bucket')
policy.setKey('user-uploads/photo.jpg') // Exact key
// OR
policy.setKeyStartsWith('user-uploads/') // Key prefix
// Set content type constraints
policy.setContentType('image/jpeg') // Exact type
// OR
policy.setContentTypeStartsWith('image/') // Type prefix
// Set file size limits (1KB to 10MB)
policy.setContentLengthRange(1024, 10 * 1024 * 1024)
// Add custom metadata
policy.setUserMetaData({
'uploaded-by': 'web-client',
'upload-session': 'abc123'
})
// Generate presigned POST policy
const { postURL, formData } = await client.presignedPostPolicy(policy)
console.log('POST URL:', postURL)
console.log('Form data:', formData)
// Advanced POST policy with multiple conditions
const advancedPolicy = new PostPolicy()
// Set expiration (1 hour)
const expiry = new Date()
expiry.setHours(expiry.getHours() + 1)
advancedPolicy.setExpires(expiry)
// Multiple conditions
advancedPolicy.setBucket('secure-uploads')
advancedPolicy.setKeyStartsWith('documents/') // Only allow documents/ prefix
advancedPolicy.setContentTypeStartsWith('application/') // Only application/* types
advancedPolicy.setContentLengthRange(100, 50 * 1024 * 1024) // 100B to 50MB
advancedPolicy.setContentDisposition('attachment') // Force download
// Custom metadata for tracking
advancedPolicy.setUserMetaData({
'department': 'legal',
'classification': 'internal',
'retention-years': '7'
})
const advancedResult = await client.presignedPostPolicy(advancedPolicy)<!-- HTML form for browser upload using POST policy -->
<!DOCTYPE html>
<html>
<head>
<title>Direct S3 Upload</title>
</head>
<body>
<form id="upload-form" method="POST" enctype="multipart/form-data">
<!-- Form fields from formData -->
<input type="hidden" name="key" value="">
<input type="hidden" name="policy" value="">
<input type="hidden" name="x-amz-algorithm" value="">
<input type="hidden" name="x-amz-credential" value="">
<input type="hidden" name="x-amz-date" value="">
<input type="hidden" name="x-amz-signature" value="">
<!-- File input -->
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<script>
// JavaScript to populate form and handle upload
async function setupUpload() {
// Get presigned POST policy from your backend
const response = await fetch('/api/presigned-post-policy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bucket: 'upload-bucket',
keyPrefix: 'user-uploads/',
maxSize: 10 * 1024 * 1024
})
})
const { postURL, formData } = await response.json()
// Set form action to POST URL
const form = document.getElementById('upload-form')
form.action = postURL
// Populate hidden fields
Object.entries(formData).forEach(([name, value]) => {
const input = form.querySelector(`input[name="${name}"]`)
if (input) input.value = value
})
// Handle form submission
form.addEventListener('submit', async (e) => {
e.preventDefault()
const formData = new FormData(form)
const file = formData.get('file')
if (!file) {
alert('Please select a file')
return
}
try {
const uploadResponse = await fetch(postURL, {
method: 'POST',
body: formData
})
if (uploadResponse.ok) {
alert('Upload successful!')
} else {
alert('Upload failed: ' + uploadResponse.statusText)
}
} catch (error) {
alert('Upload error: ' + error.message)
}
})
}
setupUpload()
</script>
</body>
</html>// Presigned PUT URL that enforces specific headers
const putUrl = await client.presignedUrl('PUT', 'my-bucket', 'strict-upload.pdf', 3600, {
// These parameters will be included in the signature
'x-amz-server-side-encryption': 'AES256',
'x-amz-meta-uploader': 'api-service'
})
// Client must include these exact headers when making the PUT request
// fetch(putUrl, {
// method: 'PUT',
// body: file,
// headers: {
// 'x-amz-server-side-encryption': 'AES256',
// 'x-amz-meta-uploader': 'api-service'
// }
// })// Generate multiple presigned URLs efficiently
async function generateBatchUrls(bucketName, objectNames, expires = 3600) {
const urls = await Promise.all(
objectNames.map(objectName =>
client.presignedGetObject(bucketName, objectName, expires)
)
)
return objectNames.reduce((acc, objectName, index) => {
acc[objectName] = urls[index]
return acc
}, {})
}
const objectNames = ['file1.pdf', 'file2.jpg', 'file3.txt']
const urlMap = await generateBatchUrls('my-bucket', objectNames)
// Usage
Object.entries(urlMap).forEach(([objectName, url]) => {
console.log(`${objectName}: ${url}`)
})// Create time-limited upload slots for multiple files
async function createUploadSlots(bucketName, fileNames, validMinutes = 30) {
const policy = new PostPolicy()
// Set expiration
const expires = new Date()
expires.setMinutes(expires.getMinutes() + validMinutes)
policy.setExpires(expires)
// Configure bucket and key prefix
policy.setBucket(bucketName)
policy.setKeyStartsWith('batch-uploads/')
// Allow various content types
policy.setContentLengthRange(1, 100 * 1024 * 1024) // 1B to 100MB
// Generate URLs for each file
const uploadSlots = []
for (const fileName of fileNames) {
// Create separate policy for each file for specific key
const filePolicy = new PostPolicy()
filePolicy.setExpires(expires)
filePolicy.setBucket(bucketName)
filePolicy.setKey(`batch-uploads/${Date.now()}-${fileName}`)
filePolicy.setContentLengthRange(1, 100 * 1024 * 1024)
const result = await client.presignedPostPolicy(filePolicy)
uploadSlots.push({
fileName,
...result
})
}
return uploadSlots
}
const files = ['document1.pdf', 'image1.jpg', 'data1.csv']
const uploadSlots = await createUploadSlots('batch-bucket', files, 15)
uploadSlots.forEach(slot => {
console.log(`Upload slot for ${slot.fileName}:`)
console.log(` URL: ${slot.postURL}`)
console.log(` Form data:`, slot.formData)
})import { PRESIGN_EXPIRY_DAYS_MAX } from 'minio'
console.log('Maximum expiry:', PRESIGN_EXPIRY_DAYS_MAX, 'seconds') // 604800 (7 days)
// This will throw an error
try {
await client.presignedGetObject('bucket', 'object', PRESIGN_EXPIRY_DAYS_MAX + 1)
} catch (error) {
console.error('Expiry too long:', error.message)
}
// Safe expiry times
const oneHour = 3600
const oneDay = 24 * 3600
const oneWeek = 7 * 24 * 3600 // Maximum allowed
const url = await client.presignedGetObject('bucket', 'object', oneWeek)// Strict POST policy with multiple security constraints
const securePolicy = new PostPolicy()
// Short expiration for security
const shortExpiry = new Date()
shortExpiry.setMinutes(shortExpiry.getMinutes() + 5) // 5 minutes only
securePolicy.setExpires(shortExpiry)
// Restrict to specific bucket and path
securePolicy.setBucket('secure-uploads')
securePolicy.setKeyStartsWith('verified-users/')
// Strict content type validation
securePolicy.setContentType('image/jpeg') // Only JPEG images
// Strict size limits
securePolicy.setContentLengthRange(1024, 2 * 1024 * 1024) // 1KB to 2MB
// Add security metadata
securePolicy.setUserMetaData({
'security-scan': 'pending',
'upload-source': 'verified-client'
})
const secureResult = await client.presignedPostPolicy(securePolicy)import {
S3Error,
ExpiresParamError,
InvalidArgumentError
} from 'minio'
try {
// Invalid expiry (too long)
await client.presignedGetObject('bucket', 'object', 10 * 24 * 3600) // 10 days
} catch (error) {
if (error instanceof ExpiresParamError) {
console.error('Invalid expiry time:', error.message)
}
}
try {
// Invalid bucket name
await client.presignedPutObject('invalid..bucket', 'object.txt')
} catch (error) {
if (error instanceof InvalidArgumentError) {
console.error('Invalid bucket name:', error.message)
}
}
// Handle POST policy errors
try {
const policy = new PostPolicy()
// Missing required expiration
await client.presignedPostPolicy(policy)
} catch (error) {
console.error('Policy error:', error.message)
}Next: Notifications - Learn about event notification system and polling