CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-cfkit--r2

High-level Cloudflare R2 storage API wrapper for generating pre-signed URLs and performing object operations

Overview
Eval results
Files

presigned-urls.mddocs/reference/

Pre-signed URLs

Pre-signed URLs provide secure, time-limited access to R2 objects without exposing credentials. This is the recommended approach for client-side file uploads and downloads.

Key Information for Agents

Core Capabilities:

  • Generate secure, time-limited URLs for client-side uploads without exposing credentials
  • Generate secure, time-limited URLs for client-side downloads without exposing credentials
  • Content type validation during URL generation (not enforced by R2)
  • Metadata attachment to pre-signed URLs (must be included in upload request)
  • Configurable expiration times for security
  • Wildcard content type patterns for flexible validation

Key Methods:

  • presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUrlResult> - Generate upload URL
  • presignedDownloadUrl(key: string, options?: PresignedDownloadUrlOptions): Promise<PresignedUrlResult> - Generate download URL

Key Interfaces:

  • PresignedUploadUrlOptions - key: string, contentType: string, metadata?, expiresIn?, maxFileSize?, allowedContentTypes?
  • PresignedDownloadUrlOptions - expiresIn?
  • PresignedUrlResult - url: string, key: string, contentType: string, expiresIn: number, metadata?

Default Behaviors:

  • Pre-signed upload URLs default to 86400 seconds (24 hours) expiration
  • Pre-signed download URLs default to 3600 seconds (1 hour) expiration
  • URLs are single-use by default (can be reused until expiration)
  • Content type validation only checks during URL generation (not enforced by R2)
  • Metadata must be included in upload request headers (exact match required)
  • URLs include AWS Signature V4 signature in query parameters
  • Expired URLs return 403 Forbidden when accessed

Threading Model:

  • URL generation is stateless and thread-safe
  • Multiple concurrent URL generations are safe
  • Generated URLs are independent (no shared state)
  • URL expiration is time-based (not dependent on generation time)

Lifecycle:

  • URLs are generated on-demand (not cached)
  • URLs remain valid until expiration time
  • Expired URLs cannot be used (403 Forbidden)
  • URLs can be reused multiple times until expiration
  • Metadata in URL must match upload request headers exactly

Common Patterns:

  • Client-side upload: Generate URL → Return to client → Client uploads via PUT → URL expires
  • Client-side download: Generate URL → Return to client → Client downloads via GET → URL expires
  • Content type validation: Use allowedContentTypes to restrict upload types
  • Metadata tracking: Include user ID, timestamp, or other metadata in URL
  • Short expiration: Use short expiration for sensitive files
  • Long expiration: Use long expiration for public assets

Integration Points:

  • Uses AWS Signature V4 signing (via aws4fetch)
  • S3-compatible pre-signed URL format
  • Requires CORS configuration for browser uploads
  • Metadata headers use x-amz-meta- prefix (S3-compatible)
  • Works with standard fetch API for client uploads/downloads

Critical Edge Cases:

  • Expired URL: Returns 403 Forbidden when used after expiration
  • Content type mismatch: Validation only on generation, R2 doesn't enforce
  • Metadata mismatch: Upload request must include exact metadata headers or fails
  • Invalid credentials: URL generation throws error
  • Non-existent object: Download URL works even if object doesn't exist (returns 404 when accessed)
  • Wildcard patterns: allowedContentTypes: ['image/*'] matches any image type
  • Multiple content types: allowedContentTypes array allows multiple patterns
  • URL reuse: Same URL can be used multiple times until expiration
  • Concurrent uploads: Multiple clients can use same URL simultaneously
  • Large files: No built-in chunking (upload entire file in one request)
  • Network failures: Client must handle retries (URL doesn't change)
  • CORS requirements: Browser uploads require CORS configuration on bucket
  • Metadata header format: Must use x-amz-meta- prefix in upload request headers
  • Exact header match: Metadata headers must match exactly (case-sensitive)

Exception Handling:

  • URL generation throws error on invalid credentials
  • Content type validation throws error if type not allowed
  • Network errors during generation throw generic fetch errors
  • Always wrap URL generation in try-catch blocks
  • Client-side URL usage returns HTTP status codes (403 for expired, 404 for not found)

Capabilities

Generate Pre-signed Upload URL

Creates a secure, time-limited URL that allows clients to upload files directly to R2 without exposing your credentials.

/**
 * Generate a pre-signed URL for uploading an object
 * @param options - Upload URL options including key, content type, and optional metadata
 * @returns Pre-signed URL and metadata
 */
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUrlResult>;

interface PresignedUploadUrlOptions {
  /** Object key (filename) in the bucket */
  key: string;
  /** Content type (MIME type) of the file */
  contentType: string;
  /** Optional metadata headers to include */
  metadata?: Record<string, string>;
  /** Expiration time in seconds (default: 86400 = 24 hours) */
  expiresIn?: number;
  /** Maximum allowed file size in bytes (validation only, not enforced by R2) */
  maxFileSize?: number;
  /** Allowed content types (supports wildcards like 'image/*') */
  allowedContentTypes?: string[];
}

interface PresignedUrlResult {
  /** The pre-signed URL */
  url: string;
  /** Object key */
  key: string;
  /** Content type */
  contentType: string;
  /** Expiration time in seconds */
  expiresIn: number;
  /** Metadata that was included in the signature */
  metadata?: Record<string, string>;
}

Usage Examples:

import { R2Client } from '@cfkit/r2';

const r2 = new R2Client({
  accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!
});

const bucket = r2.bucket('gallery');

// Basic pre-signed upload URL
const result = await bucket.presignedUploadUrl({
  key: 'photo.jpg',
  contentType: 'image/jpeg',
  expiresIn: 3600
});

// Client uploads using the pre-signed URL
await fetch(result.url, {
  method: 'PUT',
  headers: {
    'Content-Type': 'image/jpeg'
  },
  body: file
});

// Pre-signed URL with metadata
const resultWithMeta = await bucket.presignedUploadUrl({
  key: 'photo.jpg',
  contentType: 'image/jpeg',
  metadata: {
    'original-filename': 'vacation-photo.jpg',
    'uploaded-by': 'user-123'
  },
  expiresIn: 3600
});

// Client uploads with metadata headers
await fetch(resultWithMeta.url, {
  method: 'PUT',
  headers: {
    'Content-Type': 'image/jpeg',
    'x-amz-meta-original-filename': 'vacation-photo.jpg',
    'x-amz-meta-uploaded-by': 'user-123'
  },
  body: file
});

// Pre-signed URL with content type validation
try {
  const result = await bucket.presignedUploadUrl({
    key: 'document.pdf',
    contentType: 'application/pdf',
    allowedContentTypes: ['image/*']  // Only images allowed
  });
} catch (error) {
  console.error(error.message);
  // "Content type 'application/pdf' is not allowed. Allowed types: image/*"
}

Generate Pre-signed Download URL

Creates a secure, time-limited URL that allows clients to download files directly from R2 without exposing your credentials.

/**
 * Generate a pre-signed URL for downloading an object
 * @param key - Object key (filename)
 * @param options - Download URL options
 * @returns Pre-signed URL and metadata
 */
presignedDownloadUrl(key: string, options?: PresignedDownloadUrlOptions): Promise<PresignedUrlResult>;

interface PresignedDownloadUrlOptions {
  /** Expiration time in seconds (default: 3600 = 1 hour) */
  expiresIn?: number;
}

Usage Examples:

import { R2Client } from '@cfkit/r2';

const r2 = new R2Client({
  accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!
});

const bucket = r2.bucket('gallery');

// Basic pre-signed download URL
const result = await bucket.presignedDownloadUrl('photo.jpg', {
  expiresIn: 3600
});

console.log('Download URL:', result.url);
// Share this URL with users who need temporary access

// Check existence before generating download URL
const exists = await bucket.objectExists('photo.jpg');
if (exists) {
  const downloadUrl = await bucket.presignedDownloadUrl('photo.jpg');
  console.log('Download URL:', downloadUrl.url);
}

// Short expiration for sensitive files
const sensitiveUrl = await bucket.presignedDownloadUrl('private-doc.pdf', {
  expiresIn: 300  // 5 minutes
});

// Long expiration for public assets
const publicUrl = await bucket.presignedDownloadUrl('public-image.jpg', {
  expiresIn: 86400 * 7  // 7 days
});

Content Type Validation

The allowedContentTypes option in presignedUploadUrl supports wildcard patterns:

// Allow only images
allowedContentTypes: ['image/*']

// Allow specific types
allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif']

// Allow multiple type families
allowedContentTypes: ['image/*', 'video/*']

// Allow documents
allowedContentTypes: ['application/pdf', 'application/msword']

Validation Rules:

  • Validation occurs during URL generation (not when URL is used)
  • R2 does not enforce content type restrictions (validation is client-side only)
  • Wildcard patterns match any subtype (e.g., image/* matches image/jpeg, image/png)
  • Multiple patterns are OR logic (any match passes)
  • Exact match required for non-wildcard types

Usage Examples:

// Restrict to images only
try {
  const url = await bucket.presignedUploadUrl({
    key: 'photo.jpg',
    contentType: 'image/jpeg',
    allowedContentTypes: ['image/*']
  });
  // Success: image/jpeg matches image/*
} catch (error) {
  // Error if contentType doesn't match
}

// Multiple allowed types
const url = await bucket.presignedUploadUrl({
  key: 'file.jpg',
  contentType: 'image/jpeg',
  allowedContentTypes: ['image/*', 'video/*', 'application/pdf']
});

// Reject non-matching type
try {
  const url = await bucket.presignedUploadUrl({
    key: 'file.pdf',
    contentType: 'application/pdf',
    allowedContentTypes: ['image/*']  // PDF not allowed
  });
} catch (error) {
  console.error('Content type not allowed');
}

Metadata Headers

Metadata is stored with objects using x-amz-meta- prefixed headers. When using pre-signed URLs, ensure the client includes these headers exactly as specified:

// Server generates pre-signed URL with metadata
const result = await bucket.presignedUploadUrl({
  key: 'file.jpg',
  contentType: 'image/jpeg',
  metadata: {
    'original-filename': 'photo.jpg',
    'uploaded-by': 'user-123'
  }
});

// Client must include these headers when uploading
await fetch(result.url, {
  method: 'PUT',
  headers: {
    'Content-Type': 'image/jpeg',
    'x-amz-meta-original-filename': 'photo.jpg',
    'x-amz-meta-uploaded-by': 'user-123'
  },
  body: file
});

Metadata Rules:

  • Metadata keys in options are converted to x-amz-meta- prefixed headers
  • Client must include exact headers or upload fails
  • Metadata is case-sensitive (exact match required)
  • Metadata is stored with object and retrieved with getObject()
  • Metadata size limits apply (check Cloudflare R2 documentation)

Important Notes:

  • If metadata is specified in URL generation, it must be included in upload request
  • Missing or incorrect metadata headers cause upload to fail
  • Metadata can be retrieved later with getObject() or presignedDownloadUrl() doesn't include metadata

CORS Configuration

For direct client uploads using pre-signed URLs, configure your R2 bucket CORS policy. Cloudflare R2 requires explicit header names (wildcards are not supported):

{
  "rules": [
    {
      "allowed": {
        "methods": ["PUT", "GET", "POST", "DELETE"],
        "origins": ["https://yourdomain.com"],
        "headers": [
          "content-type",
          "x-amz-meta-original-filename",
          "x-amz-meta-uploaded-at",
          "x-amz-meta-uploaded-by"
        ]
      },
      "exposeHeaders": ["ETag"],
      "maxAgeSeconds": 3000
    }
  ]
}

CORS Configuration Requirements:

  • Must include PUT method for uploads
  • Must include GET method for downloads
  • Must list all metadata headers explicitly (no wildcards)
  • Must include content-type header
  • exposeHeaders can include ETag for upload verification
  • maxAgeSeconds controls browser CORS preflight cache

Common CORS Patterns:

// Allow all origins (development only)
{
  "rules": [
    {
      "allowed": {
        "methods": ["PUT", "GET"],
        "origins": ["*"],
        "headers": ["content-type", "x-amz-meta-*"]
      }
    }
  ]
}

// Production: specific origins
{
  "rules": [
    {
      "allowed": {
        "methods": ["PUT", "GET"],
        "origins": [
          "https://app.example.com",
          "https://www.example.com"
        ],
        "headers": [
          "content-type",
          "x-amz-meta-user-id",
          "x-amz-meta-timestamp"
        ]
      },
      "exposeHeaders": ["ETag"],
      "maxAgeSeconds": 3600
    }
  ]
}

Error Handling

URL generation may throw errors. Always wrap operations in try-catch blocks:

try {
  const url = await bucket.presignedUploadUrl({
    key: 'file.jpg',
    contentType: 'image/jpeg',
    expiresIn: 3600
  });
  console.log('URL generated:', url.url);
} catch (error) {
  if (error instanceof Error) {
    console.error('Failed to generate URL:', error.message);
  }
}

Error Handling Patterns:

// Content type validation error
try {
  const url = await bucket.presignedUploadUrl({
    key: 'file.pdf',
    contentType: 'application/pdf',
    allowedContentTypes: ['image/*']
  });
} catch (error) {
  if (error instanceof Error) {
    // Error: Content type 'application/pdf' is not allowed
    console.error('Validation failed:', error.message);
  }
}

// Authentication error
try {
  const url = await bucket.presignedDownloadUrl('photo.jpg');
} catch (error) {
  if (error instanceof Error) {
    // Error: Invalid credentials
    console.error('Authentication failed:', error.message);
  }
}

// Client-side URL usage errors
// When client uses expired URL:
fetch(expiredUrl, { method: 'PUT', body: file })
  .then(response => {
    if (response.status === 403) {
      console.error('URL expired');
    } else if (response.status === 404) {
      console.error('Object not found');
    }
  });

Common Error Scenarios:

  • Invalid credentials: URL generation throws error
  • Content type validation: Throws error if type not in allowedContentTypes
  • Network failures: Throws generic fetch error
  • Expired URL: Client receives 403 Forbidden
  • Non-existent object: Download URL returns 404 when accessed
  • Metadata mismatch: Upload fails if headers don't match

Install with Tessl CLI

npx tessl i tessl/npm-cfkit--r2

docs

index.md

tile.json