@ibanzajoe/uploader 0.3.0 → 1.2.1
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 +46 -21
- package/dist/{chunk-EM6NZZSU.js → chunk-6AJTZNOI.js} +152 -51
- package/dist/chunk-6AJTZNOI.js.map +1 -0
- package/dist/chunk-DJK2SQ5I.js +56 -0
- package/dist/chunk-DJK2SQ5I.js.map +1 -0
- package/dist/client-CUmYuQ7Z.d.cts +154 -0
- package/dist/client-CUmYuQ7Z.d.ts +154 -0
- package/dist/core.cjs +158 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -1
- package/dist/core.d.ts +2 -1
- package/dist/core.js +8 -4
- package/dist/index.cjs +158 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +79 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +54 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +26 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +2 -2
- package/dist/transform-BTZ0kodO.d.cts +130 -0
- package/dist/transform-BTZ0kodO.d.ts +130 -0
- package/package.json +12 -2
- package/dist/chunk-EM6NZZSU.js.map +0 -1
- package/dist/transform-ybCOCWBG.d.cts +0 -225
- package/dist/transform-ybCOCWBG.d.ts +0 -225
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-level option and result types for UploaderClient.
|
|
3
|
+
*
|
|
4
|
+
* The upload-contract types below are VENDORED from @uploader/shared so that
|
|
5
|
+
* @ibanzajoe/uploader is self-contained for consumers outside this monorepo — one
|
|
6
|
+
* install, no @uploader/* or zod transitive deps. They mirror the API contract
|
|
7
|
+
* exactly; `contract.conformance.ts` asserts at typecheck time that they stay
|
|
8
|
+
* structurally identical to the shared definitions, failing the build on drift.
|
|
9
|
+
*/
|
|
10
|
+
/** A stored file result returned by the upload API and SDK. Mirrors Filestack's FileResult. */
|
|
11
|
+
type FileResult = {
|
|
12
|
+
handle: string;
|
|
13
|
+
url: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
mimetype: string;
|
|
16
|
+
size: number;
|
|
17
|
+
status: 'Stored';
|
|
18
|
+
uploadId?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Response from the picker overlay / upload batch. */
|
|
21
|
+
type PickerResponse = {
|
|
22
|
+
filesUploaded: FileResult[];
|
|
23
|
+
filesFailed: FileResult[];
|
|
24
|
+
};
|
|
25
|
+
/** JSON payload that is base64-encoded and HMAC-signed as a security policy. */
|
|
26
|
+
type PolicySpec = {
|
|
27
|
+
/** Unix epoch seconds when the policy expires. */
|
|
28
|
+
expiry: number;
|
|
29
|
+
/** Allowed API calls, e.g. ["store", "read", "remove"]. */
|
|
30
|
+
call: string[];
|
|
31
|
+
/** Restrict to a specific file handle (read/remove). */
|
|
32
|
+
handle?: string;
|
|
33
|
+
/** Maximum file size in bytes (store). */
|
|
34
|
+
maxSize?: number;
|
|
35
|
+
/** Minimum file size in bytes (store). */
|
|
36
|
+
minSize?: number;
|
|
37
|
+
/** Restrict store destination path prefix. */
|
|
38
|
+
path?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Options passed to UploaderClient constructor. */
|
|
41
|
+
/**
|
|
42
|
+
* Per-file delivery-protection mode (docs/13). Chosen by the developer at upload:
|
|
43
|
+
* public — anyone with the link.
|
|
44
|
+
* hotlink — only loads from an allowed origin (Origin/Referer allowlist).
|
|
45
|
+
* signed — requires a valid signed read URL (optionally origin-locked too).
|
|
46
|
+
* The chosen mode must be permitted by the account's plan or the server rejects
|
|
47
|
+
* the upload (403). Omitted → the file inherits the account's default mode.
|
|
48
|
+
*/
|
|
49
|
+
type DeliveryProtection = 'public' | 'hotlink' | 'signed';
|
|
50
|
+
type UploaderClientOptions = {
|
|
51
|
+
/** Public API key (pk_...). */
|
|
52
|
+
apikey: string;
|
|
53
|
+
/** Base URL of the upload API. Defaults to the hosted endpoint. */
|
|
54
|
+
apiUrl?: string;
|
|
55
|
+
/** Optional signed security policy. */
|
|
56
|
+
security?: {
|
|
57
|
+
policy: string;
|
|
58
|
+
signature: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Direct-to-bucket upload control:
|
|
62
|
+
* undefined (default) — auto-detect via GET /api/capabilities.
|
|
63
|
+
* false — always use the proxied flow (skip direct upload + the probe).
|
|
64
|
+
* The server ultimately gates direct upload per account regardless.
|
|
65
|
+
*/
|
|
66
|
+
directUpload?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Client-level default delivery protection applied to every upload from this
|
|
69
|
+
* client (docs/13). Useful for a dedicated uploader configured for sensitive
|
|
70
|
+
* files. A per-upload `deliveryProtection` overrides it. Omitted → inherit the
|
|
71
|
+
* account default server-side.
|
|
72
|
+
*/
|
|
73
|
+
deliveryProtection?: DeliveryProtection;
|
|
74
|
+
/**
|
|
75
|
+
* Client-level default per-file allowed origins (a domain lock layered on top
|
|
76
|
+
* of `signed`, or the allowlist for `hotlink`). Overridden per upload by
|
|
77
|
+
* `allowedOrigins`. Omitted → no per-file lock.
|
|
78
|
+
*/
|
|
79
|
+
allowedOrigins?: string[];
|
|
80
|
+
};
|
|
81
|
+
/** Per-file upload options. */
|
|
82
|
+
type UploadOptions = {
|
|
83
|
+
/** Progress callback, value 0–100. */
|
|
84
|
+
onProgress?: (percent: number) => void;
|
|
85
|
+
/** Storage path prefix. */
|
|
86
|
+
path?: string;
|
|
87
|
+
/** Override the file's stored name. */
|
|
88
|
+
filename?: string;
|
|
89
|
+
/** AbortSignal to cancel the upload. */
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
/**
|
|
92
|
+
* Chunk size in bytes for multipart uploads.
|
|
93
|
+
* Defaults to ~5 MB; files larger than this threshold use multipart.
|
|
94
|
+
*/
|
|
95
|
+
chunkSize?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Per-upload delivery protection (docs/13). Overrides the client-level default
|
|
98
|
+
* for this file. Must be permitted by the account's plan. Omitted → the client
|
|
99
|
+
* default, else the account default.
|
|
100
|
+
*/
|
|
101
|
+
deliveryProtection?: DeliveryProtection;
|
|
102
|
+
/**
|
|
103
|
+
* Per-upload allowed origins for this file's domain lock. Overrides the
|
|
104
|
+
* client-level default. Omitted → the client default, else no per-file lock.
|
|
105
|
+
*/
|
|
106
|
+
allowedOrigins?: string[];
|
|
107
|
+
};
|
|
108
|
+
/** Options for uploadAll — extends per-file options with concurrency control. */
|
|
109
|
+
type UploadAllOptions = UploadOptions & {
|
|
110
|
+
/** Maximum concurrent uploads. Defaults to 3. */
|
|
111
|
+
concurrency?: number;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* UploaderClient — headless upload client.
|
|
116
|
+
*
|
|
117
|
+
* Chooses single-shot (POST /api/store, multipart/form-data) vs multipart
|
|
118
|
+
* (start/part/complete) based on file size relative to MULTIPART_THRESHOLD
|
|
119
|
+
* (5 MB). Parts are retried individually with exponential backoff. Progress
|
|
120
|
+
* is emitted as a 0–100 integer. AbortSignal cancels in-flight work.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
declare class UploaderClient {
|
|
124
|
+
#private;
|
|
125
|
+
readonly apikey: string;
|
|
126
|
+
readonly apiUrl: string;
|
|
127
|
+
readonly security: UploaderClientOptions['security'];
|
|
128
|
+
constructor(options: UploaderClientOptions);
|
|
129
|
+
/**
|
|
130
|
+
* Upload a single file.
|
|
131
|
+
*
|
|
132
|
+
* Automatically selects single-shot vs multipart upload based on file size.
|
|
133
|
+
* Emits progress via `opts.onProgress` (0–100). Respects `opts.signal` for
|
|
134
|
+
* cancellation. Retries network errors and 5xx responses up to 3 times with
|
|
135
|
+
* exponential backoff; 4xx errors are surfaced immediately.
|
|
136
|
+
*
|
|
137
|
+
* @throws {UploaderError} with code ABORTED | NETWORK_ERROR | SERVER_ERROR |
|
|
138
|
+
* CLIENT_ERROR | INVALID_RESPONSE
|
|
139
|
+
*/
|
|
140
|
+
upload(file: File | Blob, opts?: UploadOptions): Promise<FileResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Upload multiple files with concurrency limiting.
|
|
143
|
+
*
|
|
144
|
+
* Resolves once all uploads settle (fulfilled or rejected). The returned
|
|
145
|
+
* array preserves input order. `opts.concurrency` caps simultaneous
|
|
146
|
+
* in-flight uploads (default 3). Each file shares the same opts
|
|
147
|
+
* (including onProgress — the callback fires per-file, not aggregate).
|
|
148
|
+
*
|
|
149
|
+
* @returns Array of PromiseSettledResult in input order.
|
|
150
|
+
*/
|
|
151
|
+
uploadAll(files: Array<File | Blob>, opts?: UploadAllOptions): Promise<PromiseSettledResult<FileResult>[]>;
|
|
152
|
+
}
|
|
153
|
+
|
|
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 };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-level option and result types for UploaderClient.
|
|
3
|
+
*
|
|
4
|
+
* The upload-contract types below are VENDORED from @uploader/shared so that
|
|
5
|
+
* @ibanzajoe/uploader is self-contained for consumers outside this monorepo — one
|
|
6
|
+
* install, no @uploader/* or zod transitive deps. They mirror the API contract
|
|
7
|
+
* exactly; `contract.conformance.ts` asserts at typecheck time that they stay
|
|
8
|
+
* structurally identical to the shared definitions, failing the build on drift.
|
|
9
|
+
*/
|
|
10
|
+
/** A stored file result returned by the upload API and SDK. Mirrors Filestack's FileResult. */
|
|
11
|
+
type FileResult = {
|
|
12
|
+
handle: string;
|
|
13
|
+
url: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
mimetype: string;
|
|
16
|
+
size: number;
|
|
17
|
+
status: 'Stored';
|
|
18
|
+
uploadId?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Response from the picker overlay / upload batch. */
|
|
21
|
+
type PickerResponse = {
|
|
22
|
+
filesUploaded: FileResult[];
|
|
23
|
+
filesFailed: FileResult[];
|
|
24
|
+
};
|
|
25
|
+
/** JSON payload that is base64-encoded and HMAC-signed as a security policy. */
|
|
26
|
+
type PolicySpec = {
|
|
27
|
+
/** Unix epoch seconds when the policy expires. */
|
|
28
|
+
expiry: number;
|
|
29
|
+
/** Allowed API calls, e.g. ["store", "read", "remove"]. */
|
|
30
|
+
call: string[];
|
|
31
|
+
/** Restrict to a specific file handle (read/remove). */
|
|
32
|
+
handle?: string;
|
|
33
|
+
/** Maximum file size in bytes (store). */
|
|
34
|
+
maxSize?: number;
|
|
35
|
+
/** Minimum file size in bytes (store). */
|
|
36
|
+
minSize?: number;
|
|
37
|
+
/** Restrict store destination path prefix. */
|
|
38
|
+
path?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Options passed to UploaderClient constructor. */
|
|
41
|
+
/**
|
|
42
|
+
* Per-file delivery-protection mode (docs/13). Chosen by the developer at upload:
|
|
43
|
+
* public — anyone with the link.
|
|
44
|
+
* hotlink — only loads from an allowed origin (Origin/Referer allowlist).
|
|
45
|
+
* signed — requires a valid signed read URL (optionally origin-locked too).
|
|
46
|
+
* The chosen mode must be permitted by the account's plan or the server rejects
|
|
47
|
+
* the upload (403). Omitted → the file inherits the account's default mode.
|
|
48
|
+
*/
|
|
49
|
+
type DeliveryProtection = 'public' | 'hotlink' | 'signed';
|
|
50
|
+
type UploaderClientOptions = {
|
|
51
|
+
/** Public API key (pk_...). */
|
|
52
|
+
apikey: string;
|
|
53
|
+
/** Base URL of the upload API. Defaults to the hosted endpoint. */
|
|
54
|
+
apiUrl?: string;
|
|
55
|
+
/** Optional signed security policy. */
|
|
56
|
+
security?: {
|
|
57
|
+
policy: string;
|
|
58
|
+
signature: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Direct-to-bucket upload control:
|
|
62
|
+
* undefined (default) — auto-detect via GET /api/capabilities.
|
|
63
|
+
* false — always use the proxied flow (skip direct upload + the probe).
|
|
64
|
+
* The server ultimately gates direct upload per account regardless.
|
|
65
|
+
*/
|
|
66
|
+
directUpload?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Client-level default delivery protection applied to every upload from this
|
|
69
|
+
* client (docs/13). Useful for a dedicated uploader configured for sensitive
|
|
70
|
+
* files. A per-upload `deliveryProtection` overrides it. Omitted → inherit the
|
|
71
|
+
* account default server-side.
|
|
72
|
+
*/
|
|
73
|
+
deliveryProtection?: DeliveryProtection;
|
|
74
|
+
/**
|
|
75
|
+
* Client-level default per-file allowed origins (a domain lock layered on top
|
|
76
|
+
* of `signed`, or the allowlist for `hotlink`). Overridden per upload by
|
|
77
|
+
* `allowedOrigins`. Omitted → no per-file lock.
|
|
78
|
+
*/
|
|
79
|
+
allowedOrigins?: string[];
|
|
80
|
+
};
|
|
81
|
+
/** Per-file upload options. */
|
|
82
|
+
type UploadOptions = {
|
|
83
|
+
/** Progress callback, value 0–100. */
|
|
84
|
+
onProgress?: (percent: number) => void;
|
|
85
|
+
/** Storage path prefix. */
|
|
86
|
+
path?: string;
|
|
87
|
+
/** Override the file's stored name. */
|
|
88
|
+
filename?: string;
|
|
89
|
+
/** AbortSignal to cancel the upload. */
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
/**
|
|
92
|
+
* Chunk size in bytes for multipart uploads.
|
|
93
|
+
* Defaults to ~5 MB; files larger than this threshold use multipart.
|
|
94
|
+
*/
|
|
95
|
+
chunkSize?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Per-upload delivery protection (docs/13). Overrides the client-level default
|
|
98
|
+
* for this file. Must be permitted by the account's plan. Omitted → the client
|
|
99
|
+
* default, else the account default.
|
|
100
|
+
*/
|
|
101
|
+
deliveryProtection?: DeliveryProtection;
|
|
102
|
+
/**
|
|
103
|
+
* Per-upload allowed origins for this file's domain lock. Overrides the
|
|
104
|
+
* client-level default. Omitted → the client default, else no per-file lock.
|
|
105
|
+
*/
|
|
106
|
+
allowedOrigins?: string[];
|
|
107
|
+
};
|
|
108
|
+
/** Options for uploadAll — extends per-file options with concurrency control. */
|
|
109
|
+
type UploadAllOptions = UploadOptions & {
|
|
110
|
+
/** Maximum concurrent uploads. Defaults to 3. */
|
|
111
|
+
concurrency?: number;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* UploaderClient — headless upload client.
|
|
116
|
+
*
|
|
117
|
+
* Chooses single-shot (POST /api/store, multipart/form-data) vs multipart
|
|
118
|
+
* (start/part/complete) based on file size relative to MULTIPART_THRESHOLD
|
|
119
|
+
* (5 MB). Parts are retried individually with exponential backoff. Progress
|
|
120
|
+
* is emitted as a 0–100 integer. AbortSignal cancels in-flight work.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
declare class UploaderClient {
|
|
124
|
+
#private;
|
|
125
|
+
readonly apikey: string;
|
|
126
|
+
readonly apiUrl: string;
|
|
127
|
+
readonly security: UploaderClientOptions['security'];
|
|
128
|
+
constructor(options: UploaderClientOptions);
|
|
129
|
+
/**
|
|
130
|
+
* Upload a single file.
|
|
131
|
+
*
|
|
132
|
+
* Automatically selects single-shot vs multipart upload based on file size.
|
|
133
|
+
* Emits progress via `opts.onProgress` (0–100). Respects `opts.signal` for
|
|
134
|
+
* cancellation. Retries network errors and 5xx responses up to 3 times with
|
|
135
|
+
* exponential backoff; 4xx errors are surfaced immediately.
|
|
136
|
+
*
|
|
137
|
+
* @throws {UploaderError} with code ABORTED | NETWORK_ERROR | SERVER_ERROR |
|
|
138
|
+
* CLIENT_ERROR | INVALID_RESPONSE
|
|
139
|
+
*/
|
|
140
|
+
upload(file: File | Blob, opts?: UploadOptions): Promise<FileResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Upload multiple files with concurrency limiting.
|
|
143
|
+
*
|
|
144
|
+
* Resolves once all uploads settle (fulfilled or rejected). The returned
|
|
145
|
+
* array preserves input order. `opts.concurrency` caps simultaneous
|
|
146
|
+
* in-flight uploads (default 3). Each file shares the same opts
|
|
147
|
+
* (including onProgress — the callback fires per-file, not aggregate).
|
|
148
|
+
*
|
|
149
|
+
* @returns Array of PromiseSettledResult in input order.
|
|
150
|
+
*/
|
|
151
|
+
uploadAll(files: Array<File | Blob>, opts?: UploadAllOptions): Promise<PromiseSettledResult<FileResult>[]>;
|
|
152
|
+
}
|
|
153
|
+
|
|
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 };
|
package/dist/core.cjs
CHANGED
|
@@ -32,7 +32,8 @@ __export(core_exports, {
|
|
|
32
32
|
quality: () => quality,
|
|
33
33
|
resize: () => resize,
|
|
34
34
|
rotate: () => rotate,
|
|
35
|
-
transformUrl: () => transformUrl
|
|
35
|
+
transformUrl: () => transformUrl,
|
|
36
|
+
withSignedPolicy: () => withSignedPolicy
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(core_exports);
|
|
38
39
|
|
|
@@ -68,6 +69,8 @@ function planChunks(file, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
|
68
69
|
// src/core/client.ts
|
|
69
70
|
var MAX_RETRIES = 3;
|
|
70
71
|
var RETRY_BASE_MS = 200;
|
|
72
|
+
var MAX_DIRECT_PUT_BYTES = 5 * 1024 * 1024 * 1024;
|
|
73
|
+
var SHA256_MAX_BYTES = 64 * 1024 * 1024;
|
|
71
74
|
function sleep(ms, signal) {
|
|
72
75
|
return new Promise((resolve, reject) => {
|
|
73
76
|
if (signal?.aborted) {
|
|
@@ -129,14 +132,153 @@ async function fetchWithRetry(url, init, signal, maxRetries = MAX_RETRIES) {
|
|
|
129
132
|
}
|
|
130
133
|
throw lastErr;
|
|
131
134
|
}
|
|
135
|
+
async function sha256Hex(blob) {
|
|
136
|
+
try {
|
|
137
|
+
const c = globalThis.crypto;
|
|
138
|
+
if (!c?.subtle || blob.size > SHA256_MAX_BYTES) return void 0;
|
|
139
|
+
const digest = await c.subtle.digest("SHA-256", await blob.arrayBuffer());
|
|
140
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
141
|
+
} catch {
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function xhrPut(url, body, opts) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
if (opts.signal?.aborted) {
|
|
148
|
+
reject(new UploaderError("ABORTED", "Upload aborted"));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const xhr = new XMLHttpRequest();
|
|
152
|
+
xhr.open("PUT", url);
|
|
153
|
+
xhr.setRequestHeader("Content-Type", opts.contentType);
|
|
154
|
+
if (opts.onProgress) {
|
|
155
|
+
xhr.upload.onprogress = (e) => {
|
|
156
|
+
if (e.lengthComputable) {
|
|
157
|
+
opts.onProgress(Math.round(e.loaded / e.total * 95));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
xhr.onload = () => {
|
|
162
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
163
|
+
resolve();
|
|
164
|
+
} else {
|
|
165
|
+
const code = xhr.status >= 400 && xhr.status < 500 ? "CLIENT_ERROR" : "SERVER_ERROR";
|
|
166
|
+
reject(new UploaderError(code, `Bucket PUT failed: HTTP ${xhr.status}`, xhr.status));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
xhr.onerror = () => reject(new UploaderError("NETWORK_ERROR", "Bucket PUT network error"));
|
|
170
|
+
xhr.onabort = () => reject(new UploaderError("ABORTED", "Upload aborted"));
|
|
171
|
+
if (opts.signal) {
|
|
172
|
+
opts.signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
173
|
+
}
|
|
174
|
+
xhr.send(body);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
132
177
|
var UploaderClient = class {
|
|
133
178
|
apikey;
|
|
134
179
|
apiUrl;
|
|
135
180
|
security;
|
|
181
|
+
#directUploadOption;
|
|
182
|
+
/** Client-level default per-file delivery protection (docs/13). */
|
|
183
|
+
#deliveryProtection;
|
|
184
|
+
/** Client-level default per-file allowed origins (docs/13). */
|
|
185
|
+
#allowedOrigins;
|
|
186
|
+
/** Memoized capability probe — one request per client, shared across uploads. */
|
|
187
|
+
#capsPromise = null;
|
|
136
188
|
constructor(options) {
|
|
137
189
|
this.apikey = options.apikey;
|
|
138
190
|
this.apiUrl = (options.apiUrl ?? "https://api.uploaderhq.io").replace(/\/$/, "");
|
|
139
191
|
this.security = options.security;
|
|
192
|
+
this.#directUploadOption = options.directUpload;
|
|
193
|
+
this.#deliveryProtection = options.deliveryProtection;
|
|
194
|
+
this.#allowedOrigins = options.allowedOrigins;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Resolve the effective per-file protection for one upload: a per-upload value
|
|
198
|
+
* overrides the client-level default (docs/13). Returns an object carrying ONLY
|
|
199
|
+
* the keys that are set, so callers spread it into the request body and unset
|
|
200
|
+
* fields are omitted entirely — an old server ignores them and the file inherits
|
|
201
|
+
* the account mode (`null`).
|
|
202
|
+
*/
|
|
203
|
+
#resolveProtection(opts) {
|
|
204
|
+
const out = {};
|
|
205
|
+
const mode = opts.deliveryProtection ?? this.#deliveryProtection;
|
|
206
|
+
if (mode !== void 0) out.deliveryProtection = mode;
|
|
207
|
+
const origins = opts.allowedOrigins ?? this.#allowedOrigins;
|
|
208
|
+
if (origins !== void 0) out.allowedOrigins = origins;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
// ─── Capability negotiation ─────────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Probe GET /api/capabilities once per client and cache the result. Fails OPEN
|
|
214
|
+
* to the proxied flow ({ directUpload: false }) on any error/timeout, so a
|
|
215
|
+
* flaky probe never blocks uploads and old servers (404) are handled.
|
|
216
|
+
*/
|
|
217
|
+
#getCapabilities() {
|
|
218
|
+
if (this.#directUploadOption === false) {
|
|
219
|
+
return Promise.resolve({ directUpload: false });
|
|
220
|
+
}
|
|
221
|
+
if (!this.#capsPromise) {
|
|
222
|
+
this.#capsPromise = fetch(`${this.apiUrl}/api/capabilities`, {
|
|
223
|
+
headers: this.#authHeaders()
|
|
224
|
+
}).then(async (res) => {
|
|
225
|
+
if (!res.ok) return { directUpload: false };
|
|
226
|
+
const body = await res.json().catch(() => ({}));
|
|
227
|
+
return { directUpload: body["directUpload"] === true };
|
|
228
|
+
}).catch(() => ({ directUpload: false }));
|
|
229
|
+
}
|
|
230
|
+
return this.#capsPromise;
|
|
231
|
+
}
|
|
232
|
+
// ─── Direct-to-bucket upload ────────────────────────────────────────────────
|
|
233
|
+
/**
|
|
234
|
+
* Presign → PUT-to-bucket → confirm. Bytes go browser → bucket directly; our
|
|
235
|
+
* server only signs and records. Used when the account has the directUpload
|
|
236
|
+
* capability and the file fits a single PUT.
|
|
237
|
+
*/
|
|
238
|
+
async #uploadDirect(file, opts) {
|
|
239
|
+
const { onProgress, filename, signal } = opts;
|
|
240
|
+
const name = filename ?? (file instanceof File ? file.name : "upload");
|
|
241
|
+
const contentType = file instanceof File && file.type ? file.type : "application/octet-stream";
|
|
242
|
+
onProgress?.(0);
|
|
243
|
+
checkAbort(signal);
|
|
244
|
+
const protection = this.#resolveProtection(opts);
|
|
245
|
+
const presign = async () => {
|
|
246
|
+
const res = await fetchWithRetry(
|
|
247
|
+
`${this.apiUrl}/api/uploads/presign`,
|
|
248
|
+
{
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { ...this.#authHeaders(), "Content-Type": "application/json" },
|
|
251
|
+
body: JSON.stringify({ filename: name, contentType, size: file.size, ...protection })
|
|
252
|
+
},
|
|
253
|
+
signal
|
|
254
|
+
);
|
|
255
|
+
return await res.json();
|
|
256
|
+
};
|
|
257
|
+
let signed = await presign();
|
|
258
|
+
const checksum = await sha256Hex(file);
|
|
259
|
+
try {
|
|
260
|
+
await xhrPut(signed.putUrl, file, { contentType: signed.contentType, onProgress, signal });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err instanceof UploaderError && err.statusCode === 403) {
|
|
263
|
+
signed = await presign();
|
|
264
|
+
await xhrPut(signed.putUrl, file, { contentType: signed.contentType, onProgress, signal });
|
|
265
|
+
} else {
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
checkAbort(signal);
|
|
270
|
+
const confirmRes = await fetchWithRetry(
|
|
271
|
+
`${this.apiUrl}/api/uploads/confirm`,
|
|
272
|
+
{
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { ...this.#authHeaders(), "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ handle: signed.handle, checksum })
|
|
276
|
+
},
|
|
277
|
+
signal
|
|
278
|
+
);
|
|
279
|
+
const body = await confirmRes.json();
|
|
280
|
+
onProgress?.(100);
|
|
281
|
+
return this.#parseFileResult(body);
|
|
140
282
|
}
|
|
141
283
|
// ─── Auth headers ──────────────────────────────────────────────────────────
|
|
142
284
|
/**
|
|
@@ -164,6 +306,9 @@ var UploaderClient = class {
|
|
|
164
306
|
const form = new FormData();
|
|
165
307
|
form.append("file", file, filename ?? (file instanceof File ? file.name : "upload"));
|
|
166
308
|
if (filename) form.append("filename", filename);
|
|
309
|
+
const protection = this.#resolveProtection(opts);
|
|
310
|
+
if (protection.deliveryProtection) form.append("deliveryProtection", protection.deliveryProtection);
|
|
311
|
+
if (protection.allowedOrigins) form.append("allowedOrigins", JSON.stringify(protection.allowedOrigins));
|
|
167
312
|
const res = await fetchWithRetry(
|
|
168
313
|
`${this.apiUrl}/api/store`,
|
|
169
314
|
{
|
|
@@ -187,12 +332,13 @@ var UploaderClient = class {
|
|
|
187
332
|
const mime = file instanceof File ? file.type : "application/octet-stream";
|
|
188
333
|
onProgress?.(0);
|
|
189
334
|
checkAbort(signal);
|
|
335
|
+
const protection = this.#resolveProtection(opts);
|
|
190
336
|
const startRes = await fetchWithRetry(
|
|
191
337
|
`${this.apiUrl}/api/upload/start`,
|
|
192
338
|
{
|
|
193
339
|
method: "POST",
|
|
194
340
|
headers: { ...this.#authHeaders(), "Content-Type": "application/json" },
|
|
195
|
-
body: JSON.stringify({ filename: name, mimetype: mime, size: file.size })
|
|
341
|
+
body: JSON.stringify({ filename: name, mimetype: mime, size: file.size, ...protection })
|
|
196
342
|
},
|
|
197
343
|
signal
|
|
198
344
|
);
|
|
@@ -255,6 +401,10 @@ var UploaderClient = class {
|
|
|
255
401
|
* CLIENT_ERROR | INVALID_RESPONSE
|
|
256
402
|
*/
|
|
257
403
|
async upload(file, opts = {}) {
|
|
404
|
+
const caps = await this.#getCapabilities();
|
|
405
|
+
if (caps.directUpload && file.size <= MAX_DIRECT_PUT_BYTES) {
|
|
406
|
+
return this.#uploadDirect(file, opts);
|
|
407
|
+
}
|
|
258
408
|
const plan = planChunks(file, opts.chunkSize);
|
|
259
409
|
if (plan.mode === "single") {
|
|
260
410
|
return this.#uploadSingleShot(file, opts);
|
|
@@ -334,6 +484,10 @@ function transformUrl({ handle, ops, apiUrl = "" }) {
|
|
|
334
484
|
const chain = ops.map(serializeOp).join("/");
|
|
335
485
|
return chain ? `${base}/${chain}/${handle}` : `${base}/${handle}`;
|
|
336
486
|
}
|
|
487
|
+
function withSignedPolicy(url, security) {
|
|
488
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
489
|
+
return `${url}${sep}policy=${encodeURIComponent(security.policy)}&signature=${encodeURIComponent(security.signature)}`;
|
|
490
|
+
}
|
|
337
491
|
// Annotate the CommonJS export names for ESM import in node:
|
|
338
492
|
0 && (module.exports = {
|
|
339
493
|
DEFAULT_CHUNK_SIZE,
|
|
@@ -348,6 +502,7 @@ function transformUrl({ handle, ops, apiUrl = "" }) {
|
|
|
348
502
|
quality,
|
|
349
503
|
resize,
|
|
350
504
|
rotate,
|
|
351
|
-
transformUrl
|
|
505
|
+
transformUrl,
|
|
506
|
+
withSignedPolicy
|
|
352
507
|
});
|
|
353
508
|
//# sourceMappingURL=core.cjs.map
|