{"_id":"@aurabx/standalone-uploader","_rev":"3-ba78dfaf92360fb434871e968b358932","name":"@aurabx/standalone-uploader","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@aurabx/standalone-uploader","version":"1.0.0","keywords":["dicom","uploader","medical-imaging","aurabox"],"author":{"name":"Aurabx"},"license":"MIT","_id":"@aurabx/standalone-uploader@1.0.0","maintainers":[{"name":"xtfer","email":"chris.skene@gmail.com"}],"dist":{"shasum":"cc434017cb0ddd3335fc6548840c96f84ac977b5","tarball":"https://registry.npmjs.org/@aurabx/standalone-uploader/-/standalone-uploader-1.0.0.tgz","fileCount":17,"integrity":"sha512-IRk0eTDvks86+j5OjyBqQf3JYHo7b3WZqYHmoxL5ndV8if9OfYA6bcDZxT1ZPW7xyabjrzAm5EhmGlNiDZUdEQ==","signatures":[{"sig":"MEUCIEj5XZfSCfdtuNEvOG4CeiXUx7NugY7XM1ht6LRil/pfAiEAvDTsHgRD8kO5u3louMKUtRVr+pj+0EJmZvv4+7ZTjPk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":3781460},"main":"dist/standalone-auraloader.umd.js","type":"module","types":"dist/types/index.d.ts","module":"dist/standalone-auraloader.es.js","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/standalone-auraloader.es.js","require":"./dist/standalone-auraloader.umd.js"}},"gitHead":"96705d436a259038462496cba940d0e3f2a18c01","scripts":{"dev":"vite","build":"vite build && tsc --declaration --outDir dist/types --emitDeclarationOnly","preview":"vite preview","typecheck":"tsc --noEmit"},"_npmUser":{"name":"xtfer","email":"chris.skene@gmail.com"},"_npmVersion":"10.9.3","description":"Standalone DICOM uploader component for embedding in external applications","directories":{},"_nodeVersion":"20.16.0","dependencies":{"uuid":"^13.0.0","axios":"^1.13.2","@uppy/tus":"^5.1.0","@uppy/core":"^5.2.0","dicom-parser":"^1.8.21","@zip.js/zip.js":"^2.8.15","@uppy/drag-drop":"^5.1.0","@uppy/status-bar":"^5.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.23.0","devDependencies":{"vite":"^7.3.1","vitest":"^4.0.18","@vitest/ui":"^4.0.18","typescript":"^5.9.3","@types/node":"^25.0.9","rollup-plugin-dts":"^6.3.0","vite-plugin-css-injected-by-js":"^3.5.2"},"peerDependencies":{"@aurabx/uploader-client":"^1.0.0"},"peerDependenciesMeta":{"@aurabx/uploader-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/standalone-uploader_1.0.0_1769383046023_0.5420189772246462","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@aurabx/standalone-uploader","version":"1.0.1","keywords":["dicom","uploader","medical-imaging","aurabox"],"author":{"name":"Aurabx"},"license":"MIT","_id":"@aurabx/standalone-uploader@1.0.1","maintainers":[{"name":"xtfer","email":"chris.skene@gmail.com"}],"dist":{"shasum":"0ad0c55eacd7cd496cbc29f964a15824a5c595ad","tarball":"https://registry.npmjs.org/@aurabx/standalone-uploader/-/standalone-uploader-1.0.1.tgz","fileCount":17,"integrity":"sha512-7iKjW+dHvQXA29XhS3TJhAxBwBQ2hfeubz924qwyLQuMGI+qHCG4/zYq6aP+iHpHLtx1cGhfXPBJewNiNS4kwg==","signatures":[{"sig":"MEUCIQC2wb7GTGr+d/FZZH17MqDKPbswyhOz7+u7hbluADY7DQIgSJnTbjr9oZAvapY7gB62BWFIOkgzd8dxgykZa1ZA8ZU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":3828080},"main":"dist/standalone-auraloader.umd.js","type":"module","_from":"file:aurabx-standalone-uploader-1.0.1.tgz","types":"dist/types/index.d.ts","module":"dist/standalone-auraloader.es.js","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/standalone-auraloader.es.js","require":"./dist/standalone-auraloader.umd.js"}},"scripts":{"dev":"vite","build":"vite build && tsc --declaration --outDir dist/types --emitDeclarationOnly","example":"vite preview --open /examples/index.html","preview":"vite preview","typecheck":"tsc --noEmit"},"_npmUser":{"name":"xtfer","email":"chris.skene@gmail.com"},"_resolved":"/private/var/folders/k3/p7l_4_t12n1f35c854wq1mz00000gn/T/93ab1cb9cab97995b82fc0b5b86b9ebf/aurabx-standalone-uploader-1.0.1.tgz","_integrity":"sha512-7iKjW+dHvQXA29XhS3TJhAxBwBQ2hfeubz924qwyLQuMGI+qHCG4/zYq6aP+iHpHLtx1cGhfXPBJewNiNS4kwg==","_npmVersion":"10.9.3","description":"Standalone DICOM uploader component for embedding in external applications","directories":{},"_nodeVersion":"20.16.0","dependencies":{"uuid":"^13.0.0","axios":"^1.15.1","@uppy/tus":"^5.1.1","@uppy/core":"^5.2.0","dicom-parser":"^1.8.21","@zip.js/zip.js":"^2.8.26","@uppy/drag-drop":"^5.1.0","@uppy/status-bar":"^5.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.3.2","vitest":"^4.1.4","@vitest/ui":"^4.1.4","typescript":"^5.9.3","@types/node":"^25.6.0","rollup-plugin-dts":"^6.4.1","vite-plugin-css-injected-by-js":"^3.5.2"},"peerDependencies":{"@aurabx/uploader-client":"^1.0.0"},"peerDependenciesMeta":{"@aurabx/uploader-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/standalone-uploader_1.0.1_1776843080376_0.6875114663209807","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@aurabx/standalone-uploader","version":"1.1.0","description":"Standalone DICOM uploader component for embedding in external applications","type":"module","main":"dist/standalone-auraloader.umd.js","module":"dist/standalone-auraloader.es.js","types":"dist/types/index.d.ts","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/standalone-auraloader.es.js","require":"./dist/standalone-auraloader.umd.js"}},"publishConfig":{"access":"public"},"keywords":["dicom","uploader","medical-imaging","aurabox"],"author":{"name":"Aurabx"},"license":"MIT","devDependencies":{"@types/node":"^25.6.0","@vitest/coverage-v8":"^4.1.10","@vitest/ui":"^4.1.10","happy-dom":"^20.11.2","rollup-plugin-dts":"^6.4.1","typescript":"^5.9.3","vite":"^7.3.2","vite-plugin-css-injected-by-js":"^3.5.2","vitest":"^4.1.10"},"dependencies":{"@uppy/core":"^5.2.0","@uppy/drag-drop":"^5.1.0","@uppy/status-bar":"^5.1.0","@uppy/tus":"^5.1.1","@zip.js/zip.js":"^2.8.26","axios":"^1.15.1","dicom-parser":"^1.8.21","uuid":"^13.0.0"},"peerDependencies":{"@aurabx/uploader-client":"^1.0.0"},"peerDependenciesMeta":{"@aurabx/uploader-client":{"optional":true}},"scripts":{"dev":"vite","build":"vite build && tsc --declaration --outDir dist/types --emitDeclarationOnly","typecheck":"tsc --noEmit","preview":"vite preview","example":"npm run build && vite --open /examples/index.html","test":"vitest run","test:watch":"vitest","test:ui":"vitest --ui","coverage":"vitest run --coverage"},"_id":"@aurabx/standalone-uploader@1.1.0","_integrity":"sha512-OmNHy7seEwownRhZjiZWsYwLK+LTTKg1HN8uth4dhlCyPZY8yKKegsLkh8TfX+6pb+oaRYKUK2/D5sTZ8SfBRQ==","_resolved":"/private/var/folders/k3/p7l_4_t12n1f35c854wq1mz00000gn/T/1b5ee7c24f0d417a05741b7374aa3cf1/aurabx-standalone-uploader-1.1.0.tgz","_from":"file:aurabx-standalone-uploader-1.1.0.tgz","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-OmNHy7seEwownRhZjiZWsYwLK+LTTKg1HN8uth4dhlCyPZY8yKKegsLkh8TfX+6pb+oaRYKUK2/D5sTZ8SfBRQ==","shasum":"dd01c765df69d2a7d842c7955371ed621d34415b","tarball":"https://registry.npmjs.org/@aurabx/standalone-uploader/-/standalone-uploader-1.1.0.tgz","fileCount":17,"unpackedSize":3881994,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCn/1FETLY5dfVkHoOeDCLXSTa//te8yz0wGURm7k9VNwIhAK/vEkTn9WeYGCONBgNgaNF68qe+QvAXz5VW9xUzsrXf"}]},"_npmUser":{"name":"xtfer","email":"chris.skene@gmail.com"},"directories":{},"maintainers":[{"name":"xtfer","email":"chris.skene@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/standalone-uploader_1.1.0_1787199538848_0.3807760377774143"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-25T23:17:25.958Z","modified":"2026-08-20T04:18:59.220Z","1.0.0":"2026-01-25T23:17:26.275Z","1.0.1":"2026-04-22T07:31:20.569Z","1.1.0":"2026-08-20T04:18:59.063Z"},"author":{"name":"Aurabx"},"license":"MIT","keywords":["dicom","uploader","medical-imaging","aurabox"],"description":"Standalone DICOM uploader component for embedding in external applications","maintainers":[{"name":"xtfer","email":"chris.skene@gmail.com"}],"readme":"# Standalone Auraloader\n\nA standalone DICOM uploader component for embedding in external applications. This package provides a complete, self-contained upload widget that handles DICOM file processing, validation, zipping, and upload via Tus (resumable) uploads.\n\n**This library is only required by (and can only be implemented by) Aurabox integration partners. Contact Aurabox for more information.**\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Authentication Flow](#authentication-flow)\n- [HMAC Implementation Guide](#hmac-implementation-guide)\n- [Usage](#usage)\n- [API](#api)\n- [Backend Requirements](#backend-requirements)\n- [Security Best Practices](#security-best-practices)\n- [Development](#development)\n\n## Installation\n\n### Via npx skills add\n\n```bash\nnpx skills add https://github.com/aurabx/skills\n```\n\n### Via npm/pnpm\n\n```bash\npnpm add @aurabx/standalone-uploader\n```\n\n### Via script tag\n\n```html\n<!-- All CSS is bundled - no external stylesheets required -->\n<script src=\"https://your-cdn.com/standalone-auraloader.umd.js\"></script>\n```\n\n## Authentication Flow\n\n**⚠️ IMPORTANT: HMAC secrets must NEVER be exposed to the browser!**\n\nThe standalone uploader uses a secure two-phase authentication flow:\n\n### Phase 1: Backend Token Exchange (Server-to-Server)\n\nYour **backend** exchanges HMAC credentials for an ephemeral upload token:\n\n```javascript\n// YOUR BACKEND (Node.js example)\nimport { HmacSigner } from \"@aurabx/standalone-uploader\";\n\nasync function getUploadToken(req, res) {\n  // HMAC credentials stay on the backend\n  const signer = new HmacSigner(\n    process.env.AURA_APP_ID,\n    process.env.AURA_APP_SECRET\n  );\n\n  const config = {\n    method: \"POST\",\n    url: \"/auth/exchange\",\n    baseURL: \"https://aura-instance.com/api\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"X-Api-Key\": process.env.SERVICE_API_KEY,\n    },\n    data: {\n      ttl: 3600, // Token valid for 1 hour\n      scopes: [\"upload:init\", \"upload:manage\"],\n    },\n  };\n\n  // Sign the request with HMAC\n  await signer.sign(config);\n\n  // Exchange for upload token\n  const response = await fetch(`${config.baseURL}${config.url}`, {\n    method: config.method,\n    headers: config.headers,\n    body: JSON.stringify(config.data),\n  });\n\n  const { token, expires_at } = await response.json();\n\n  // Return token to your frontend\n  res.json({ token, expires_at });\n}\n```\n\n### Phase 2: Browser Upload with Token\n\nYour **frontend** uses the ephemeral token to upload files:\n\n```javascript\n// YOUR FRONTEND (Browser)\nimport { StandaloneAuraloader } from \"@aurabx/standalone-uploader\";\n\nasync function initializeUploader() {\n  // Get upload token from YOUR backend (not Aura backend)\n  // e.g.\n  const { token } = await fetch(\"/api/upload-token\").then((r) => r.json());\n\n  // Load the StandaloneAuraloader component\n  const uploader = new StandaloneAuraloader({\n    apiToken: \"your-service-api-key\", // For team/realm identification, e.g. process.env.SERVICE_API_KEY\n    apiBaseUrl: \"https://aura-instance.com/api\",\n    uploadToken: token, // Ephemeral token from your backend\n    containerId: \"uploader-container\",\n    patientId: \"patient_123\", // Optional\n    context: { \n        // Used to match patients (depends in integration)\n        patient_id: \"your-patient-id\",\n        // Optional additional information\n        referral_id: \"your-referral-id\",\n        // Patient metadata (for creating a new patient)\n        patient: {\n          \"given_names\": \"john\",\n          \"family_name\": \"smith\",\n          \"date_of_birth\": \"1977-06-03\",\n          \"sex\": \"m\"\n        }\n    },\n    callbacks: {\n      onUploadComplete: (result) => console.log(\"Upload complete!\", result),\n      onUploadError: (error) => console.error(\"Upload failed:\", error),\n    },\n  });\n\n  await uploader.init();\n  await uploader.mount();\n}\n```\n\n## Usage\n\n### ES Module\n\n```javascript\nimport { StandaloneAuraloader } from \"@aurabx/standalone-uploader\";\n\nconst uploader = new StandaloneAuraloader({\n  apiToken: \"your-service-api-key\",\n  apiBaseUrl: \"https://aura-instance.com/api\",\n  uploadToken: \"aubt_...\", // Obtained from your backend\n  containerId: \"uploader-container\",\n  patientId: \"patient_123\", // Optional\n  context: { source: \"external-app\" }, // Optional\n  callbacks: {\n    onUploadComplete: (result) => {\n      console.log(\"Upload complete!\", result);\n    },\n    onUploadError: (error) => {\n      console.error(\"Upload failed:\", error);\n    },\n  },\n});\n\nawait uploader.init();\nawait uploader.mount();\n```\n\n### Script Tag (UMD)\n\n```html\n<div id=\"uploader\"></div>\n\n<script src=\"standalone-auraloader.umd.js\"></script>\n<script>\n  // Get upload token from your backend first\n  async function initUploader() {\n    const { token } = await fetch(\"/api/upload-token\").then((r) => r.json());\n\n    const uploader = new StandaloneAuraloader({\n      apiToken: \"your-service-api-key\",\n      apiBaseUrl: \"https://aura-instance.com/api\",\n      uploadToken: token,\n      containerId: \"uploader\",\n      callbacks: {\n        onUploadComplete: (result) => console.log(\"Complete!\", result),\n        onUploadError: (error) => console.error(\"Error:\", error),\n      },\n    });\n\n    await uploader.init();\n    await uploader.mount();\n  }\n\n  initUploader();\n</script>\n```\n\n## HMAC Implementation Guide\n\n📖 **For comprehensive implementation details, see [HMAC_IMPLEMENTATION.md](./HMAC_IMPLEMENTATION.md)**\n\n### Quick Reference\n\nThe AURA-HMAC-SHA256 protocol requires:\n\n1. **Required Headers** (always signed):\n   - `X-Aura-Timestamp` - Unix timestamp (seconds)\n   - `X-Aura-Nonce` - UUID v4 per request\n   - `Authorization` - Complete HMAC signature\n\n2. **Key Components**:\n   - Request canonicalization (AWS Signature V4 compatible)\n   - String to sign: `ALGORITHM\\\\nTIMESTAMP\\\\nCREDENTIAL_SCOPE\\\\nHASHED_REQUEST`\n   - Key derivation: `date_key → signing_key`\n\n3. **Security Rules**:\n   - ✅ HMAC secrets NEVER exposed to browser\n   - ✅ User authentication before token issuance\n   - ✅ Rate limiting on token exchange (10/minute)\n   - ✅ Appropriate token TTL and scopes\n\n4. **Implementation Options**:\n   - Node.js: Use built-in `HmacSigner` class\n   - Python/Java/Go: Implement AURA-HMAC-SHA256 protocol\n   - See detailed guide for complete examples\n\n## Configuration\n\n| Option        | Type     | Required | Description                                   |\n| ------------- | -------- | -------- |-----------------------------------------------|\n| `apiToken`    | `string` | Yes      | Service API key for team/realm identification |\n| `apiBaseUrl`  | `string` | Yes      | Base URL for the Aura API                     |\n| `uploadToken` | `string` | Yes      | Ephemeral upload token (from your backend)    |\n| `containerId` | `string` | Yes      | ID of the container element                   |\n| `patientId`   | `string` | No       | Aurabox Patient ID, if known                  |\n| `context`     | `object` | No       | Extra metadata sent during upload init        |\n| `callbacks`   | `object` | No       | Event callbacks                               |\n\n### Upload Token Format\n\nUpload tokens are ephemeral bearer tokens:\n\n- **Prefix**: `aubt_` (AUra Bearer Token)\n- **Lifetime**: Default 1 hour, maximum 24 hours\n- **Scope-limited**: Can restrict to specific operations\n- **Obtained from**: Your backend via HMAC exchange\n\n### Callbacks\n\n```typescript\ninterface Callbacks {\n  onUploadComplete?: (result: UploadResult) => void;\n  onUploadError?: (error: AuraloaderError) => void;\n  onUploadCancel?: () => void;\n  onStudiesFound?: (studies: StudyInfo[]) => void;\n  onStateChange?: (state: UploaderState) => void;\n}\n```\n\n## API\n\n### Methods\n\n| Method             | Description                                          |\n| ------------------ | ---------------------------------------------------- |\n| `init()`           | Initialize the uploader (fetches config from server) |\n| `mount()`          | Mount the UI to the DOM                              |\n| `upload()`         | Start the upload process                             |\n| `cancel()`         | Cancel the current upload                            |\n| `reset()`          | Reset to initial state                               |\n| `removeStudy(uid)` | Remove a study from the queue                        |\n| `destroy()`        | Cleanup and unmount                                  |\n\n## Backend Requirements\n\n### Aura API Endpoints\n\nThe standalone uploader expects the following endpoints under your `apiBaseUrl`:\n\n#### Token Exchange (HMAC authenticated - backend only)\n\n`POST /auth/exchange` - Exchange HMAC credentials for upload token\n\n- **Auth**: HMAC-signed request + Service API key via `X-Api-Key` header\n- **Body**:\n  ```json\n  {\n    \"ttl\": 3600,\n    \"scopes\": [\"upload:init\", \"upload:manage\", \"integration:read\"],\n    \"max_uses\": 100\n  }\n  ```\n- **Returns**:\n  ```json\n  {\n    \"token\": \"aubt_...\",\n    \"expires_at\": \"2025-01-24T12:00:00Z\",\n    \"scopes\": [\"upload:init\", \"upload:manage\"],\n    \"max_uses\": 100\n  }\n  ```\n- **Rate limit**: 10 requests/minute\n\n#### Configuration Endpoint (Upload token authenticated - browser)\n\n`GET /uploader/config` - Returns upload configuration (Tus endpoint, credentials)\n\n- **Auth**: Upload token (scope: `integration:read`) + Service API key\n- **Headers**: `X-Api-Key: <service-api-key>`, `Authorization: Bearer <upload-token>`\n- **Returns**:\n  ```json\n  {\n    \"lift\": {\n      \"endpoint\": \"https://tus-server.com/files/\",\n      \"token\": \"tus-auth-token\",\n      \"bucket\": \"uploads\"\n    },\n    \"mode\": \"bulk\"\n  }\n  ```\n- **Rate limit**: 30 requests/minute\n\n#### Upload Lifecycle Endpoints (Upload token authenticated - browser)\n\n`POST /uploader/upload/init` - Initialize upload session\n\n- **Auth**: Upload token (scope: `upload:init`) + Service API key\n- **Body**:\n  ```json\n  {\n    \"upload_id\": \"uuid-v4\",\n    \"studies\": { \"study-uid\": { \"patient_name\": \"...\", ... } },\n    \"mode\": \"bulk\",\n    \"source\": \"standalone-uploader\",\n    \"patient_id\": \"PAT-123\",\n    \"context\": { \"referral_id\": \"REF-456\" }\n  }\n  ```\n- **Returns**:\n  ```json\n  {\n    \"studies\": [{ \"id\": \"internal-study-id\" }]\n  }\n  ```\n\n`POST /uploader/upload/start` - Mark upload as started\n\n- **Auth**: Upload token (scope: `upload:manage`) + Service API key\n- **Body**: `{ \"upload_id\": \"...\", \"assembly_id\": \"...\", \"mode\": \"uploader\" }`\n\n`POST /uploader/upload/complete` - Mark upload as complete\n\n- **Auth**: Upload token (scope: `upload:manage`) + Service API key\n- **Body**: `{ \"upload_id\": \"...\", \"assembly_id\": \"...\", \"mode\": \"uploader\" }`\n\n`POST /uploader/upload/error` - Report upload error\n\n- **Auth**: Upload token (scope: `upload:manage`) + Service API key\n- **Body**: `{ \"upload_id\": \"...\", \"message\": \"error message\", \"mode\": \"uploader\" }`\n\n`POST /uploader/upload/cancel` - Cancel upload\n\n- **Auth**: Upload token (scope: `upload:manage`) + Service API key\n- **Body**: `{ \"upload_id\": \"...\", \"mode\": \"uploader\" }`\n\n**Rate limit**: 60 requests/minute for all upload lifecycle endpoints\n\n### Your Backend Implementation\n\nYou need to implement an endpoint to exchange HMAC credentials for upload tokens.\n\n📖 **For complete implementation details, see [HMAC_IMPLEMENTATION.md](./HMAC_IMPLEMENTATION.md)**\n\nHere's a simplified Node.js example:\n\n```javascript\n// Example: /api/upload-token\nimport { HmacSigner } from \"@aurabx/standalone-uploader\";\n\napp.post(\"/api/upload-token\", async (req, res) => {\n  // 1. Authenticate your user session\n  if (!req.session.user) {\n    return res.status(401).json({ error: \"Unauthorized\" });\n  }\n\n  // 2. Sign request with HMAC (using secrets from environment)\n  const signer = new HmacSigner(\n    process.env.AURA_APP_ID,\n    process.env.AURA_APP_SECRET\n  );\n\n  const config = {\n    method: \"POST\",\n    url: \"/auth/exchange\",\n    baseURL: process.env.AURA_API_URL,\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"X-Api-Key\": process.env.SERVICE_API_KEY,\n    },\n    data: {\n      ttl: 3600, // 1 hour\n      scopes: [\"upload:init\", \"upload:manage\", \"integration:read\"],\n    },\n  };\n\n  await signer.sign(config);\n\n  // 3. Call Aura's token exchange endpoint\n  const response = await fetch(`${config.baseURL}${config.url}`, {\n    method: config.method,\n    headers: config.headers,\n    body: JSON.stringify(config.data),\n  });\n\n  if (!response.ok) {\n    const error = await response.json();\n    return res.status(response.status).json(error);\n  }\n\n  const { token, expires_at, scopes } = await response.json();\n\n  // 4. Return upload token to your frontend\n  res.json({ token, expires_at, scopes });\n});\n```\n\n**Key Implementation Points:**\n- ✅ HMAC secrets stay on backend (never exposed to browser)\n- ✅ User authentication before token issuance  \n- ✅ Proper error handling and rate limiting\n- ✅ Use of appropriate scopes and TTL\n\n## Security Best Practices\n\n1. **NEVER expose HMAC secrets to browsers** - Keep them on your backend\n2. **Validate user sessions** before issuing upload tokens\n3. **Use short token lifetimes** (1 hour recommended)\n4. **Restrict token scopes** to minimum required operations\n5. **Implement rate limiting** on your token exchange endpoint\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Run typecheck\npnpm typecheck\n\n# Build\npnpm build\n\n# Dev server (for examples)\npnpm dev\n```\n\n### Example Application\n\nThe `examples/index.html` demonstrates the complete authentication flow including HMAC exchange:\n\n- **Step 1**: Backend token exchange (simulated in browser for demo purposes)\n- **Step 2**: Initialize uploader with obtained token\n- Includes Service API key, HMAC credentials, and upload token fields\n- Shows proper error handling and token validation\n\n**⚠️ Security Warning**: The example includes HMAC exchange in the browser for demonstration purposes only. In production:\n\n1. HMAC credentials (`app.id` and `app.secret`) must NEVER be exposed to the browser\n2. Token exchange must happen on your backend server\n3. Your frontend should fetch tokens from YOUR backend endpoint (e.g., `/api/upload-token`)\n\nThe example is useful for:\n\n- Understanding the two-phase authentication flow\n- Testing the uploader component locally\n- Seeing how HMAC signing works (for backend implementation reference)\n- Debugging authentication issues\n\n## License\n\nUNLICENSED - Proprietary software\n","readmeFilename":"README.md"}