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