{"_id":"@alexwhitmore/storage-helpers-cli","_rev":"2-d9ddecde99d8b764d122ea181ecf04f3","name":"@alexwhitmore/storage-helpers-cli","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@alexwhitmore/storage-helpers-cli","version":"1.0.0","author":{"name":"Alex Whitmore"},"license":"MIT","_id":"@alexwhitmore/storage-helpers-cli@1.0.0","maintainers":[{"name":"alexwhitmore","email":"heyimalexw@gmail.com"}],"homepage":"https://github.com/alexwhitmore/storage-helpers#readme","bugs":{"url":"https://github.com/alexwhitmore/storage-helpers/issues"},"bin":{"storage-helpers":"dist/index.js"},"dist":{"shasum":"186fe8975c272bb1d2db66c91af0799a37c24a31","tarball":"https://registry.npmjs.org/@alexwhitmore/storage-helpers-cli/-/storage-helpers-cli-1.0.0.tgz","fileCount":3,"integrity":"sha512-c6Mh/FXAhJ7cdhgOPoTrYoBempY7A/4LRhfkOvBfI0dDtc6y1j2IUZHM2ggsUbtca/53WppiPmWWZUISzd1pUA==","signatures":[{"sig":"MEUCIQCM/jFSzNexamvhmbjxtqXXZKQmSCB3jp8Iy/9gZ4NJxAIgQD5vOr0Zi21qHPd9n2cCKwiG553uC6WuPPiUdnoO2EY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":15746},"engines":{"node":">=18.0.0"},"gitHead":"68eae2752649c7ba2c6be3da38eb391ab1132886","scripts":{"build":"tsup src/index.ts --format cjs --dts"},"_npmUser":{"name":"alexwhitmore","email":"heyimalexw@gmail.com"},"repository":{"url":"git+https://github.com/alexwhitmore/storage-helpers.git","type":"git"},"_npmVersion":"10.9.2","description":"CLI tools for @alexwhitmore/storage-helpers","directories":{},"_nodeVersion":"22.16.0","dependencies":{"chalk":"^5.3.0","commander":"^12.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.0.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/storage-helpers-cli_1.0.0_1766425962652_0.5942974995690202","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@alexwhitmore/storage-helpers-cli","version":"1.0.1","description":"CLI tools for @alexwhitmore/storage-helpers","bin":{"storage-helpers":"dist/index.js"},"scripts":{"build":"tsup src/index.ts --format cjs --dts"},"dependencies":{"chalk":"^5.3.0","commander":"^12.0.0"},"devDependencies":{"@types/node":"^20.0.0","tsup":"^8.0.0","typescript":"^5.0.0"},"homepage":"https://github.com/alexwhitmore/storage-helpers#readme","bugs":{"url":"https://github.com/alexwhitmore/storage-helpers/issues"},"repository":{"type":"git","url":"git+https://github.com/alexwhitmore/storage-helpers.git"},"license":"MIT","author":{"name":"Alex Whitmore"},"engines":{"node":">=18.0.0"},"_id":"@alexwhitmore/storage-helpers-cli@1.0.1","gitHead":"2e27cf8d5e089b976b8f72c3cd7b9dd72ade2e9f","_nodeVersion":"22.16.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-xRzmOCvSqZw0RCe6lTPnFzFOQEpf9AzyregM0zDEjnrJYwKsfU09ihOw/IjgCUDPgpeHuNTxrcCL6jp1Ti2C8w==","shasum":"fdedae1e6624d8df5d83b2edad3a8430f56919f6","tarball":"https://registry.npmjs.org/@alexwhitmore/storage-helpers-cli/-/storage-helpers-cli-1.0.1.tgz","fileCount":4,"unpackedSize":24698,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD62Rbv/nV4sPMRueXKVW73btbGeAEJnthiEzUuVPE2awIgP5c5J6847WHDQRzPSdHJVSzup/Ky8cZPLyo+JDumjLo="}]},"_npmUser":{"name":"alexwhitmore","email":"heyimalexw@gmail.com"},"directories":{},"maintainers":[{"name":"alexwhitmore","email":"heyimalexw@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/storage-helpers-cli_1.0.1_1766426296013_0.6603594536229549"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-22T17:52:42.533Z","modified":"2025-12-22T17:58:16.374Z","1.0.0":"2025-12-22T17:52:42.780Z","1.0.1":"2025-12-22T17:58:16.158Z"},"bugs":{"url":"https://github.com/alexwhitmore/storage-helpers/issues"},"author":{"name":"Alex Whitmore"},"license":"MIT","homepage":"https://github.com/alexwhitmore/storage-helpers#readme","repository":{"type":"git","url":"git+https://github.com/alexwhitmore/storage-helpers.git"},"description":"CLI tools for @alexwhitmore/storage-helpers","maintainers":[{"name":"alexwhitmore","email":"heyimalexw@gmail.com"}],"readme":"# @alexwhitmore/storage-helpers\n\n[![npm version](https://badge.fury.io/js/%40alexwhitmore%2Fstorage-helpers.svg)](https://www.npmjs.com/package/@alexwhitmore/storage-helpers)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nUpload files from external URLs directly to Supabase Storage — **no double bandwidth, no client-side buffering**.\n\n## The Problem\n\nWhen you need to upload a file from an external URL (like AI-generated images, Dropbox files, or webhook payloads), the typical flow looks like this:\n\n```\nExternal URL → Your App (download) → Your App (upload) → Supabase Storage\n                    ↓                      ↓\n              Uses bandwidth          Uses bandwidth AGAIN\n              Uses memory             Uses memory AGAIN\n```\n\nThis means:\n\n- 🔴 **2x bandwidth usage** (you download, then upload)\n- 🔴 **High latency** (two network hops)\n- 🔴 **Memory pressure** (file buffered in client memory)\n- 🔴 **Size limitations** (serverless limits, browser constraints)\n\n## The Solution\n\n```\nExternal URL → Edge Function → Supabase Storage\n                    ↓\n            Single server-side transfer\n```\n\n- ✅ **1x bandwidth** (server-side only)\n- ✅ **Lower latency** (single hop from edge)\n- ✅ **No client memory** (streaming on edge)\n- ✅ **Larger files** (up to 50MB)\n\n## Quick Start\n\n### 1. Install the package\n\n```bash\nnpm install @alexwhitmore/storage-helpers\n```\n\n### 2. Deploy the Edge Function\n\nCopy the edge function to your Supabase project:\n\n```bash\n# Create the function directory\nmkdir -p supabase/functions/upload-from-url\nmkdir -p supabase/functions/_shared\n\n# Copy the files (or use the CLI - see below)\n```\n\nOr use the CLI:\n\n```bash\nnpx @alexwhitmore/storage-helpers-cli init\n```\n\nThen deploy:\n\n```bash\nsupabase functions deploy upload-from-url\n```\n\n### 3. Use in your app\n\n```typescript\nimport { createClient } from '@supabase/supabase-js'\nimport { createStorageHelpers } from '@alexwhitmore/storage-helpers'\n\nconst supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)\n\nconst helpers = createStorageHelpers(supabase)\n\n// Upload a single file\nconst result = await helpers.uploadFromUrl({\n  bucket: 'images',\n  path: 'ai-generated/image.png',\n  sourceUrl: 'https://api.openai.com/v1/images/abc123.png',\n})\n\nconsole.log('Uploaded to:', result.path)\n// => 'ai-generated/image.png'\n```\n\n### Batch uploads\n\nUpload multiple files in parallel with controlled concurrency:\n\n```typescript\nconst results = await helpers.batchUploadFromUrl({\n  bucket: 'images',\n  jobs: [\n    { sourceUrl: 'https://example.com/1.png', path: 'batch/1.png' },\n    { sourceUrl: 'https://example.com/2.png', path: 'batch/2.png' },\n    { sourceUrl: 'https://example.com/3.png', path: 'batch/3.png' },\n  ],\n  concurrency: 3, // Process 3 at a time\n  upsert: true, // Overwrite existing files\n})\n\nconst successful = results.filter((r) => r.success)\nconst failed = results.filter((r) => !r.success)\n\nconsole.log(`Uploaded ${successful.length}/${results.length} files`)\n```\n\n## API Reference\n\n### `createStorageHelpers(supabase, config?)`\n\nCreates a new StorageHelpers instance.\n\n| Parameter           | Type           | Required | Description                                     |\n| ------------------- | -------------- | -------- | ----------------------------------------------- |\n| supabase            | SupabaseClient | ✅       | Supabase client instance                        |\n| config.functionName | string         | ❌       | Edge function name (default: \"upload-from-url\") |\n\n### `helpers.uploadFromUrl(options)`\n\nUpload a file from an external URL.\n\n| Option       | Type    | Required | Default | Description                |\n| ------------ | ------- | -------- | ------- | -------------------------- |\n| bucket       | string  | ✅       | -       | Storage bucket name        |\n| path         | string  | ✅       | -       | Destination path in bucket |\n| sourceUrl    | string  | ✅       | -       | URL to fetch file from     |\n| contentType  | string  | ❌       | auto    | Override content type      |\n| cacheControl | string  | ❌       | \"3600\"  | Cache-Control header       |\n| upsert       | boolean | ❌       | false   | Overwrite existing files   |\n\n**Returns:** `Promise<{ path: string; id: string; fullPath: string }>`\n\n### `helpers.batchUploadFromUrl(options)`\n\nUpload multiple files in parallel.\n\n| Option       | Type             | Required | Default | Description                   |\n| ------------ | ---------------- | -------- | ------- | ----------------------------- |\n| bucket       | string           | ✅       | -       | Storage bucket name           |\n| jobs         | BatchUploadJob[] | ✅       | -       | Array of upload jobs (max 20) |\n| concurrency  | number           | ❌       | 3       | Max parallel uploads          |\n| cacheControl | string           | ❌       | \"3600\"  | Cache-Control for all files   |\n| upsert       | boolean          | ❌       | false   | Overwrite existing files      |\n\n**Returns:** `Promise<BatchUploadResult[]>`\n\n## Security\n\nThis package includes **robust SSRF (Server-Side Request Forgery) protection**:\n\n| Protection                 | Description                                                 |\n| -------------------------- | ----------------------------------------------------------- |\n| 🛡️ Private IP blocking     | Blocks 10.x, 172.16.x, 192.168.x, 127.x, etc.               |\n| 🛡️ Cloud metadata blocking | Blocks 169.254.169.254 (AWS/GCP/Azure metadata)             |\n| 🛡️ DNS validation          | Resolves hostnames and checks resulting IPs                 |\n| 🛡️ Redirect blocking       | Disables HTTP redirects (prevents redirect-based bypass)    |\n| 🛡️ Protocol restriction    | Only HTTP/HTTPS allowed (no file://, gopher://, etc.)       |\n| 🛡️ Port blocking           | Blocks common internal service ports (22, 3306, 5432, etc.) |\n| 🛡️ Encoding protection     | Rejects octal IP notation and URL encoding tricks           |\n| 🛡️ Authentication required | Requires valid Supabase JWT                                 |\n\nSee [SECURITY.md](./SECURITY.md) for complete details.\n\n## Error Handling\n\n```typescript\nimport { createStorageHelpers, SsrfError, UploadError, ValidationError } from '@alexwhitmore/storage-helpers'\n\ntry {\n  await helpers.uploadFromUrl({\n    bucket: 'images',\n    path: 'test.png',\n    sourceUrl: 'https://example.com/image.png',\n  })\n} catch (error) {\n  if (error instanceof SsrfError) {\n    // URL was blocked for security reasons\n    console.error('Security blocked:', error.message)\n  } else if (error instanceof UploadError) {\n    // Upload failed\n    console.error('Upload failed:', error.code, error.message)\n    if (error.isRetryable()) {\n      // Retry the upload\n    }\n  } else if (error instanceof ValidationError) {\n    // Invalid input\n    console.error('Invalid input:', error.field, error.message)\n  }\n}\n```\n\n## Use Cases\n\n- 📸 **AI Image Generation**: Upload DALL-E/Midjourney/Stable Diffusion images directly\n- 📁 **Cloud Storage Integration**: Transfer files from Dropbox, Google Drive, etc.\n- 🔗 **Webhook Processing**: Store files from incoming webhooks\n- 🖼️ **Image Aggregation**: Collect images from multiple sources\n- 📦 **Asset Migration**: Bulk transfer assets from other services\n\n## Storage Bucket Setup\n\nYour storage bucket needs RLS policies to allow uploads. Here are common configurations:\n\n### Public bucket (anyone can read)\n\n```sql\n-- Create bucket\nINSERT INTO storage.buckets (id, name, public)\nVALUES ('my-bucket', 'my-bucket', true);\n\n-- Allow authenticated users to upload\nCREATE POLICY \"Authenticated users can upload\"\nON storage.objects FOR INSERT\nTO authenticated\nWITH CHECK (bucket_id = 'my-bucket');\n\n-- Allow public read access\nCREATE POLICY \"Public read access\"\nON storage.objects FOR SELECT\nTO public\nUSING (bucket_id = 'my-bucket');\n```\n\n### Private bucket (user-specific folders)\n\n```sql\n-- Create private bucket\nINSERT INTO storage.buckets (id, name, public)\nVALUES ('user-files', 'user-files', false);\n\n-- Users can only upload to their own folder\nCREATE POLICY \"Users upload to own folder\"\nON storage.objects FOR INSERT\nTO authenticated\nWITH CHECK (\n  bucket_id = 'user-files' AND\n  (storage.foldername(name))[1] = auth.uid()::text\n);\n\n-- Users can only read their own files\nCREATE POLICY \"Users read own files\"\nON storage.objects FOR SELECT\nTO authenticated\nUSING (\n  bucket_id = 'user-files' AND\n  (storage.foldername(name))[1] = auth.uid()::text\n);\n```\n\n## Requirements\n\n- Node.js 18+\n- Supabase project with Edge Functions enabled\n- `@supabase/supabase-js` v2.0+\n\n## Contributing\n\nContributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) first.\n\n## License\n\nMIT © Alex Whitmore\n\n---\n\n<p align=\"center\">\n  <sub>Built with ❤️ for the Supabase community</sub>\n</p>\n","readmeFilename":"README.md"}