npm package: @ircg/ios
Description: Image Optimization Service SDK for IRCG
Author: Isar Ramses
Latest version: 1.23.7

# @ircg/ios

Image Optimization Service (IOS) SDK for IRCG.

## Installation

```bash
pnpm add @ircg/ios
```

## Usage

```typescript
import { IOSClient } from '@ircg/ios'

// Initialize the client
const ios = new IOSClient({
	apiKey: 'your-api-key',
	baseUrl: 'https://ircg.dev', // optional, defaults to https://ircg.dev
})

// Upload an image (browser)
const fileInput = document.querySelector('input[type="file"]')
const file = fileInput.files[0]

const { optimizedImage, error } = await ios.upload({
	image: file,
	requireSignedURLs: false,
	fields: ['imageId', 'name', 'requireSignedURLs', 'createdAt', 'requests', 'currentVariants'],
})

if (error) {
	console.error('Upload failed:', error.message)
	return
}

console.log(optimizedImage.imageId)
console.log(optimizedImage.currentVariants)

// Get image URL
const imageUrl = ios.getImageUrl(optimizedImage.imageId, 'thumbnail')

// Upload with extended response (includes base64 placeholder)
const { optimizedImage: extendedImage, error: extError } = await ios.upload({
	image: file,
	requireSignedURLs: false,
	fields: ['imageId', 'name', 'requireSignedURLs', 'createdAt', 'requests', 'currentVariants', 'placeholderBase64'],
})

if (!extError) {
	console.log(extendedImage.placeholderBase64)
}

// Get all images
const images = await ios.getAll({
	page: 1,
	amount: 50,
	ascending: false,
})

console.log(images.optimizedImages)
console.log(`Total: ${images.totalAmount}`)

// Get a specific image by ID
const { optimizedImage: image, error: getError } = await ios.getById({
	imageId: 'image-id-123',
})
if (!getError) {
	console.log(image)
}

// Delete an image
const { error: deleteError } = await ios.delete({ imageId: 'image-id-123' })
if (deleteError) {
	console.error('Delete failed:', deleteError.message)
}

// Sign an image variant (for images with requireSignedURLs: true)
// Minimal response (default)
const {
	optimizedImage: minimalSig,
	error: sigError,
	reusedSignature,
} = await ios.signImage({
	imageId: 'image-id-123',
	variant: 'thumbnail',
	reuseSignature: true,
})
if (!sigError) {
	console.log(minimalSig.sig)
	console.log(minimalSig.exp)
	console.log(reusedSignature)
}

// Request specific fields - TypeScript infers the correct type!
const { optimizedImage: completeSig, error: compError } = await ios.signImage({
	imageId: 'image-id-123',
	variant: 'thumbnail',
	fields: ['imageId', 'sig', 'exp', 'signedUrl', 'requests'],
})

// No type guard needed - TypeScript knows these properties exist
if (!compError) {
	console.log(completeSig.imageId)
	console.log(completeSig.signedUrl)
	console.log(completeSig.requests)
}

// All methods return errors by default (no try-catch needed!)
const { optimizedImage, error } = await ios.signImage({
	imageId: 'image-id-123',
	variant: 'thumbnail',
	fields: ['sig', 'exp', 'signedUrl'],
})

if (error) {
	// Handle error without try-catch
	console.error(error.message)
	return
}

// TypeScript knows optimizedImage exists here
console.log(optimizedImage.signedUrl)
console.log(optimizedImage.requests)

// Delete example - ignore 404 errors
const deleteResult = await ios.delete(uploadImageValue.imageId)
if (deleteResult.error && deleteResult.error.status !== 404) {
	console.error(`Can't delete image: ${deleteResult.error.message}`)
}
```

## API Reference

### `IOSClient`

#### Constructor

```typescript
new IOSClient(config: IRCGClientConfig)
```

- `config.apiKey` (required): Your IRCG API key
- `config.baseUrl` (optional): Base URL for the API (default: 'https://ircg.dev')

#### Methods

##### `upload(data)` ⭐ Recommended

Upload and optimize an image. Returns errors instead of throwing exceptions.

- `data.image` (required): File or Blob object
- `data.requireSignedURLs` (optional): Whether to require signed URLs (default: false)
- `data.fields` (required): Array of fields to include in the response
- `data.lang` (optional): Language for error messages ('es' | 'en')

Returns: `Promise<{ optimizedImage?, error? }>` where one is defined and the other is undefined

**TypeScript automatically infers the correct return type based on the `fields` parameter!**

```typescript
// Request specific fields - TypeScript knows optimizedImage has placeholderBase64
const { optimizedImage, error } = await ios.upload({
	image: file,
	fields: ['imageId', 'placeholderBase64', 'currentVariants'],
})

if (!error) {
	console.log(optimizedImage.placeholderBase64) // ✅ Available
	console.log(optimizedImage.imageId) // ✅ Available
	console.log(optimizedImage.currentVariants) // ✅ Available
}

// Request only specific fields
const result = await ios.upload({
	image: file,
	fields: ['imageId', 'currentVariants'],
})
if (!result.error) {
	console.log(result.optimizedImage.currentVariants) // ✅ Available
	// result.optimizedImage.placeholderBase64 // ❌ TypeScript error - not requested
}

// Request minimal fields
const minimal = await ios.upload({
	image: file,
	fields: ['imageId'],
})
if (!minimal.error) {
	console.log(minimal.optimizedImage.imageId) // ✅ Available
	// minimal.optimizedImage.currentVariants // ❌ TypeScript error - not requested
}
```

**Alternative:** Use `uploadUnsafe()` if you prefer exceptions.

##### `getAll(options)` ⭐ Recommended

Get all optimized images with pagination. Returns errors instead of throwing exceptions.

- `options.page` (optional): Page number (default: 1)
- `options.amount` (optional): Items per page (default: 50)
- `options.ascending` (optional): Sort order (default: false)
- `options.fields` (required): Array of fields to include in the response
- `options.lang` (optional): Language for error messages ('es' | 'en')

Returns: `Promise<{ optimizedImages?, totalAmount?, error? }>` where error is defined on failure

**Alternative:** Use `getAllUnsafe()` if you prefer exceptions.

##### `getById(options)` ⭐ Recommended

Get an optimized image by its ID. Returns errors instead of throwing exceptions.

- `options.imageId` (required): The image ID
- `options.fields` (required): Array of fields to include in the response
- `options.lang` (optional): Language for error messages ('es' | 'en')

Returns: `Promise<{ optimizedImage?, error? }>` where one is defined and the other is undefined

**Alternative:** Use `getByIdUnsafe()` if you prefer exceptions.

##### `delete(imageId)` ⭐ Recommended

Delete an optimized image. Returns errors instead of throwing exceptions.

- `imageId` (required): The image ID to delete

Returns: `Promise<{ success?, error? }>` where one is defined and the other is undefined

**Alternative:** Use `deleteUnsafe()` if you prefer exceptions.

##### `signImage(data)` ⭐ Recommended

Sign an image variant to generate a signed URL (required for images with `requireSignedURLs: true`). Returns errors instead of throwing exceptions.

- `data.imageId` (required): The image ID
- `data.variant` (required): The variant name (e.g., 'thumbnail', 'medium', 'large')
- `data.reuseSignature` (optional): If true, the API will try to reuse a recently cached signed URL for the same `imageId` + `variant`.
- `data.fields` (required): Array of fields to include in the response
- `data.lang` (optional): Language for error messages ('es' | 'en')

Returns: `Promise<{ optimizedImage?, error? }>` where one is defined and the other is undefined

The response may include `reusedSignature?: boolean` at the top-level. When `true`, the signed URL was served from the server-side cache.

**Alternative:** Use `signImageUnsafe()` if you prefer exceptions.

**TypeScript automatically infers the correct return type based on the `fields` parameter!**

```typescript
// Request minimal fields
const { optimizedImage, error, reusedSignature } = await ios.signImage({
	imageId: 'abc',
	variant: 'thumb',
	reuseSignature: true,
	fields: ['sig', 'exp'],
})

if (!error) {
	optimizedImage.sig // ✅ Available
	optimizedImage.exp // ✅ Available
	// optimizedImage.signedUrl;  // ❌ TypeScript error - not requested
}

if (!error) {
	console.log('Was signature reused?', reusedSignature === true)
}

// Request all fields - TypeScript knows it has all fields
const result = await ios.signImage({
	imageId: 'abc',
	variant: 'thumb',
	reuseSignature: true,
	fields: ['sig', 'exp', 'imageId', 'signedUrl', 'requests'],
})

if (!result.error) {
	result.optimizedImage.sig // ✅ Available
	result.optimizedImage.exp // ✅ Available
	result.optimizedImage.signedUrl // ✅ Available
	result.optimizedImage.imageId // ✅ Available
	result.optimizedImage.requests // ✅ Available
}
```

**Available fields:**

- `sig`: The signature string
- `exp`: Expiration timestamp (as string)
- `imageId`: The image ID
- `signedUrl`: The complete signed URL
- `requests`: Number of times the image has been accessed

##### `signImageUnsafe(data)`

**Unsafe version that throws exceptions instead of returning errors.**

- `data.imageId` (required): The image ID
- `data.variant` (required): The variant name (e.g., 'thumbnail', 'medium', 'large')
- `data.reuseSignature` (optional): If true, the API will try to reuse a recently cached signed URL for the same `imageId` + `variant`.
- `data.fields` (required): Array of fields to include in the response
- `data.lang` (optional): Language for error messages ('es' | 'en')

Returns: `Promise<{ optimizedImage: Pick<SignImage, T[number]> }>`

The response may include `reusedSignature?: boolean` at the top-level. When `true`, the signed URL was served from the server-side cache.

Throws: `IRCGAuthenticationError`, `IRCGValidationError`, `IRCGRateLimitError`, `IRCGServerError`, or `IRCGError`

```typescript
import { IRCGAuthenticationError, IRCGValidationError } from '@ircg/core'

try {
	const result = await ios.signImageUnsafe(
		{
			imageId: 'abc',
			variant: 'thumb',
		},
		'complete'
	)

	console.log(result.optimizedImage.signedUrl)
	console.log('Total requests:', result.optimizedImage.requests)
} catch (error) {
	if (error instanceof IRCGAuthenticationError) {
		console.error('Invalid API key')
	} else if (error instanceof IRCGValidationError) {
		console.error('Invalid data:', error.details)
	}
}
```

**When to use:**

- You prefer exception-based error handling
- You have existing try-catch infrastructure
- You want errors to bubble up automatically

##### `getImageUrl(imageId, variant?, signed?)`

Get the URL for a specific image variant.

- `imageId` (required): The image ID
- `variant` (optional): Variant name (default: 'public')
- `signed` (optional): Signature parameters `{ exp: number, sig: string }`

Returns: `string`

## Return Types

- **minimal**: Returns only `imageId` and `requireSignedURLs`
- **complete**: Includes name, createdAt, and available variants
- **extended**: Includes everything from complete plus a base64 placeholder

## Error Handling

**All methods return errors by default** - no try-catch needed! This makes error handling explicit and type-safe.

### Default Behavior (Returns Errors)

```typescript
import { IOSClient } from '@ircg/ios'

const ios = new IOSClient({ apiKey: 'your-api-key' })

// Example 1: Basic error handling
const { optimizedImage, error } = await ios.signImage(
	{
		imageId: 'abc123',
		variant: 'thumbnail',
	},
	'complete'
)

if (error) {
	console.error('Failed to sign image:', error.message)
	console.error('Status:', error.status)
	console.error('Type:', error.type)
	return // or throw, or handle differently
}

// TypeScript knows optimizedImage is defined here
console.log('Signed URL:', optimizedImage.signedUrl)
console.log('Requests:', optimizedImage.requests)

// Example 2: Handling specific error types
const result = await ios.signImage(
	{
		imageId: 'xyz789',
		variant: 'large',
	},
	'complete'
)

if (result.error) {
	switch (result.error.type) {
		case 'authentication':
			return fail(401, { message: 'Invalid API key' })
		case 'validation':
			return fail(400, { message: result.error.message, details: result.error.details })
		case 'rate_limit':
			return fail(429, { message: 'Too many requests' })
		case 'server':
			return fail(500, { message: 'Server error' })
		default:
			return fail(500, { message: result.error.message })
	}
}

// Continue with success case
const signedUrl = result.optimizedImage.signedUrl

// Example 3: In a SvelteKit form action
export const actions = {
	sign_image: async ({ request }) => {
		const formData = await request.formData()
		const imageId = formData.get('imageId') as string
		const variant = formData.get('variant') as string

		const { optimizedImage, error } = await ios.signImage({ imageId, variant }, 'complete')

		if (error) {
			return fail(error.status || 500, {
				message: error.message,
				details: error.details,
			})
		}

		return {
			success: true,
			signedUrl: optimizedImage.signedUrl,
		}
	},
}
```

// Example 4: Delete with error handling (ignore 404)
const deleteResult = await ios.delete(imageId);
if (deleteResult.error && deleteResult.error.status !== 404) {
console.error('Failed to delete:', deleteResult.error.message);
}

````

### Alternative: Using `*Unsafe()` Methods (Throws Exceptions)

If you prefer exceptions, use the `*Unsafe()` variants (e.g., `uploadUnsafe()`, `getAllUnsafe()`, `getByIdUnsafe()`, `deleteUnsafe()`, `signImageUnsafe()`). All unsafe methods throw typed errors.

### Available Error Types

- **`IRCGAuthenticationError`** (401/403): Invalid or missing API key
- **`IRCGValidationError`** (400): Invalid request data (includes `details` object)
- **`IRCGRateLimitError`** (429): Too many requests
- **`IRCGServerError`** (500/502/503): Server-side errors
- **`IRCGError`**: Base error class for other HTTP errors

### Example with Unsafe Methods

```typescript
import { IOSClient } from '@ircg/ios';
import {
  IRCGError,
  IRCGAuthenticationError,
  IRCGValidationError,
  IRCGRateLimitError,
  IRCGServerError
} from '@ircg/core';

const ios = new IOSClient({ apiKey: 'your-api-key' });

try {
  // Using the unsafe variant - throws on error
  const result = await ios.signImageUnsafe({
    imageId: 'abc123',
    variant: 'thumbnail'
  }, 'complete');

  console.log(result.optimizedImage.signedUrl);
} catch (error) {
  if (error instanceof IRCGAuthenticationError) {
    console.error('Invalid API key:', error.message);
  } else if (error instanceof IRCGValidationError) {
    console.error('Validation error:', error.message);
    console.error('Details:', error.details); // Field-specific errors
  } else if (error instanceof IRCGRateLimitError) {
    console.error('Rate limit exceeded:', error.message);
  } else if (error instanceof IRCGServerError) {
    console.error('Server error:', error.message);
  } else if (error instanceof IRCGError) {
    console.error('IRCG error:', error.message, error.status);
  } else {
    console.error('Unknown error:', error);
  }
}
````

### Error Properties

All IRCG errors include:

- `message`: Human-readable error message
- `status`: HTTP status code (optional)
- `details`: Additional error information (optional, mainly in validation errors)

## Practical Examples

### Example 1: Sign Multiple Images

```typescript
const imageIds = ['img1', 'img2', 'img3']
const results = await Promise.all(
	imageIds.map((imageId) => ios.signImage({ imageId, variant: 'thumbnail' }, 'complete'))
)

// Filter successful results
const successful = results.filter((r) => !r.error)
const failed = results.filter((r) => r.error)

console.log(`Signed ${successful.length} images`)
console.log(`Failed ${failed.length} images`)

// Use successful results
successful.forEach(({ optimizedImage }) => {
	console.log(optimizedImage.signedUrl)
})
```

### Example 2: Retry Logic

```typescript
async function signImageWithRetry(imageId: string, variant: string, maxRetries = 3) {
	for (let attempt = 1; attempt <= maxRetries; attempt++) {
		const { optimizedImage, error } = await ios.signImage({ imageId, variant }, 'complete')

		if (!error) {
			return { success: true, data: optimizedImage }
		}

		// Don't retry on authentication or validation errors
		if (error.type === 'authentication' || error.type === 'validation') {
			return { success: false, error }
		}

		// Retry on server errors or rate limits
		if (attempt < maxRetries) {
			console.log(`Attempt ${attempt} failed, retrying...`)
			await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
		}
	}

	return { success: false, error: { message: 'Max retries exceeded' } }
}
```

### Example 3: Caching Signatures

```typescript
const signatureCache = new Map<string, { sig: string; exp: string; expiresAt: number }>()

async function getSignedUrl(imageId: string, variant: string): Promise<string | null> {
	const cacheKey = `${imageId}:${variant}`
	const cached = signatureCache.get(cacheKey)

	// Check if cached signature is still valid (with 5 min buffer)
	if (cached && Date.now() < cached.expiresAt - 5 * 60 * 1000) {
		return ios.getImageUrl(imageId, variant, {
			exp: parseInt(cached.exp),
			sig: cached.sig,
		})
	}

	// Get new signature
	const { optimizedImage, error } = await ios.signImage({ imageId, variant }, 'complete')

	if (error) {
		console.error('Failed to sign image:', error.message)
		return null
	}

	// Cache the signature
	signatureCache.set(cacheKey, {
		sig: optimizedImage.sig,
		exp: optimizedImage.exp,
		expiresAt: parseInt(optimizedImage.exp) * 1000,
	})

	return optimizedImage.signedUrl
}
```

### Example 4: Type-Safe Error Handling

```typescript
function handleImageError(error: { message: string; status?: number; type?: string }) {
	const errorMessages = {
		authentication: 'Invalid API key. Please check your credentials.',
		validation: 'Invalid image ID or variant.',
		rate_limit: 'Too many requests. Please try again later.',
		server: 'Server error. Please try again.',
		unknown: 'An unexpected error occurred.',
	}

	const message = errorMessages[error.type as keyof typeof errorMessages] || errorMessages.unknown

	return {
		userMessage: message,
		technicalMessage: error.message,
		statusCode: error.status || 500,
	}
}

// Usage
const { optimizedImage, error } = await ios.signImage({ imageId: 'abc', variant: 'thumb' }, 'complete')

if (error) {
	const errorInfo = handleImageError(error)
	console.error(errorInfo.userMessage)
	console.error('Technical:', errorInfo.technicalMessage)
}
```

### Example 5: Integration with React/Vue

```typescript
// React Hook
function useSignedImage(imageId: string, variant: string) {
  const [signedUrl, setSignedUrl] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchSignedUrl() {
      setLoading(true);
      const { optimizedImage, error } = await ios.signImage(
        { imageId, variant },
        'complete'
      );

      if (error) {
        setError(error.message);
      } else {
        setSignedUrl(optimizedImage.signedUrl);
      }
      setLoading(false);
    }

    fetchSignedUrl();
  }, [imageId, variant]);

  return { signedUrl, loading, error };
}

// Usage in component
function ImageComponent({ imageId }: { imageId: string }) {
  const { signedUrl, loading, error } = useSignedImage(imageId, 'thumbnail');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return <img src={signedUrl} alt="Optimized image" />;
}
```

## License

MIT