@dcd-sdk/storage-client 0.0.0-stage → 1.0.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/dist/index.js ADDED
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AuthenticationError: () => AuthenticationError,
34
+ BucketClient: () => BucketClient,
35
+ DCDStorageError: () => DCDStorageError,
36
+ MediaClient: () => MediaClient,
37
+ NotFoundError: () => NotFoundError,
38
+ PayloadTooLargeError: () => PayloadTooLargeError,
39
+ StorageClient: () => StorageClient,
40
+ TransformationBuilder: () => TransformationBuilder,
41
+ UploadError: () => UploadError,
42
+ createBucketClient: () => createBucketClient,
43
+ createMediaClient: () => createMediaClient,
44
+ createStorageClient: () => createStorageClient
45
+ });
46
+ module.exports = __toCommonJS(index_exports);
47
+
48
+ // src/builder.ts
49
+ var TransformationBuilder = class {
50
+ constructor(endpoint, publicId) {
51
+ this.options = {};
52
+ this.endpoint = endpoint.replace(/\/+$/, "");
53
+ this.publicId = publicId.replace(/^\/+/, "");
54
+ }
55
+ width(w) {
56
+ this.options.width = Math.round(w);
57
+ return this;
58
+ }
59
+ height(h) {
60
+ this.options.height = Math.round(h);
61
+ return this;
62
+ }
63
+ resize(params) {
64
+ if (params.width) this.options.width = Math.round(params.width);
65
+ if (params.height) this.options.height = Math.round(params.height);
66
+ if (params.crop) this.options.crop = params.crop;
67
+ return this;
68
+ }
69
+ crop(mode) {
70
+ this.options.crop = mode;
71
+ return this;
72
+ }
73
+ format(fmt) {
74
+ this.options.format = fmt;
75
+ return this;
76
+ }
77
+ quality(q) {
78
+ this.options.quality = q;
79
+ return this;
80
+ }
81
+ blur(sigma = 5) {
82
+ this.options.blur = sigma;
83
+ return this;
84
+ }
85
+ grayscale(enable = true) {
86
+ this.options.grayscale = enable;
87
+ return this;
88
+ }
89
+ sharpen(enable = true) {
90
+ this.options.sharpen = enable;
91
+ return this;
92
+ }
93
+ rotation(rot) {
94
+ this.options.rotation = rot;
95
+ return this;
96
+ }
97
+ /**
98
+ * Builds the formatted transformation string (e.g. w_400,h_300,c_fill,f_auto)
99
+ */
100
+ toParamsString() {
101
+ const parts = [];
102
+ if (this.options.width) parts.push(`w_${this.options.width}`);
103
+ if (this.options.height) parts.push(`h_${this.options.height}`);
104
+ if (this.options.crop) parts.push(`c_${this.options.crop}`);
105
+ if (this.options.format) parts.push(`f_${this.options.format}`);
106
+ if (this.options.quality) parts.push(`q_${this.options.quality}`);
107
+ if (this.options.blur) parts.push(`e_blur_${this.options.blur}`);
108
+ if (this.options.grayscale) parts.push("e_gray");
109
+ if (this.options.sharpen) parts.push("e_sharp");
110
+ if (this.options.rotation) parts.push(`r_${this.options.rotation}`);
111
+ return parts.length > 0 ? parts.join(",") : "original";
112
+ }
113
+ /**
114
+ * Returns the final full URL string
115
+ */
116
+ build() {
117
+ const params = this.toParamsString();
118
+ if (params === "original") {
119
+ return `${this.endpoint}/v1/image/${this.publicId}`;
120
+ }
121
+ return `${this.endpoint}/v1/image/${params}/${this.publicId}`;
122
+ }
123
+ toString() {
124
+ return this.build();
125
+ }
126
+ };
127
+
128
+ // src/errors.ts
129
+ var DCDStorageError = class extends Error {
130
+ constructor(message, status, code, details) {
131
+ super(message);
132
+ this.name = "DCDStorageError";
133
+ this.status = status;
134
+ this.code = code;
135
+ this.details = details;
136
+ Object.setPrototypeOf(this, new.target.prototype);
137
+ }
138
+ };
139
+ var AuthenticationError = class extends DCDStorageError {
140
+ constructor(message = "Authentication failed: Invalid or missing API key", status = 401, details) {
141
+ super(message, status, "AUTH_ERROR", details);
142
+ this.name = "AuthenticationError";
143
+ }
144
+ };
145
+ var NotFoundError = class extends DCDStorageError {
146
+ constructor(message = "Resource not found", status = 404, details) {
147
+ super(message, status, "NOT_FOUND", details);
148
+ this.name = "NotFoundError";
149
+ }
150
+ };
151
+ var PayloadTooLargeError = class extends DCDStorageError {
152
+ constructor(message = "File payload exceeds maximum permitted size", status = 413, details) {
153
+ super(message, status, "PAYLOAD_TOO_LARGE", details);
154
+ this.name = "PayloadTooLargeError";
155
+ }
156
+ };
157
+ var UploadError = class extends DCDStorageError {
158
+ constructor(message, status, details) {
159
+ super(message, status, "UPLOAD_ERROR", details);
160
+ this.name = "UploadError";
161
+ }
162
+ };
163
+
164
+ // src/client.ts
165
+ var StorageClient = class {
166
+ constructor(config) {
167
+ if (!config || !config.endpoint) {
168
+ throw new Error("StorageClient: endpoint is required");
169
+ }
170
+ this.endpoint = config.endpoint.replace(/\/+$/, "");
171
+ this.apiKey = (config.apiKey || "").trim();
172
+ this.defaultTimeoutMs = config.timeoutMs || 3e4;
173
+ }
174
+ /**
175
+ * Initializes a fluent URL builder for on-the-fly CDN image transformations
176
+ */
177
+ url(publicId) {
178
+ return new TransformationBuilder(this.endpoint, publicId);
179
+ }
180
+ /**
181
+ * Checks the health and connectivity of the storage engine server
182
+ */
183
+ async healthCheck() {
184
+ const healthUrl = `${this.endpoint}/health`;
185
+ try {
186
+ const response = await this.fetchWithTimeout(healthUrl, { method: "GET" });
187
+ if (!response.ok) {
188
+ return { status: "error" };
189
+ }
190
+ return await response.json();
191
+ } catch {
192
+ return { status: "error" };
193
+ }
194
+ }
195
+ /**
196
+ * Retrieves server performance, storage, and cache statistics (requires authorized API key)
197
+ */
198
+ async getStats() {
199
+ const statsUrl = `${this.endpoint}/v1/admin/stats`;
200
+ const response = await this.fetchWithTimeout(statsUrl, {
201
+ method: "GET",
202
+ headers: this.getAuthHeaders()
203
+ });
204
+ const data = await this.parseJsonResponse(response);
205
+ return data.stats;
206
+ }
207
+ /**
208
+ * Lists files in the storage cluster with pagination and optional folder filter
209
+ */
210
+ async listFiles(options = {}) {
211
+ const url = new URL(`${this.endpoint}/v1/admin/files`);
212
+ if (options.page) url.searchParams.set("page", String(options.page));
213
+ if (options.limit) url.searchParams.set("limit", String(options.limit));
214
+ if (options.folder) url.searchParams.set("folder", options.folder);
215
+ const response = await this.fetchWithTimeout(url.toString(), {
216
+ method: "GET",
217
+ headers: this.getAuthHeaders()
218
+ });
219
+ return await this.parseJsonResponse(response);
220
+ }
221
+ /**
222
+ * Generates a short-lived cryptographically signed upload token for safe client-side browser/mobile uploads.
223
+ * Keeps master API keys strictly on backend servers.
224
+ */
225
+ async createSignedUploadToken(options = {}) {
226
+ if (!this.apiKey) {
227
+ throw new AuthenticationError("API key is required on backend to generate signed upload tokens.");
228
+ }
229
+ const signUrl = `${this.endpoint}/v1/upload/sign`;
230
+ const response = await this.fetchWithTimeout(signUrl, {
231
+ method: "POST",
232
+ headers: {
233
+ "Content-Type": "application/json",
234
+ ...this.getAuthHeaders()
235
+ },
236
+ body: JSON.stringify(options)
237
+ });
238
+ return await this.parseJsonResponse(response);
239
+ }
240
+ /**
241
+ * Directly uploads a file to the storage server.
242
+ * Supports:
243
+ * - String file path (Node.js)
244
+ * - Buffer / Uint8Array / ArrayBuffer
245
+ * - File / Blob (Browser and universal runtimes)
246
+ * Auth:
247
+ * - Master API Key OR Client Signed Upload Token (dcd_sig_...)
248
+ */
249
+ async upload(file, options = {}) {
250
+ const uploadUrl = `${this.endpoint}/v1/upload`;
251
+ const formData = new FormData();
252
+ let fileToAppend = file;
253
+ let filename = options.filename;
254
+ if (typeof file === "string") {
255
+ if (typeof process !== "undefined" && process.versions?.node) {
256
+ try {
257
+ const fs = await import("fs");
258
+ const path = await import("path");
259
+ if (fs.existsSync(file)) {
260
+ const buf = fs.readFileSync(file);
261
+ filename = filename || path.basename(file);
262
+ fileToAppend = new Blob([new Uint8Array(buf)]);
263
+ }
264
+ } catch {
265
+ }
266
+ }
267
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(file)) {
268
+ fileToAppend = new Blob([new Uint8Array(file)]);
269
+ filename = filename || "upload.bin";
270
+ } else if (file instanceof ArrayBuffer) {
271
+ fileToAppend = new Blob([new Uint8Array(file)]);
272
+ filename = filename || "upload.bin";
273
+ } else if (ArrayBuffer.isView(file)) {
274
+ fileToAppend = new Blob([new Uint8Array(file.buffer, file.byteOffset, file.byteLength)]);
275
+ filename = filename || "upload.bin";
276
+ }
277
+ if (filename) {
278
+ formData.append("file", fileToAppend, filename);
279
+ } else {
280
+ formData.append("file", fileToAppend);
281
+ }
282
+ if (options.folder) formData.append("folder", options.folder);
283
+ if (options.publicId) formData.append("public_id", options.publicId);
284
+ if (options.overwrite !== void 0) formData.append("overwrite", String(options.overwrite));
285
+ if (options.uniqueFilename !== void 0) formData.append("unique_filename", String(options.uniqueFilename));
286
+ const headers = {};
287
+ if (options.token) {
288
+ headers["X-Upload-Token"] = options.token;
289
+ } else if (this.apiKey) {
290
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
291
+ } else {
292
+ throw new AuthenticationError("Either apiKey or a signed upload token must be provided to upload.");
293
+ }
294
+ if (typeof XMLHttpRequest !== "undefined" && options.onProgress) {
295
+ return new Promise((resolve, reject) => {
296
+ const xhr = new XMLHttpRequest();
297
+ xhr.open("POST", uploadUrl, true);
298
+ for (const [k, v] of Object.entries(headers)) {
299
+ xhr.setRequestHeader(k, v);
300
+ }
301
+ xhr.upload.onprogress = (e) => {
302
+ if (e.lengthComputable && options.onProgress) {
303
+ const percent = Math.round(e.loaded / e.total * 100);
304
+ options.onProgress(percent);
305
+ }
306
+ };
307
+ xhr.onload = () => {
308
+ let errData = null;
309
+ try {
310
+ const parsed = JSON.parse(xhr.responseText);
311
+ if (xhr.status >= 200 && xhr.status < 300) {
312
+ return resolve(parsed.asset || parsed);
313
+ }
314
+ errData = parsed;
315
+ } catch {
316
+ }
317
+ const msg = errData?.error?.message || `Upload failed with HTTP status ${xhr.status}`;
318
+ if (xhr.status === 401 || xhr.status === 403) {
319
+ return reject(new AuthenticationError(msg, xhr.status, errData));
320
+ }
321
+ if (xhr.status === 413) {
322
+ return reject(new PayloadTooLargeError(msg, xhr.status, errData));
323
+ }
324
+ reject(new UploadError(msg, xhr.status, errData));
325
+ };
326
+ xhr.onerror = () => reject(new UploadError("Network connection error during upload"));
327
+ xhr.ontimeout = () => reject(new UploadError("Upload request timed out"));
328
+ xhr.timeout = options.timeoutMs || this.defaultTimeoutMs;
329
+ xhr.send(formData);
330
+ });
331
+ }
332
+ const response = await this.fetchWithTimeout(
333
+ uploadUrl,
334
+ {
335
+ method: "POST",
336
+ headers,
337
+ body: formData
338
+ },
339
+ options.timeoutMs
340
+ );
341
+ const data = await this.parseJsonResponse(response);
342
+ return data.asset || data;
343
+ }
344
+ /**
345
+ * Helper method for frontend browser/mobile clients to upload using only a signed upload token (zero API key)
346
+ */
347
+ async uploadWithToken(file, token, options = {}) {
348
+ return this.upload(file, { ...options, token });
349
+ }
350
+ /**
351
+ * Permanently deletes a storage asset (requires API key with delete permissions)
352
+ */
353
+ async delete(publicId) {
354
+ const cleanPublicId = publicId.replace(/^\/+/, "");
355
+ const deleteUrl = `${this.endpoint}/v1/admin/files/${cleanPublicId}`;
356
+ const response = await this.fetchWithTimeout(deleteUrl, {
357
+ method: "DELETE",
358
+ headers: this.getAuthHeaders()
359
+ });
360
+ if (response.status === 404) {
361
+ throw new NotFoundError(`Asset '${cleanPublicId}' not found`);
362
+ }
363
+ if (response.status === 401 || response.status === 403) {
364
+ throw new AuthenticationError("Unauthorized to delete asset", response.status);
365
+ }
366
+ return response.ok;
367
+ }
368
+ // --- Private Helpers ---
369
+ getAuthHeaders() {
370
+ const headers = {};
371
+ if (this.apiKey) {
372
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
373
+ }
374
+ return headers;
375
+ }
376
+ async fetchWithTimeout(url, init, customTimeoutMs) {
377
+ const timeout = customTimeoutMs || this.defaultTimeoutMs;
378
+ const controller = new AbortController();
379
+ const id = setTimeout(() => controller.abort(), timeout);
380
+ try {
381
+ const response = await fetch(url, {
382
+ ...init,
383
+ signal: controller.signal
384
+ });
385
+ return response;
386
+ } catch (err) {
387
+ if (err.name === "AbortError") {
388
+ throw new DCDStorageError(`Request timed out after ${timeout}ms`, 408, "REQUEST_TIMEOUT");
389
+ }
390
+ throw new DCDStorageError(`Network error: ${err.message}`, void 0, "NETWORK_ERROR");
391
+ } finally {
392
+ clearTimeout(id);
393
+ }
394
+ }
395
+ async parseJsonResponse(response) {
396
+ let data = null;
397
+ try {
398
+ data = await response.json();
399
+ } catch {
400
+ throw new DCDStorageError(
401
+ `Failed to parse server response with HTTP status ${response.status}`,
402
+ response.status
403
+ );
404
+ }
405
+ if (!response.ok) {
406
+ const msg = data?.error?.message || `Storage request failed with status ${response.status}`;
407
+ if (response.status === 401 || response.status === 403) {
408
+ throw new AuthenticationError(msg, response.status, data);
409
+ }
410
+ if (response.status === 404) {
411
+ throw new NotFoundError(msg, response.status, data);
412
+ }
413
+ if (response.status === 413) {
414
+ throw new PayloadTooLargeError(msg, response.status, data);
415
+ }
416
+ throw new DCDStorageError(msg, response.status, data?.error?.code, data);
417
+ }
418
+ return data;
419
+ }
420
+ };
421
+ function createStorageClient(config) {
422
+ return new StorageClient(config);
423
+ }
424
+ var BucketClient = StorageClient;
425
+ var createBucketClient = createStorageClient;
426
+ var MediaClient = StorageClient;
427
+ var createMediaClient = createStorageClient;
428
+ // Annotate the CommonJS export names for ESM import in node:
429
+ 0 && (module.exports = {
430
+ AuthenticationError,
431
+ BucketClient,
432
+ DCDStorageError,
433
+ MediaClient,
434
+ NotFoundError,
435
+ PayloadTooLargeError,
436
+ StorageClient,
437
+ TransformationBuilder,
438
+ UploadError,
439
+ createBucketClient,
440
+ createMediaClient,
441
+ createStorageClient
442
+ });