@ibanzajoe/uploader 1.2.1 → 1.3.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
package/dist/index.cjs CHANGED
@@ -506,11 +506,22 @@ function nextId() {
506
506
  function usePicker(opts) {
507
507
  const [files, setFiles] = (0, import_react.useState)([]);
508
508
  const clientRef = (0, import_react.useRef)(null);
509
- if (!clientRef.current || clientRef.current.apikey !== opts.apikey || clientRef.current.apiUrl !== (opts.apiUrl ?? "https://api.uploaderhq.io")) {
509
+ const clientSigRef = (0, import_react.useRef)("");
510
+ const clientSig = JSON.stringify({
511
+ apikey: opts.apikey,
512
+ apiUrl: opts.apiUrl ?? null,
513
+ security: opts.security ?? null,
514
+ deliveryProtection: opts.deliveryProtection ?? null,
515
+ allowedOrigins: opts.allowedOrigins ?? null
516
+ });
517
+ if (!clientRef.current || clientSigRef.current !== clientSig) {
518
+ clientSigRef.current = clientSig;
510
519
  clientRef.current = new UploaderClient({
511
520
  apikey: opts.apikey,
512
521
  apiUrl: opts.apiUrl,
513
- security: opts.security
522
+ security: opts.security,
523
+ deliveryProtection: opts.deliveryProtection,
524
+ allowedOrigins: opts.allowedOrigins
514
525
  });
515
526
  }
516
527
  const addFiles = (0, import_react.useCallback)(
@@ -1613,6 +1624,8 @@ function PickerOverlay({
1613
1624
  apikey,
1614
1625
  apiUrl,
1615
1626
  security,
1627
+ deliveryProtection,
1628
+ allowedOrigins,
1616
1629
  pickerOptions,
1617
1630
  onUploadDone,
1618
1631
  onFileUploadFinished,
@@ -1631,6 +1644,8 @@ function PickerOverlay({
1631
1644
  apikey,
1632
1645
  apiUrl,
1633
1646
  security,
1647
+ deliveryProtection,
1648
+ allowedOrigins,
1634
1649
  pickerOptions,
1635
1650
  onUploadDone,
1636
1651
  onFileUploadFinished,
@@ -1913,6 +1928,8 @@ function DropPane({
1913
1928
  apikey,
1914
1929
  apiUrl,
1915
1930
  security,
1931
+ deliveryProtection,
1932
+ allowedOrigins,
1916
1933
  pickerOptions,
1917
1934
  onUploadDone,
1918
1935
  onFileUploadFinished,
@@ -1930,6 +1947,8 @@ function DropPane({
1930
1947
  apikey,
1931
1948
  apiUrl,
1932
1949
  security,
1950
+ deliveryProtection,
1951
+ allowedOrigins,
1933
1952
  pickerOptions,
1934
1953
  onUploadDone,
1935
1954
  onFileUploadFinished,