@ibanzajoe/uploader 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,22 +37,77 @@ function App() {
37
37
  }
38
38
  ```
39
39
 
40
- ## Headless client (no React required)
40
+ ## Headless upload client (no React required)
41
+
42
+ The `@ibanzajoe/uploader/core` entry is a framework-free client — use it in a Node
43
+ script, a serverless function, a Vue/Svelte app, or behind your own UI. It has no
44
+ React dependency.
41
45
 
42
46
  ```ts
43
47
  import { UploaderClient } from '@ibanzajoe/uploader/core'
44
48
 
45
49
  const client = new UploaderClient({
46
- apikey: 'pk_your_api_key',
50
+ apikey: 'pk_your_api_key', // required — your public key
47
51
  apiUrl: 'https://your-api.example.com',
48
52
  })
49
53
 
50
- const result = await client.upload(file, { filename: 'photo.jpg' })
51
- console.log(result.handle) // e.g. "abc123def456"
52
- console.log(result.url) // public delivery URL
54
+ // Upload one file (a browser File/Blob, or a Node Blob/File):
55
+ const result = await client.upload(file, {
56
+ filename: 'photo.jpg',
57
+ onProgress: (pct) => console.log(`${pct}%`),
58
+ })
59
+
60
+ console.log(result.handle) // stable id, e.g. "abc123def456"
61
+ console.log(result.url) // delivery URL (cdn for public, edge for hotlink/signed)
62
+ console.log(result.size, result.mimetype)
63
+ ```
64
+
65
+ Upload **many** files with bounded concurrency:
66
+
67
+ ```ts
68
+ const results = await client.uploadAll(files, { concurrency: 3 })
69
+ ```
70
+
71
+ **`new UploaderClient(options)`**
72
+
73
+ | Option | Type | Description |
74
+ |---|---|---|
75
+ | `apikey` | `string` | **Required.** Public key (`pk_…`). |
76
+ | `apiUrl` | `string` | Base URL of the API. Omit for same-origin. |
77
+ | `security` | `{ policy, signature }` | Signed policy applied to every request (see Delivery protection). |
78
+ | `deliveryProtection` | `'public' \| 'hotlink' \| 'signed'` | Client-wide **default** protection for uploads (see below). |
79
+ | `allowedOrigins` | `string[]` | Client-wide default per-file origin lock. |
80
+ | `directUpload` | `boolean` | Leave unset to auto-detect direct-to-bucket; `false` forces the proxied flow. |
81
+
82
+ **`client.upload(file, options?) → Promise<FileResult>`**
83
+
84
+ | Upload option | Type | Description |
85
+ |---|---|---|
86
+ | `onProgress` | `(percent: number) => void` | `0`–`100`. Real byte progress on the direct + multipart paths. |
87
+ | `filename` | `string` | Override the stored filename. |
88
+ | `path` | `string` | Storage path prefix. |
89
+ | `signal` | `AbortSignal` | Cancel the upload (throws `UploaderError` code `ABORTED`). |
90
+ | `chunkSize` | `number` | Multipart chunk size in bytes (default ~5 MB). |
91
+ | `deliveryProtection` | `'public' \| 'hotlink' \| 'signed'` | **Per-upload** protection for this file — overrides the client default. |
92
+ | `allowedOrigins` | `string[]` | Per-upload origin lock for this file. |
93
+
94
+ **`FileResult`**
95
+
96
+ ```ts
97
+ type FileResult = {
98
+ handle: string // stable public id — build delivery/transform URLs from it
99
+ url: string // delivery URL for the original
100
+ filename: string
101
+ mimetype: string
102
+ size: number // bytes
103
+ status: 'Stored'
104
+ }
53
105
  ```
54
106
 
55
- Large files (≥ 6 MiB) are automatically uploaded via multipart; smaller files use a single PUT.
107
+ Large files (≥ ~6 MiB) upload via multipart automatically; smaller files use a
108
+ single PUT. When the account/plan allows it, bytes go **direct to storage** via a
109
+ presigned URL (they never transit the API); otherwise the client transparently
110
+ falls back to a proxied upload.
56
111
 
57
112
  ## Components
58
113
 
@@ -67,16 +122,93 @@ Full-screen modal picker with drag-and-drop, progress, and error UI.
67
122
  | `open` | `boolean` | Whether the modal is open. **Required.** |
68
123
  | `onClose` | `() => void` | Called when the modal should close (ESC, backdrop, cancel) |
69
124
  | `onUploadDone` | `(res: PickerResponse) => void` | Called when all uploads complete |
125
+ | `deliveryProtection` | `'public' \| 'hotlink' \| 'signed'` | Protection for every file this picker uploads (see Delivery protection). Omit → account default. |
126
+ | `allowedOrigins` | `string[]` | Per-file origin lock (see Delivery protection). Omit → account allowlist. |
70
127
  | `pickerOptions` | `PickerOptions` | File constraints + sources — `{ accept?: string[], maxFiles?, maxSize?, fromSources?, cameraFacingMode? }` |
71
128
  | `theme` | `UploaderTheme` | Per-instance design tokens (see Theming) |
72
129
 
130
+ `<DropPane>` accepts the same `apikey` / `apiUrl` / `deliveryProtection` /
131
+ `allowedOrigins` / callback props.
132
+
133
+ > **Requires `@ibanzajoe/uploader` ≥ 1.3.0** for `deliveryProtection` /
134
+ > `allowedOrigins` on the picker components. (Earlier versions only honored them
135
+ > on the headless client.)
136
+
73
137
  ### `<DropPane>`
74
138
 
75
139
  Inline drop zone that can be embedded in a form.
76
140
 
77
141
  ### `usePicker(options)`
78
142
 
79
- Headless hook returns `{ open, uploading, files, errors }`.
143
+ Headless hook for building a fully custom picker UI. Takes the same options as the
144
+ components (all `UploaderClientOptions` incl. `deliveryProtection`/`allowedOrigins`,
145
+ plus `pickerOptions` and the `onUpload*` callbacks) and returns:
146
+
147
+ ```ts
148
+ { files, addFiles, removeFile, editFile, retryFile, upload, progress, isUploading, isDone }
149
+ ```
150
+
151
+ ## Delivery protection (public / hotlink / signed)
152
+
153
+ Every file is served under one of three protection modes. **You choose the mode per
154
+ upload** (or set a client-wide default); it must be one your account's plan allows.
155
+
156
+ | Mode | Who can access | Delivery URL you get back |
157
+ |---|---|---|
158
+ | `public` | anyone with the link | `https://cdn.…/<key>` (CDN-cached, cheapest) |
159
+ | `hotlink` | requests from your allowed domains (Origin/Referer allowlist) | `https://edge.…/file/<handle>` |
160
+ | `signed` | only holders of a valid **signed URL** you mint server-side | `https://edge.…/file/<handle>` |
161
+
162
+ Set it per upload (headless or picker), or as a client-wide default:
163
+
164
+ ```ts
165
+ // headless — per upload wins over the client default
166
+ await client.upload(logo, { deliveryProtection: 'public' })
167
+ await client.upload(productImg, { deliveryProtection: 'hotlink' })
168
+ await client.upload(idScan, { deliveryProtection: 'signed' })
169
+
170
+ // client-wide default
171
+ const client = new UploaderClient({ apikey, apiUrl, deliveryProtection: 'signed' })
172
+ ```
173
+
174
+ ```tsx
175
+ // React picker (≥ 1.3.0)
176
+ <PickerOverlay apikey="pk_…" deliveryProtection="signed" open={open} onClose={close} />
177
+ ```
178
+
179
+ Resolution order: **per-upload → client default → the account default** configured in
180
+ the dashboard. If you request a mode your plan doesn't allow, the API responds `403
181
+ DELIVERY_MODE_NOT_ALLOWED`.
182
+
183
+ **`allowedOrigins`** (the domain list):
184
+ - For `hotlink`: omit it and the file uses your **account allowlist** (set in the
185
+ dashboard settings). Pass a per-file `allowedOrigins` only to override it for that
186
+ file (it replaces, not merges).
187
+ - For `signed`: the signature is the gate; a per-file `allowedOrigins` is an optional
188
+ extra domain lock layered on top.
189
+
190
+ ### Viewing a `signed` file
191
+
192
+ A `signed` file's `url` is not directly loadable — each view needs a fresh signed URL,
193
+ minted **server-side** with your API key secret. Use the one-call helper from the
194
+ **server** entry (Node only — never ship the secret to the browser):
195
+
196
+ ```ts
197
+ import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'
198
+
199
+ // in an authenticated backend route:
200
+ const url = getSignedDeliveryUrl({
201
+ handle: file.handle,
202
+ secret: process.env.UPLOADER_API_SECRET, // the API key's secret
203
+ baseUrl: 'https://edge.your-domain.com', // origin of FileResult.url
204
+ expiresIn: 300, // seconds (default 300)
205
+ // ops: [resize({ w: 400 }), output({ format: 'webp' })], // optional transform
206
+ })
207
+ // → https://edge.…/file/<handle>?policy=…&signature=… (hand to <img src>)
208
+ ```
209
+
210
+ Prefer to build it yourself? `withSignedPolicy(url, { policy, signature })` from
211
+ `@ibanzajoe/uploader/core` appends a `policy`/`signature` pair you produced.
80
212
 
81
213
  ## Theming
82
214
 
@@ -178,11 +310,13 @@ const client = new UploaderClient({
178
310
  })
179
311
  ```
180
312
 
181
- For building **read** URLs of signed files, use `withSignedPolicy(url, { policy,
182
- signature })`. The `policy`/`signature` pair is always produced **server-side** by
183
- your backend (HMAC over the API key's secret) the SDK never signs and never sees
184
- the secret. Compute it yourself, or use the dev helper
185
- `POST /api/admin/api-keys/:id/sign`. Full recipe + example:
313
+ For building **read** URLs of signed files, prefer the one-call
314
+ `getSignedDeliveryUrl()` from `@ibanzajoe/uploader/server` (see
315
+ [Delivery protection Viewing a signed file](#viewing-a-signed-file)). It signs and
316
+ assembles the URL for you. If you'd rather bring your own `policy`/`signature`
317
+ (produced **server-side** HMAC over the API key's secret; the SDK never signs in
318
+ the browser), append them with `withSignedPolicy(url, { policy, signature })`. Full
319
+ recipe + example:
186
320
  [docs/14 §7.3](https://github.com/ibanzajoe/file-uploader/blob/main/docs/14-sdk-developer-guide.md#73-signed-urls-the-private-tier).
187
321
 
188
322
  ## Building
@@ -144,6 +144,8 @@ var UploaderClient = class {
144
144
  #deliveryProtection;
145
145
  /** Client-level default per-file allowed origins (docs/13). */
146
146
  #allowedOrigins;
147
+ /** Optional consumer hook for non-disruptive usage warnings (soft bandwidth cap). */
148
+ #onUsageWarning;
147
149
  /** Memoized capability probe — one request per client, shared across uploads. */
148
150
  #capsPromise = null;
149
151
  constructor(options) {
@@ -153,6 +155,21 @@ var UploaderClient = class {
153
155
  this.#directUploadOption = options.directUpload;
154
156
  this.#deliveryProtection = options.deliveryProtection;
155
157
  this.#allowedOrigins = options.allowedOrigins;
158
+ this.#onUsageWarning = options.onUsageWarning;
159
+ }
160
+ /**
161
+ * Surface a non-disruptive usage warning carried on an upload response. Routes
162
+ * to the consumer's `onUsageWarning` hook if provided, else `console.warn`.
163
+ * Tolerates arbitrary JSON: only a well-formed `{ usageWarning }` triggers it.
164
+ */
165
+ #emitUsageWarning(body) {
166
+ if (typeof body !== "object" || body === null) return;
167
+ const w = body.usageWarning;
168
+ if (typeof w !== "object" || w === null) return;
169
+ const warning = w;
170
+ if (warning.type !== "bandwidth") return;
171
+ if (this.#onUsageWarning) this.#onUsageWarning(warning);
172
+ else console.warn(`[uploader] ${warning.message}`);
156
173
  }
157
174
  /**
158
175
  * Resolve the effective per-file protection for one upload: a per-upload value
@@ -239,6 +256,7 @@ var UploaderClient = class {
239
256
  );
240
257
  const body = await confirmRes.json();
241
258
  onProgress?.(100);
259
+ this.#emitUsageWarning(body);
242
260
  return this.#parseFileResult(body);
243
261
  }
244
262
  // ─── Auth headers ──────────────────────────────────────────────────────────
@@ -413,4 +431,4 @@ export {
413
431
  planChunks,
414
432
  UploaderClient
415
433
  };
416
- //# sourceMappingURL=chunk-6AJTZNOI.js.map
434
+ //# sourceMappingURL=chunk-7LTEX76J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts","../src/core/chunk.ts","../src/core/client.ts"],"sourcesContent":["/**\n * Typed errors for UploaderClient.\n *\n * UploaderError is thrown by upload / uploadAll on any non-retried failure.\n * The caller can narrow on `err.code` for structured handling.\n */\n\nexport type UploaderErrorCode =\n | 'ABORTED' // AbortSignal fired\n | 'NETWORK_ERROR' // fetch() threw (no response)\n | 'SERVER_ERROR' // 5xx after all retries exhausted\n | 'CLIENT_ERROR' // 4xx (not retried)\n | 'INVALID_RESPONSE' // response body did not match expected shape\n\nexport class UploaderError extends Error {\n readonly code: UploaderErrorCode\n /** HTTP status code when available (undefined for ABORTED / NETWORK_ERROR). */\n readonly statusCode?: number\n\n constructor(code: UploaderErrorCode, message: string, statusCode?: number) {\n super(message)\n this.name = 'UploaderError'\n this.code = code\n this.statusCode = statusCode\n }\n}\n","/**\n * Chunk planner for multipart uploads.\n *\n * Decides single-shot vs multipart by comparing the file size against\n * MULTIPART_THRESHOLD. For multipart files, slices the Blob into parts of\n * `chunkSize` bytes.\n */\n\n/** Files ≤ this size use single-shot POST /api/store. */\nexport const MULTIPART_THRESHOLD = 5 * 1024 * 1024 // 5 MB\n\n/** Default part size for multipart uploads. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024 // 5 MB\n\nexport type ChunkPlan =\n | { mode: 'single' }\n | { mode: 'multipart'; parts: Blob[]; partSize: number }\n\n/**\n * Build a chunk plan for a file.\n *\n * @param file The File or Blob to upload.\n * @param chunkSize Desired part size in bytes (default DEFAULT_CHUNK_SIZE).\n * @returns A plan describing whether to use single-shot or multipart.\n */\nexport function planChunks(file: File | Blob, chunkSize = DEFAULT_CHUNK_SIZE): ChunkPlan {\n if (file.size <= MULTIPART_THRESHOLD) {\n return { mode: 'single' }\n }\n\n const parts: Blob[] = []\n let offset = 0\n while (offset < file.size) {\n parts.push(file.slice(offset, offset + chunkSize))\n offset += chunkSize\n }\n return { mode: 'multipart', parts, partSize: chunkSize }\n}\n","/**\n * UploaderClient — headless upload client.\n *\n * Chooses single-shot (POST /api/store, multipart/form-data) vs multipart\n * (start/part/complete) based on file size relative to MULTIPART_THRESHOLD\n * (5 MB). Parts are retried individually with exponential backoff. Progress\n * is emitted as a 0–100 integer. AbortSignal cancels in-flight work.\n */\n\nimport type {\n FileResult,\n UploadStartResponse,\n UploadPartResponse,\n UploaderClientOptions,\n UploadAllOptions,\n UploadOptions,\n UsageWarning,\n} from './types.js'\nimport { UploaderError } from './errors.js'\nimport { planChunks } from './chunk.js'\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst MAX_RETRIES = 3\nconst RETRY_BASE_MS = 200\n\n/**\n * Single presigned PUT ceiling (mirrors the server's Phase-A limit). Files above\n * this fall back to the proxied multipart flow.\n */\nconst MAX_DIRECT_PUT_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB\n\n/**\n * Skip the advisory client checksum above this size — SubtleCrypto has no\n * streaming API, so hashing would load the whole file into memory a second time.\n * The checksum is advisory only (the server can't verify it anyway).\n */\nconst SHA256_MAX_BYTES = 64 * 1024 * 1024 // 64 MB\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/** Sleep for `ms` milliseconds, resolving early if signal fires. */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new UploaderError('ABORTED', 'Upload aborted'))\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener('abort', () => {\n clearTimeout(timer)\n reject(new UploaderError('ABORTED', 'Upload aborted'))\n }, { once: true })\n })\n}\n\n/** Throw UploaderError(ABORTED) if signal has already fired. */\nfunction checkAbort(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw new UploaderError('ABORTED', 'Upload aborted')\n }\n}\n\n/**\n * Fetch with retry on network errors and 5xx responses.\n * 4xx responses are not retried — they surface immediately as CLIENT_ERROR.\n */\nasync function fetchWithRetry(\n url: string,\n init: RequestInit,\n signal?: AbortSignal,\n maxRetries = MAX_RETRIES,\n): Promise<Response> {\n let lastErr: unknown\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n checkAbort(signal)\n try {\n const res = await fetch(url, { ...init, signal })\n if (res.status >= 400 && res.status < 500) {\n // Client error — do not retry\n const body = await res.text().catch(() => '')\n throw new UploaderError(\n 'CLIENT_ERROR',\n `HTTP ${res.status}: ${body}`,\n res.status,\n )\n }\n if (res.status >= 500) {\n // Server error — retry with backoff\n lastErr = new UploaderError(\n 'SERVER_ERROR',\n `HTTP ${res.status}`,\n res.status,\n )\n if (attempt < maxRetries - 1) {\n await sleep(RETRY_BASE_MS * 2 ** attempt, signal)\n }\n continue\n }\n return res\n } catch (err) {\n if (err instanceof UploaderError) {\n if (err.code === 'CLIENT_ERROR' || err.code === 'ABORTED') throw err\n lastErr = err\n } else {\n // fetch() threw (network failure, CORS, etc.)\n lastErr = new UploaderError(\n 'NETWORK_ERROR',\n err instanceof Error ? err.message : String(err),\n )\n }\n if (attempt < maxRetries - 1) {\n await sleep(RETRY_BASE_MS * 2 ** attempt, signal)\n }\n }\n }\n throw lastErr\n}\n\n/**\n * Best-effort SHA-256 (hex) of a Blob for the advisory checksum sent at confirm.\n * Returns undefined when SubtleCrypto is unavailable (insecure context / older\n * runtime) or the file is large — never throws. The server treats this as a hint\n * only, so skipping it is safe.\n */\nasync function sha256Hex(blob: Blob): Promise<string | undefined> {\n try {\n const c = (globalThis as { crypto?: Crypto }).crypto\n if (!c?.subtle || blob.size > SHA256_MAX_BYTES) return undefined\n const digest = await c.subtle.digest('SHA-256', await blob.arrayBuffer())\n return Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n } catch {\n return undefined\n }\n}\n\n/**\n * PUT a body straight to a presigned bucket URL via XMLHttpRequest.\n *\n * XHR (not fetch) so real upload progress is reported (fetch cannot). The\n * Content-Type header MUST equal the value the server signed, or S3/R2 reject\n * the signature. Bytes go browser → bucket; our server never sees them.\n */\nfunction xhrPut(\n url: string,\n body: Blob,\n opts: { contentType: string; onProgress?: (percent: number) => void; signal?: AbortSignal },\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (opts.signal?.aborted) {\n reject(new UploaderError('ABORTED', 'Upload aborted'))\n return\n }\n const xhr = new XMLHttpRequest()\n xhr.open('PUT', url)\n xhr.setRequestHeader('Content-Type', opts.contentType)\n\n if (opts.onProgress) {\n xhr.upload.onprogress = (e: ProgressEvent) => {\n if (e.lengthComputable) {\n // Reserve the last 5% for the confirm round-trip.\n opts.onProgress!(Math.round((e.loaded / e.total) * 95))\n }\n }\n }\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve()\n } else {\n const code = xhr.status >= 400 && xhr.status < 500 ? 'CLIENT_ERROR' : 'SERVER_ERROR'\n reject(new UploaderError(code, `Bucket PUT failed: HTTP ${xhr.status}`, xhr.status))\n }\n }\n xhr.onerror = () => reject(new UploaderError('NETWORK_ERROR', 'Bucket PUT network error'))\n xhr.onabort = () => reject(new UploaderError('ABORTED', 'Upload aborted'))\n\n if (opts.signal) {\n opts.signal.addEventListener('abort', () => xhr.abort(), { once: true })\n }\n xhr.send(body)\n })\n}\n\n/** Shape returned by POST /api/uploads/presign. */\ntype PresignResponse = { handle: string; storageKey: string; putUrl: string; contentType: string }\n\n// ─── UploaderClient ────────────────────────────────────────────────────────────\n\nexport class UploaderClient {\n readonly apikey: string\n readonly apiUrl: string\n readonly security: UploaderClientOptions['security']\n readonly #directUploadOption?: boolean\n /** Client-level default per-file delivery protection (docs/13). */\n readonly #deliveryProtection?: UploaderClientOptions['deliveryProtection']\n /** Client-level default per-file allowed origins (docs/13). */\n readonly #allowedOrigins?: string[]\n /** Optional consumer hook for non-disruptive usage warnings (soft bandwidth cap). */\n readonly #onUsageWarning?: UploaderClientOptions['onUsageWarning']\n\n /** Memoized capability probe — one request per client, shared across uploads. */\n #capsPromise: Promise<{ directUpload: boolean }> | null = null\n\n constructor(options: UploaderClientOptions) {\n this.apikey = options.apikey\n this.apiUrl = (options.apiUrl ?? 'https://api.uploaderhq.io').replace(/\\/$/, '')\n this.security = options.security\n this.#directUploadOption = options.directUpload\n this.#deliveryProtection = options.deliveryProtection\n this.#allowedOrigins = options.allowedOrigins\n this.#onUsageWarning = options.onUsageWarning\n }\n\n /**\n * Surface a non-disruptive usage warning carried on an upload response. Routes\n * to the consumer's `onUsageWarning` hook if provided, else `console.warn`.\n * Tolerates arbitrary JSON: only a well-formed `{ usageWarning }` triggers it.\n */\n #emitUsageWarning(body: unknown): void {\n if (typeof body !== 'object' || body === null) return\n const w = (body as { usageWarning?: unknown }).usageWarning\n if (typeof w !== 'object' || w === null) return\n const warning = w as UsageWarning\n if (warning.type !== 'bandwidth') return\n if (this.#onUsageWarning) this.#onUsageWarning(warning)\n else console.warn(`[uploader] ${warning.message}`)\n }\n\n /**\n * Resolve the effective per-file protection for one upload: a per-upload value\n * overrides the client-level default (docs/13). Returns an object carrying ONLY\n * the keys that are set, so callers spread it into the request body and unset\n * fields are omitted entirely — an old server ignores them and the file inherits\n * the account mode (`null`).\n */\n #resolveProtection(opts: UploadOptions): {\n deliveryProtection?: UploaderClientOptions['deliveryProtection']\n allowedOrigins?: string[]\n } {\n const out: { deliveryProtection?: UploaderClientOptions['deliveryProtection']; allowedOrigins?: string[] } = {}\n const mode = opts.deliveryProtection ?? this.#deliveryProtection\n if (mode !== undefined) out.deliveryProtection = mode\n const origins = opts.allowedOrigins ?? this.#allowedOrigins\n if (origins !== undefined) out.allowedOrigins = origins\n return out\n }\n\n // ─── Capability negotiation ─────────────────────────────────────────────────\n\n /**\n * Probe GET /api/capabilities once per client and cache the result. Fails OPEN\n * to the proxied flow ({ directUpload: false }) on any error/timeout, so a\n * flaky probe never blocks uploads and old servers (404) are handled.\n */\n #getCapabilities(): Promise<{ directUpload: boolean }> {\n // Explicit opt-out — never probe, never use direct upload.\n if (this.#directUploadOption === false) {\n return Promise.resolve({ directUpload: false })\n }\n if (!this.#capsPromise) {\n this.#capsPromise = fetch(`${this.apiUrl}/api/capabilities`, {\n headers: this.#authHeaders(),\n })\n .then(async (res) => {\n if (!res.ok) return { directUpload: false }\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>\n return { directUpload: body['directUpload'] === true }\n })\n .catch(() => ({ directUpload: false }))\n }\n return this.#capsPromise\n }\n\n // ─── Direct-to-bucket upload ────────────────────────────────────────────────\n\n /**\n * Presign → PUT-to-bucket → confirm. Bytes go browser → bucket directly; our\n * server only signs and records. Used when the account has the directUpload\n * capability and the file fits a single PUT.\n */\n async #uploadDirect(file: File | Blob, opts: UploadOptions): Promise<FileResult> {\n const { onProgress, filename, signal } = opts\n const name = filename ?? (file instanceof File ? file.name : 'upload')\n // Must match the Content-Type we send on the PUT (the server signs it).\n const contentType = file instanceof File && file.type ? file.type : 'application/octet-stream'\n\n onProgress?.(0)\n checkAbort(signal)\n\n const protection = this.#resolveProtection(opts)\n const presign = async (): Promise<PresignResponse> => {\n const res = await fetchWithRetry(\n `${this.apiUrl}/api/uploads/presign`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ filename: name, contentType, size: file.size, ...protection }),\n },\n signal,\n )\n return (await res.json()) as PresignResponse\n }\n\n let signed = await presign()\n\n // Advisory checksum (best-effort; unverifiable server-side).\n const checksum = await sha256Hex(file)\n\n // PUT straight to the bucket. A 403 means the short-lived signature lapsed\n // (or a stale key) — re-presign once and retry before giving up.\n try {\n await xhrPut(signed.putUrl, file, { contentType: signed.contentType, onProgress, signal })\n } catch (err) {\n if (err instanceof UploaderError && err.statusCode === 403) {\n signed = await presign()\n await xhrPut(signed.putUrl, file, { contentType: signed.contentType, onProgress, signal })\n } else {\n throw err\n }\n }\n\n // Confirm — server verifies the object exists, records usage, enqueues.\n checkAbort(signal)\n const confirmRes = await fetchWithRetry(\n `${this.apiUrl}/api/uploads/confirm`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ handle: signed.handle, checksum }),\n },\n signal,\n )\n const body = (await confirmRes.json()) as unknown\n onProgress?.(100)\n // Non-disruptive soft-cap heads-up (bandwidth). Emitted before returning so a\n // consumer sees it even if they ignore the return value; never throws.\n this.#emitUsageWarning(body)\n return this.#parseFileResult(body)\n }\n\n // ─── Auth headers ──────────────────────────────────────────────────────────\n\n /**\n * Build the auth headers shared by all requests.\n * Attaches the API key and, when present, the signed policy pair.\n */\n #authHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'X-Uploader-Key': this.apikey,\n }\n if (this.security) {\n headers['X-Uploader-Policy'] = this.security.policy\n headers['X-Uploader-Signature'] = this.security.signature\n }\n return headers\n }\n\n // ─── Single-shot upload ────────────────────────────────────────────────────\n\n /**\n * POST /api/store — multipart/form-data for files ≤ MULTIPART_THRESHOLD.\n */\n async #uploadSingleShot(\n file: File | Blob,\n opts: UploadOptions,\n ): Promise<FileResult> {\n const { onProgress, filename, signal } = opts\n\n onProgress?.(0)\n checkAbort(signal)\n\n const form = new FormData()\n form.append('file', file, filename ?? (file instanceof File ? file.name : 'upload'))\n if (filename) form.append('filename', filename)\n // Per-file delivery protection (docs/13) — sent as form fields; allowedOrigins\n // is JSON-encoded to match the server's parse. Omitted when unset (inherit).\n const protection = this.#resolveProtection(opts)\n if (protection.deliveryProtection) form.append('deliveryProtection', protection.deliveryProtection)\n if (protection.allowedOrigins) form.append('allowedOrigins', JSON.stringify(protection.allowedOrigins))\n\n const res = await fetchWithRetry(\n `${this.apiUrl}/api/store`,\n {\n method: 'POST',\n headers: this.#authHeaders(),\n body: form,\n },\n signal,\n )\n\n const body = await res.json() as unknown\n onProgress?.(100)\n return this.#parseFileResult(body)\n }\n\n // ─── Multipart upload ──────────────────────────────────────────────────────\n\n /**\n * start → parts (parallel with progress) → complete for files > MULTIPART_THRESHOLD.\n */\n async #uploadMultipart(\n file: File | Blob,\n parts: Blob[],\n opts: UploadOptions,\n ): Promise<FileResult> {\n const { onProgress, filename, signal } = opts\n const name = filename ?? (file instanceof File ? file.name : 'upload')\n const mime = file instanceof File ? file.type : 'application/octet-stream'\n\n onProgress?.(0)\n checkAbort(signal)\n\n // 1. Start\n const protection = this.#resolveProtection(opts)\n const startRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/start`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ filename: name, mimetype: mime, size: file.size, ...protection }),\n },\n signal,\n )\n const { uploadId } = await startRes.json() as UploadStartResponse\n\n // 2. Upload parts sequentially (retried individually)\n const etags: { partNumber: number; etag: string }[] = []\n let uploadedBytes = 0\n\n for (let i = 0; i < parts.length; i++) {\n checkAbort(signal)\n const partBlob = parts[i]!\n const partNumber = i + 1\n\n const partForm = new FormData()\n partForm.append('uploadId', uploadId)\n partForm.append('partNumber', String(partNumber))\n partForm.append('part', partBlob)\n\n const partRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/part`,\n {\n method: 'POST',\n headers: this.#authHeaders(),\n body: partForm,\n },\n signal,\n )\n const { etag } = await partRes.json() as UploadPartResponse\n\n etags.push({ partNumber, etag })\n uploadedBytes += partBlob.size\n onProgress?.(Math.round((uploadedBytes / file.size) * 95)) // reserve 5% for complete\n }\n\n // 3. Complete\n checkAbort(signal)\n const completeRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/complete`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ uploadId, parts: etags }),\n },\n signal,\n )\n const body = await completeRes.json() as unknown\n onProgress?.(100)\n return this.#parseFileResult(body)\n }\n\n // ─── Response parser ───────────────────────────────────────────────────────\n\n #parseFileResult(body: unknown): FileResult {\n if (\n typeof body !== 'object' ||\n body === null ||\n typeof (body as Record<string, unknown>)['handle'] !== 'string' ||\n typeof (body as Record<string, unknown>)['url'] !== 'string'\n ) {\n throw new UploaderError('INVALID_RESPONSE', 'Unexpected response shape from upload API')\n }\n return body as FileResult\n }\n\n // ─── Public API ───────────────────────────────────────────────────────────\n\n /**\n * Upload a single file.\n *\n * Automatically selects single-shot vs multipart upload based on file size.\n * Emits progress via `opts.onProgress` (0–100). Respects `opts.signal` for\n * cancellation. Retries network errors and 5xx responses up to 3 times with\n * exponential backoff; 4xx errors are surfaced immediately.\n *\n * @throws {UploaderError} with code ABORTED | NETWORK_ERROR | SERVER_ERROR |\n * CLIENT_ERROR | INVALID_RESPONSE\n */\n async upload(\n file: File | Blob,\n opts: UploadOptions = {},\n ): Promise<FileResult> {\n // Prefer direct-to-bucket when the account supports it and the file fits a\n // single PUT. The probe fails open, so an old server or a disabled account\n // transparently uses the proxied flow below.\n const caps = await this.#getCapabilities()\n if (caps.directUpload && file.size <= MAX_DIRECT_PUT_BYTES) {\n return this.#uploadDirect(file, opts)\n }\n\n const plan = planChunks(file, opts.chunkSize)\n if (plan.mode === 'single') {\n return this.#uploadSingleShot(file, opts)\n }\n return this.#uploadMultipart(file, plan.parts, opts)\n }\n\n /**\n * Upload multiple files with concurrency limiting.\n *\n * Resolves once all uploads settle (fulfilled or rejected). The returned\n * array preserves input order. `opts.concurrency` caps simultaneous\n * in-flight uploads (default 3). Each file shares the same opts\n * (including onProgress — the callback fires per-file, not aggregate).\n *\n * @returns Array of PromiseSettledResult in input order.\n */\n async uploadAll(\n files: Array<File | Blob>,\n opts: UploadAllOptions = {},\n ): Promise<PromiseSettledResult<FileResult>[]> {\n const concurrency = opts.concurrency ?? 3\n const results: PromiseSettledResult<FileResult>[] = new Array(files.length)\n\n let index = 0\n\n async function worker(client: UploaderClient): Promise<void> {\n while (index < files.length) {\n const i = index++\n const file = files[i]!\n try {\n results[i] = { status: 'fulfilled', value: await client.upload(file, opts) }\n } catch (err) {\n results[i] = { status: 'rejected', reason: err }\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, files.length) }, () =>\n worker(this),\n )\n await Promise.all(workers)\n return results\n }\n}\n"],"mappings":"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","names":[]}
@@ -37,6 +37,20 @@ type PolicySpec = {
37
37
  /** Restrict store destination path prefix. */
38
38
  path?: string;
39
39
  };
40
+ /**
41
+ * A non-disruptive usage warning surfaced alongside an upload response (mirrors
42
+ * @uploader/shared UsageWarning). Bandwidth limits are SOFT — delivery is never
43
+ * blocked when exceeded — so this is only ever an informational heads-up when the
44
+ * account is approaching or over its included monthly bandwidth.
45
+ */
46
+ type UsageWarning = {
47
+ type: 'bandwidth';
48
+ level: 'approaching' | 'over';
49
+ usedBytes: number;
50
+ limitBytes: number;
51
+ percent: number;
52
+ message: string;
53
+ };
40
54
  /** Options passed to UploaderClient constructor. */
41
55
  /**
42
56
  * Per-file delivery-protection mode (docs/13). Chosen by the developer at upload:
@@ -77,6 +91,13 @@ type UploaderClientOptions = {
77
91
  * `allowedOrigins`. Omitted → no per-file lock.
78
92
  */
79
93
  allowedOrigins?: string[];
94
+ /**
95
+ * Called when an upload response carries a non-disruptive usage warning — e.g.
96
+ * the account is approaching or over its included monthly bandwidth. Delivery is
97
+ * NOT affected (bandwidth is a soft cap); this is purely informational. When
98
+ * omitted, the SDK logs the warning via `console.warn` instead.
99
+ */
100
+ onUsageWarning?: (warning: UsageWarning) => void;
80
101
  };
81
102
  /** Per-file upload options. */
82
103
  type UploadOptions = {
@@ -151,4 +172,4 @@ declare class UploaderClient {
151
172
  uploadAll(files: Array<File | Blob>, opts?: UploadAllOptions): Promise<PromiseSettledResult<FileResult>[]>;
152
173
  }
153
174
 
154
- export { type DeliveryProtection as D, type FileResult as F, type PickerResponse as P, type UploadAllOptions as U, type PolicySpec as a, type UploadOptions as b, UploaderClient as c, type UploaderClientOptions as d };
175
+ export { type DeliveryProtection as D, type FileResult as F, type PickerResponse as P, type UploadAllOptions as U, type PolicySpec as a, type UploadOptions as b, UploaderClient as c, type UploaderClientOptions as d, type UsageWarning as e };
@@ -37,6 +37,20 @@ type PolicySpec = {
37
37
  /** Restrict store destination path prefix. */
38
38
  path?: string;
39
39
  };
40
+ /**
41
+ * A non-disruptive usage warning surfaced alongside an upload response (mirrors
42
+ * @uploader/shared UsageWarning). Bandwidth limits are SOFT — delivery is never
43
+ * blocked when exceeded — so this is only ever an informational heads-up when the
44
+ * account is approaching or over its included monthly bandwidth.
45
+ */
46
+ type UsageWarning = {
47
+ type: 'bandwidth';
48
+ level: 'approaching' | 'over';
49
+ usedBytes: number;
50
+ limitBytes: number;
51
+ percent: number;
52
+ message: string;
53
+ };
40
54
  /** Options passed to UploaderClient constructor. */
41
55
  /**
42
56
  * Per-file delivery-protection mode (docs/13). Chosen by the developer at upload:
@@ -77,6 +91,13 @@ type UploaderClientOptions = {
77
91
  * `allowedOrigins`. Omitted → no per-file lock.
78
92
  */
79
93
  allowedOrigins?: string[];
94
+ /**
95
+ * Called when an upload response carries a non-disruptive usage warning — e.g.
96
+ * the account is approaching or over its included monthly bandwidth. Delivery is
97
+ * NOT affected (bandwidth is a soft cap); this is purely informational. When
98
+ * omitted, the SDK logs the warning via `console.warn` instead.
99
+ */
100
+ onUsageWarning?: (warning: UsageWarning) => void;
80
101
  };
81
102
  /** Per-file upload options. */
82
103
  type UploadOptions = {
@@ -151,4 +172,4 @@ declare class UploaderClient {
151
172
  uploadAll(files: Array<File | Blob>, opts?: UploadAllOptions): Promise<PromiseSettledResult<FileResult>[]>;
152
173
  }
153
174
 
154
- export { type DeliveryProtection as D, type FileResult as F, type PickerResponse as P, type UploadAllOptions as U, type PolicySpec as a, type UploadOptions as b, UploaderClient as c, type UploaderClientOptions as d };
175
+ export { type DeliveryProtection as D, type FileResult as F, type PickerResponse as P, type UploadAllOptions as U, type PolicySpec as a, type UploadOptions as b, UploaderClient as c, type UploaderClientOptions as d, type UsageWarning as e };
package/dist/core.cjs CHANGED
@@ -183,6 +183,8 @@ var UploaderClient = class {
183
183
  #deliveryProtection;
184
184
  /** Client-level default per-file allowed origins (docs/13). */
185
185
  #allowedOrigins;
186
+ /** Optional consumer hook for non-disruptive usage warnings (soft bandwidth cap). */
187
+ #onUsageWarning;
186
188
  /** Memoized capability probe — one request per client, shared across uploads. */
187
189
  #capsPromise = null;
188
190
  constructor(options) {
@@ -192,6 +194,21 @@ var UploaderClient = class {
192
194
  this.#directUploadOption = options.directUpload;
193
195
  this.#deliveryProtection = options.deliveryProtection;
194
196
  this.#allowedOrigins = options.allowedOrigins;
197
+ this.#onUsageWarning = options.onUsageWarning;
198
+ }
199
+ /**
200
+ * Surface a non-disruptive usage warning carried on an upload response. Routes
201
+ * to the consumer's `onUsageWarning` hook if provided, else `console.warn`.
202
+ * Tolerates arbitrary JSON: only a well-formed `{ usageWarning }` triggers it.
203
+ */
204
+ #emitUsageWarning(body) {
205
+ if (typeof body !== "object" || body === null) return;
206
+ const w = body.usageWarning;
207
+ if (typeof w !== "object" || w === null) return;
208
+ const warning = w;
209
+ if (warning.type !== "bandwidth") return;
210
+ if (this.#onUsageWarning) this.#onUsageWarning(warning);
211
+ else console.warn(`[uploader] ${warning.message}`);
195
212
  }
196
213
  /**
197
214
  * Resolve the effective per-file protection for one upload: a per-upload value
@@ -278,6 +295,7 @@ var UploaderClient = class {
278
295
  );
279
296
  const body = await confirmRes.json();
280
297
  onProgress?.(100);
298
+ this.#emitUsageWarning(body);
281
299
  return this.#parseFileResult(body);
282
300
  }
283
301
  // ─── Auth headers ──────────────────────────────────────────────────────────