@sanvika/cloudinary 0.1.2 → 0.1.4

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/client.js ADDED
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ // src/SanvikaCloudinaryProvider.jsx
4
+ import { createContext, useContext, useMemo } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var SanvikaCloudinaryContext = createContext(null);
7
+ function SanvikaCloudinaryProvider({ appName, cloudName, uploadPreset = "ml_default", children }) {
8
+ const value = useMemo(
9
+ () => ({ appName, cloudName, uploadPreset }),
10
+ [appName, cloudName, uploadPreset]
11
+ );
12
+ return /* @__PURE__ */ jsx(SanvikaCloudinaryContext.Provider, { value, children });
13
+ }
14
+ function useSanvikaCloudinary() {
15
+ const ctx = useContext(SanvikaCloudinaryContext);
16
+ if (!ctx) {
17
+ throw new Error("useSanvikaCloudinary must be used within a SanvikaCloudinaryProvider");
18
+ }
19
+ return ctx;
20
+ }
21
+
22
+ // src/useCloudinaryUpload.js
23
+ import { useState, useCallback } from "react";
24
+ function useCloudinaryUpload(options = {}) {
25
+ const { folder, resourceType = "image", onProgress } = options;
26
+ const { appName, cloudName, uploadPreset } = useSanvikaCloudinary();
27
+ const [uploading, setUploading] = useState(false);
28
+ const [error, setError] = useState(null);
29
+ const reset = useCallback(() => {
30
+ setError(null);
31
+ setUploading(false);
32
+ }, []);
33
+ const upload = useCallback(
34
+ async (file) => {
35
+ setError(null);
36
+ setUploading(true);
37
+ try {
38
+ const folderPath = folder ? `${appName}/${folder}` : appName;
39
+ const formData = new FormData();
40
+ formData.append("file", file);
41
+ formData.append("upload_preset", uploadPreset);
42
+ formData.append("folder", folderPath);
43
+ const xhr = new XMLHttpRequest();
44
+ const url = `https://api.cloudinary.com/v1_1/${cloudName}/${resourceType}/upload`;
45
+ const result = await new Promise((resolve, reject) => {
46
+ xhr.open("POST", url);
47
+ if (onProgress) {
48
+ xhr.upload.onprogress = (e) => {
49
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
50
+ };
51
+ }
52
+ xhr.onload = () => {
53
+ if (xhr.status >= 200 && xhr.status < 300) {
54
+ resolve(JSON.parse(xhr.responseText));
55
+ } else {
56
+ reject(new Error(xhr.responseText || "Upload failed"));
57
+ }
58
+ };
59
+ xhr.onerror = () => reject(new Error("Network error during upload"));
60
+ xhr.send(formData);
61
+ });
62
+ return result;
63
+ } catch (err) {
64
+ const msg = (err == null ? void 0 : err.message) || "Upload failed";
65
+ setError(msg);
66
+ throw err;
67
+ } finally {
68
+ setUploading(false);
69
+ }
70
+ },
71
+ [appName, cloudName, uploadPreset, folder, resourceType, onProgress]
72
+ );
73
+ return { upload, uploading, error, reset };
74
+ }
75
+
76
+ // src/cloudinaryUtils.js
77
+ function isCloudinaryUrl(url) {
78
+ return typeof url === "string" && (url.includes("res.cloudinary.com") || url.includes("cloudinary.com"));
79
+ }
80
+ function extractPublicId(url) {
81
+ if (!isCloudinaryUrl(url)) return null;
82
+ try {
83
+ const parts = url.split("/");
84
+ const uploadIdx = parts.indexOf("upload");
85
+ if (uploadIdx === -1) return null;
86
+ let publicIdPath = parts.slice(uploadIdx + 1).join("/");
87
+ const versionMatch = publicIdPath.match(/^v\d+\//);
88
+ if (versionMatch) {
89
+ publicIdPath = publicIdPath.substring(versionMatch[0].length);
90
+ }
91
+ const lastDot = publicIdPath.lastIndexOf(".");
92
+ if (lastDot !== -1) {
93
+ const ext = publicIdPath.substring(lastDot + 1);
94
+ if (/^[a-zA-Z0-9]{2,5}$/.test(ext)) {
95
+ publicIdPath = publicIdPath.substring(0, lastDot);
96
+ }
97
+ }
98
+ return publicIdPath || null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function validatePublicId(publicId) {
104
+ if (!publicId || typeof publicId !== "string") return false;
105
+ const parts = publicId.split("/");
106
+ return parts.length >= 2 && parts.every((p) => p.length > 0);
107
+ }
108
+ function getFolderPath(appName, subfolder) {
109
+ if (!appName) throw new Error("appName is required for getFolderPath");
110
+ const parts = [appName];
111
+ if (subfolder) {
112
+ const cleaned = subfolder.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).join("/");
113
+ if (cleaned) parts.push(cleaned);
114
+ }
115
+ return parts.join("/");
116
+ }
117
+ function getOptimizedUrl(cloudName, publicId, transforms = {}, resourceType = "image") {
118
+ const base = `https://res.cloudinary.com/${cloudName}/${resourceType}/upload`;
119
+ const parts = Object.entries(transforms).map(([k, v]) => `${k}_${v}`).join(",");
120
+ return parts ? `${base}/${parts}/${publicId}` : `${base}/${publicId}`;
121
+ }
122
+ var TRANSFORM_PRESETS = {
123
+ thumbnail: { w: 150, h: 150, c: "fill", g: "auto", q: "auto", f: "auto" },
124
+ adCard: { w: 300, h: 200, c: "fill", g: "auto", q: "auto", f: "auto" },
125
+ profilePicture: { w: 100, h: 100, c: "fill", g: "face", r: "max", q: "auto", f: "auto" },
126
+ adDetail: { w: 800, h: 600, c: "fill", q: "auto", f: "auto" },
127
+ responsive: { w: "auto", dpr: "auto", q: "auto", f: "auto" }
128
+ };
129
+ export {
130
+ SanvikaCloudinaryProvider,
131
+ TRANSFORM_PRESETS,
132
+ extractPublicId,
133
+ getFolderPath,
134
+ getOptimizedUrl,
135
+ isCloudinaryUrl,
136
+ useCloudinaryUpload,
137
+ useSanvikaCloudinary,
138
+ validatePublicId
139
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  // src/cloudinaryCore.js
4
2
  import { v2 as cloudinary } from "cloudinary";
5
3
 
@@ -148,10 +146,17 @@ async function uploadImage(fileOrBuffer, options = {}) {
148
146
  folder,
149
147
  resourceType = "image",
150
148
  tags = [],
149
+ eager,
151
150
  transforms,
151
+ eagerAsync,
152
+ eagerNotificationUrl,
152
153
  publicId,
153
154
  overwrite = false,
154
- notificationUrl
155
+ notificationUrl,
156
+ chunked,
157
+ chunkSize = 6 * 1024 * 1024,
158
+ timeout,
159
+ maxAttempts = 3
155
160
  } = options;
156
161
  const uploadFolder = getFolderPath(_appName, folder);
157
162
  const uploadOpts = {
@@ -161,22 +166,40 @@ async function uploadImage(fileOrBuffer, options = {}) {
161
166
  tags: [_appName, ...tags]
162
167
  };
163
168
  if (publicId) uploadOpts.public_id = publicId;
164
- if (transforms) uploadOpts.eager = transforms;
169
+ const eagerValue = eager || transforms;
170
+ if (eagerValue) uploadOpts.eager = eagerValue;
171
+ if (eagerAsync) uploadOpts.eager_async = true;
172
+ if (eagerNotificationUrl) uploadOpts.eager_notification_url = eagerNotificationUrl;
165
173
  if (notificationUrl) uploadOpts.notification_url = notificationUrl;
174
+ if (timeout) uploadOpts.timeout = timeout;
175
+ const fileSize = Buffer.isBuffer(fileOrBuffer) ? fileOrBuffer.length : null;
176
+ const useChunked = chunked || resourceType === "video" && fileSize !== null && fileSize > 10 * 1024 * 1024;
177
+ if (useChunked) {
178
+ uploadOpts.chunk_size = chunkSize;
179
+ }
166
180
  return withRetry(
167
181
  () => {
168
182
  if (Buffer.isBuffer(fileOrBuffer)) {
169
183
  return new Promise((resolve, reject) => {
170
- const stream = cloudinary.uploader.upload_stream(uploadOpts, (err, result) => {
184
+ const streamFactory = useChunked ? cloudinary.uploader.upload_chunked_stream : cloudinary.uploader.upload_stream;
185
+ const stream = streamFactory.call(cloudinary.uploader, uploadOpts, (err, result) => {
171
186
  if (err) return reject(err);
172
187
  resolve(result);
173
188
  });
174
189
  stream.end(fileOrBuffer);
175
190
  });
176
191
  }
192
+ if (useChunked) {
193
+ return new Promise((resolve, reject) => {
194
+ cloudinary.uploader.upload_large(fileOrBuffer, uploadOpts, (err, result) => {
195
+ if (err) return reject(err);
196
+ resolve(result);
197
+ });
198
+ });
199
+ }
177
200
  return cloudinary.uploader.upload(fileOrBuffer, uploadOpts);
178
201
  },
179
- { operationName: "uploadImage", maxAttempts: 3 }
202
+ { operationName: "uploadImage", maxAttempts }
180
203
  );
181
204
  }
182
205
  async function uploadVideo(fileOrBuffer, options = {}) {
@@ -255,99 +278,115 @@ async function pingCloudinary() {
255
278
  ensureConfigured();
256
279
  return cloudinary.api.ping();
257
280
  }
258
-
259
- // src/SanvikaCloudinaryProvider.jsx
260
- import { createContext, useContext, useMemo } from "react";
261
- import { jsx } from "react/jsx-runtime";
262
- var SanvikaCloudinaryContext = createContext(null);
263
- function SanvikaCloudinaryProvider({ appName, cloudName, uploadPreset = "ml_default", children }) {
264
- const value = useMemo(
265
- () => ({ appName, cloudName, uploadPreset }),
266
- [appName, cloudName, uploadPreset]
267
- );
268
- return /* @__PURE__ */ jsx(SanvikaCloudinaryContext.Provider, { value, children });
281
+ async function getCloudinaryUsage(options = {}) {
282
+ ensureConfigured();
283
+ const { resourceType } = options;
284
+ return cloudinary.api.usage(resourceType ? { resource_type: resourceType } : {});
269
285
  }
270
- function useSanvikaCloudinary() {
271
- const ctx = useContext(SanvikaCloudinaryContext);
272
- if (!ctx) {
273
- throw new Error("useSanvikaCloudinary must be used within a SanvikaCloudinaryProvider");
274
- }
275
- return ctx;
286
+ function getCloudinarySdkVersion() {
287
+ return cloudinary.CLOUDINARY_VERSION;
276
288
  }
277
289
 
278
- // src/useCloudinaryUpload.js
279
- import { useState, useCallback } from "react";
280
- function useCloudinaryUpload(options = {}) {
281
- const { folder, resourceType = "image", onProgress } = options;
282
- const { appName, cloudName, uploadPreset } = useSanvikaCloudinary();
283
- const [uploading, setUploading] = useState(false);
284
- const [error, setError] = useState(null);
285
- const reset = useCallback(() => {
286
- setError(null);
287
- setUploading(false);
288
- }, []);
289
- const upload = useCallback(
290
- async (file) => {
291
- setError(null);
292
- setUploading(true);
293
- try {
294
- const folderPath = folder ? `${appName}/${folder}` : appName;
295
- const formData = new FormData();
296
- formData.append("file", file);
297
- formData.append("upload_preset", uploadPreset);
298
- formData.append("folder", folderPath);
299
- const xhr = new XMLHttpRequest();
300
- const url = `https://api.cloudinary.com/v1_1/${cloudName}/${resourceType}/upload`;
301
- const result = await new Promise((resolve, reject) => {
302
- xhr.open("POST", url);
303
- if (onProgress) {
304
- xhr.upload.onprogress = (e) => {
305
- if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
306
- };
307
- }
308
- xhr.onload = () => {
309
- if (xhr.status >= 200 && xhr.status < 300) {
310
- resolve(JSON.parse(xhr.responseText));
311
- } else {
312
- reject(new Error(xhr.responseText || "Upload failed"));
313
- }
314
- };
315
- xhr.onerror = () => reject(new Error("Network error during upload"));
316
- xhr.send(formData);
317
- });
318
- return result;
319
- } catch (err) {
320
- const msg = (err == null ? void 0 : err.message) || "Upload failed";
321
- setError(msg);
322
- throw err;
323
- } finally {
324
- setUploading(false);
325
- }
290
+ // src/cloudinaryDiagnostics.js
291
+ import crypto from "crypto";
292
+ function testCloudinaryWebhookSignature(apiSecret, options = {}) {
293
+ try {
294
+ if (!apiSecret) {
295
+ return { success: false, error: "apiSecret is required" };
296
+ }
297
+ const testPayload = { test: "data", timestamp: Math.floor(Date.now() / 1e3) };
298
+ const testPayloadString = JSON.stringify(testPayload);
299
+ const testTimestamp = testPayload.timestamp.toString();
300
+ const signatureData = testTimestamp + testPayloadString;
301
+ const signature = crypto.createHmac("sha1", apiSecret).update(signatureData).digest("hex");
302
+ return {
303
+ success: true,
304
+ testPayload,
305
+ testTimestamp,
306
+ signature,
307
+ headers: {
308
+ "X-Cld-Timestamp": testTimestamp,
309
+ "X-Cld-Signature": signature
310
+ },
311
+ testEndpoint: options.testEndpoint || "/api/webhooks/cloudinary/debug"
312
+ };
313
+ } catch (error) {
314
+ return { success: false, error: error.message };
315
+ }
316
+ }
317
+ async function runCloudinaryDiagnostics(options = {}) {
318
+ const {
319
+ testWebhook = false,
320
+ webhookSecret,
321
+ testFolder = "diagnostics"
322
+ } = options;
323
+ const report = {
324
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
325
+ cloudinaryVersion: getCloudinarySdkVersion(),
326
+ nodeVersion: process.version,
327
+ configuration: {
328
+ cloudName: process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME ? "set" : "missing",
329
+ apiKey: process.env.CLOUDINARY_API_KEY ? "set" : "missing",
330
+ apiSecret: process.env.CLOUDINARY_API_SECRET ? "set" : "missing",
331
+ uploadPreset: process.env.CLOUDINARY_UPLOAD_PRESET ? "set" : "missing"
326
332
  },
327
- [appName, cloudName, uploadPreset, folder, resourceType, onProgress]
333
+ tests: {}
334
+ };
335
+ report.tests.ping = await pingCloudinary().then(
336
+ (result) => ({ success: true, result }),
337
+ (error) => ({ success: false, error: error.message })
338
+ );
339
+ const testBuffer = Buffer.from("Cloudinary diagnostic test");
340
+ report.tests.upload = await uploadImage(testBuffer, {
341
+ folder: testFolder,
342
+ resourceType: "raw",
343
+ tags: ["test", "diagnostics"],
344
+ publicId: `diagnostic_${Date.now()}`,
345
+ overwrite: true
346
+ }).then(
347
+ (result) => ({
348
+ success: true,
349
+ publicId: result.public_id,
350
+ url: result.secure_url,
351
+ bytes: result.bytes
352
+ }),
353
+ (error) => ({
354
+ success: false,
355
+ error: error.message,
356
+ httpCode: error.http_code,
357
+ name: error.name
358
+ })
359
+ );
360
+ report.tests.videoLimits = await getCloudinaryUsage({ resourceType: "video" }).then(
361
+ (result) => ({ success: true, result }),
362
+ (error) => ({ success: false, error: error.message })
328
363
  );
329
- return { upload, uploading, error, reset };
364
+ if (testWebhook) {
365
+ report.tests.webhook = testCloudinaryWebhookSignature(webhookSecret);
366
+ }
367
+ return report;
330
368
  }
331
369
  export {
332
370
  CloudinaryError,
333
- SanvikaCloudinaryProvider,
334
371
  TRANSFORM_PRESETS,
335
372
  configureSanvikaCloudinary,
336
373
  deleteImage,
337
374
  deleteImages,
338
375
  extractPublicId,
339
376
  getAppName,
377
+ getCloudinarySdkVersion,
378
+ getCloudinaryUsage,
340
379
  getFolderPath,
341
380
  getOptimizedUrl,
342
381
  isCloudinaryUrl,
343
382
  isRetriableError,
344
383
  pingCloudinary,
384
+ runCloudinaryDiagnostics,
385
+ testCloudinaryWebhookSignature,
345
386
  uploadImage,
346
387
  uploadImages,
347
388
  uploadRawFile,
348
389
  uploadVideo,
349
- useCloudinaryUpload,
350
- useSanvikaCloudinary,
351
390
  validatePublicId,
352
391
  withRetry
353
392
  };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@sanvika/cloudinary",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Centralized Cloudinary SDK for the Sanvika ecosystem — upload, delete, transform, and manage media across 50+ projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "exports": {
9
- ".": "./dist/index.js"
9
+ ".": "./dist/index.js",
10
+ "./client": "./dist/client.js"
10
11
  },
11
12
  "files": [
12
13
  "dist"
@@ -36,8 +37,12 @@
36
37
  "react-dom": ">=18.0.0"
37
38
  },
38
39
  "peerDependenciesMeta": {
39
- "react": { "optional": true },
40
- "react-dom": { "optional": true }
40
+ "react": {
41
+ "optional": true
42
+ },
43
+ "react-dom": {
44
+ "optional": true
45
+ }
41
46
  },
42
47
  "engines": {
43
48
  "node": ">=18.0.0"