@sanvika/cloudinary 0.1.3 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +297 -29
  2. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- // src/cloudinaryCore.js
2
- import { v2 as cloudinary } from "cloudinary";
3
-
4
1
  // src/cloudinaryErrors.js
5
2
  var CloudinaryError = class extends Error {
6
3
  constructor(message, operation, details = {}, originalError = null) {
@@ -111,46 +108,166 @@ var TRANSFORM_PRESETS = {
111
108
  responsive: { w: "auto", dpr: "auto", q: "auto", f: "auto" }
112
109
  };
113
110
 
111
+ // src/gatewayClient.js
112
+ function isProxyMode() {
113
+ return Boolean(process.env.SANVIKA_CLOUDINARY_GATEWAY_URL && process.env.SANVIKA_CLOUDINARY_TOKEN);
114
+ }
115
+ function requireProxyEnv() {
116
+ const base = process.env.SANVIKA_CLOUDINARY_GATEWAY_URL;
117
+ const token = process.env.SANVIKA_CLOUDINARY_TOKEN;
118
+ if (!base || !token) {
119
+ throw new CloudinaryError(
120
+ "Proxy gateway env missing. Set SANVIKA_CLOUDINARY_GATEWAY_URL and SANVIKA_CLOUDINARY_TOKEN.",
121
+ "gateway_env"
122
+ );
123
+ }
124
+ return { base: base.replace(/\/+$/, ""), token };
125
+ }
126
+ async function handleResponse(operation, res) {
127
+ let body = null;
128
+ const text = await res.text();
129
+ try {
130
+ body = text ? JSON.parse(text) : null;
131
+ } catch {
132
+ body = { raw: text };
133
+ }
134
+ if (!res.ok) {
135
+ const msg = (body == null ? void 0 : body.error) || (body == null ? void 0 : body.message) || `Gateway returned ${res.status}`;
136
+ throw new CloudinaryError(msg, operation, { status: res.status, body });
137
+ }
138
+ return body;
139
+ }
140
+ async function gatewayPost(pathname, jsonBody) {
141
+ const { base, token } = requireProxyEnv();
142
+ const res = await fetch(`${base}${pathname}`, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${token}`,
146
+ "Content-Type": "application/json"
147
+ },
148
+ body: JSON.stringify(jsonBody || {})
149
+ });
150
+ return handleResponse(`POST ${pathname}`, res);
151
+ }
152
+ async function gatewayGet(pathname, query) {
153
+ const { base, token } = requireProxyEnv();
154
+ const qs = query ? `?${new URLSearchParams(query).toString()}` : "";
155
+ const res = await fetch(`${base}${pathname}${qs}`, {
156
+ method: "GET",
157
+ headers: { Authorization: `Bearer ${token}` }
158
+ });
159
+ return handleResponse(`GET ${pathname}`, res);
160
+ }
161
+ async function gatewayUpload(bufferOrBlob, options = {}) {
162
+ const { base, token } = requireProxyEnv();
163
+ const form = new FormData();
164
+ let blob;
165
+ if (bufferOrBlob instanceof Blob) {
166
+ blob = bufferOrBlob;
167
+ } else if (Buffer.isBuffer(bufferOrBlob)) {
168
+ blob = new Blob([bufferOrBlob]);
169
+ } else if (typeof bufferOrBlob === "string") {
170
+ return gatewayPost("/api/v1/upload?mode=source", { source: bufferOrBlob, options });
171
+ } else {
172
+ throw new CloudinaryError("Unsupported upload input", "upload", { type: typeof bufferOrBlob });
173
+ }
174
+ form.append("file", blob, options.filename || "asset");
175
+ form.append("options", JSON.stringify(options || {}));
176
+ const res = await fetch(`${base}/api/v1/upload`, {
177
+ method: "POST",
178
+ headers: { Authorization: `Bearer ${token}` },
179
+ body: form
180
+ });
181
+ return handleResponse("upload", res);
182
+ }
183
+
114
184
  // src/cloudinaryCore.js
115
185
  var _appName = null;
116
186
  var _configured = false;
117
- function configureSanvikaCloudinary({ appName, cloudName, apiKey, apiSecret } = {}) {
118
- if (!appName) throw new Error("appName is required for configureSanvikaCloudinary");
187
+ var _cloudinary = null;
188
+ async function loadLegacyCloudinary() {
189
+ if (_cloudinary) return _cloudinary;
190
+ const mod = await import("cloudinary");
191
+ _cloudinary = mod.v2;
192
+ return _cloudinary;
193
+ }
194
+ async function configureSanvikaCloudinary({ appName, cloudName, apiKey, apiSecret } = {}) {
195
+ const resolvedApp = appName || process.env.SANVIKA_CLOUDINARY_APP_NAME;
196
+ if (!resolvedApp) throw new Error("appName is required for configureSanvikaCloudinary");
197
+ _appName = resolvedApp;
198
+ if (isProxyMode()) {
199
+ _configured = true;
200
+ return;
201
+ }
119
202
  const cn = cloudName || process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME;
120
203
  const ak = apiKey || process.env.CLOUDINARY_API_KEY;
121
204
  const as = apiSecret || process.env.CLOUDINARY_API_SECRET;
122
205
  if (!cn || !ak || !as) {
123
- throw new Error("Cloudinary credentials missing. Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET in env.");
206
+ throw new Error(
207
+ "Cloudinary credentials missing. Either use proxy mode (SANVIKA_CLOUDINARY_GATEWAY_URL + SANVIKA_CLOUDINARY_TOKEN) or legacy env (CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET)."
208
+ );
124
209
  }
210
+ const cloudinary = await loadLegacyCloudinary();
125
211
  cloudinary.config({ cloud_name: cn, api_key: ak, api_secret: as, secure: true });
126
- _appName = appName;
127
212
  _configured = true;
128
213
  }
129
- function ensureConfigured() {
130
- if (!_configured) {
131
- const envApp = process.env.SANVIKA_CLOUDINARY_APP_NAME;
132
- if (envApp) {
133
- configureSanvikaCloudinary({ appName: envApp });
134
- } else {
135
- throw new Error("Call configureSanvikaCloudinary({ appName }) before using SDK operations.");
136
- }
214
+ async function ensureConfigured() {
215
+ if (_configured) return;
216
+ if (isProxyMode()) {
217
+ _appName = _appName || process.env.SANVIKA_CLOUDINARY_APP_NAME || "sanvika-app";
218
+ _configured = true;
219
+ return;
137
220
  }
221
+ if (process.env.SANVIKA_CLOUDINARY_APP_NAME) {
222
+ await configureSanvikaCloudinary({ appName: process.env.SANVIKA_CLOUDINARY_APP_NAME });
223
+ return;
224
+ }
225
+ throw new Error("Call configureSanvikaCloudinary({ appName }) before using SDK operations.");
138
226
  }
139
- function getAppName() {
140
- ensureConfigured();
227
+ async function getAppName() {
228
+ await ensureConfigured();
141
229
  return _appName;
142
230
  }
143
231
  async function uploadImage(fileOrBuffer, options = {}) {
144
- ensureConfigured();
232
+ await ensureConfigured();
145
233
  const {
146
234
  folder,
147
235
  resourceType = "image",
148
236
  tags = [],
237
+ eager,
149
238
  transforms,
239
+ eagerAsync,
240
+ eagerNotificationUrl,
150
241
  publicId,
151
242
  overwrite = false,
152
- notificationUrl
243
+ notificationUrl,
244
+ chunked,
245
+ chunkSize = 6 * 1024 * 1024,
246
+ timeout,
247
+ maxAttempts = 3
153
248
  } = options;
249
+ const eagerValue = eager || transforms;
250
+ const uploadOptions = {
251
+ folder: folder || "",
252
+ resourceType,
253
+ tags: [_appName, ...tags],
254
+ eager: eagerValue,
255
+ eagerAsync,
256
+ eagerNotificationUrl,
257
+ publicId,
258
+ overwrite,
259
+ notificationUrl,
260
+ chunked,
261
+ chunkSize,
262
+ timeout
263
+ };
264
+ if (isProxyMode()) {
265
+ return withRetry(() => gatewayUpload(fileOrBuffer, uploadOptions), {
266
+ operationName: "uploadImage",
267
+ maxAttempts
268
+ });
269
+ }
270
+ const cloudinary = await loadLegacyCloudinary();
154
271
  const uploadFolder = getFolderPath(_appName, folder);
155
272
  const uploadOpts = {
156
273
  folder: uploadFolder,
@@ -159,22 +276,37 @@ async function uploadImage(fileOrBuffer, options = {}) {
159
276
  tags: [_appName, ...tags]
160
277
  };
161
278
  if (publicId) uploadOpts.public_id = publicId;
162
- if (transforms) uploadOpts.eager = transforms;
279
+ if (eagerValue) uploadOpts.eager = eagerValue;
280
+ if (eagerAsync) uploadOpts.eager_async = true;
281
+ if (eagerNotificationUrl) uploadOpts.eager_notification_url = eagerNotificationUrl;
163
282
  if (notificationUrl) uploadOpts.notification_url = notificationUrl;
283
+ if (timeout) uploadOpts.timeout = timeout;
284
+ const fileSize = Buffer.isBuffer(fileOrBuffer) ? fileOrBuffer.length : null;
285
+ const useChunked = chunked || resourceType === "video" && fileSize !== null && fileSize > 10 * 1024 * 1024;
286
+ if (useChunked) uploadOpts.chunk_size = chunkSize;
164
287
  return withRetry(
165
288
  () => {
166
289
  if (Buffer.isBuffer(fileOrBuffer)) {
167
290
  return new Promise((resolve, reject) => {
168
- const stream = cloudinary.uploader.upload_stream(uploadOpts, (err, result) => {
291
+ const streamFactory = useChunked ? cloudinary.uploader.upload_chunked_stream : cloudinary.uploader.upload_stream;
292
+ const stream = streamFactory.call(cloudinary.uploader, uploadOpts, (err, result) => {
169
293
  if (err) return reject(err);
170
294
  resolve(result);
171
295
  });
172
296
  stream.end(fileOrBuffer);
173
297
  });
174
298
  }
299
+ if (useChunked) {
300
+ return new Promise((resolve, reject) => {
301
+ cloudinary.uploader.upload_large(fileOrBuffer, uploadOpts, (err, result) => {
302
+ if (err) return reject(err);
303
+ resolve(result);
304
+ });
305
+ });
306
+ }
175
307
  return cloudinary.uploader.upload(fileOrBuffer, uploadOpts);
176
308
  },
177
- { operationName: "uploadImage", maxAttempts: 3 }
309
+ { operationName: "uploadImage", maxAttempts }
178
310
  );
179
311
  }
180
312
  async function uploadVideo(fileOrBuffer, options = {}) {
@@ -185,11 +317,18 @@ async function uploadImages(files, options = {}) {
185
317
  return Promise.all(files.map((file) => uploadImage(file, options)));
186
318
  }
187
319
  async function uploadRawFile(buffer, options = {}) {
188
- ensureConfigured();
320
+ await ensureConfigured();
189
321
  const { folder, filename, tags = [] } = options;
190
322
  if (!buffer || !Buffer.isBuffer(buffer)) throw new Error("Buffer is required for uploadRawFile");
191
- const uploadFolder = getFolderPath(_appName, folder);
192
323
  const baseName = filename ? filename.replace(/\.[^.]+$/, "") : "file";
324
+ if (isProxyMode()) {
325
+ return withRetry(
326
+ () => gatewayUpload(buffer, { folder: folder || "", resourceType: "raw", tags: ["raw", _appName, ...tags], publicId: baseName, overwrite: true, filename }),
327
+ { operationName: "uploadRawFile", maxAttempts: 3 }
328
+ );
329
+ }
330
+ const cloudinary = await loadLegacyCloudinary();
331
+ const uploadFolder = getFolderPath(_appName, folder);
193
332
  return withRetry(
194
333
  () => new Promise((resolve, reject) => {
195
334
  const stream = cloudinary.uploader.upload_stream(
@@ -211,8 +350,14 @@ async function uploadRawFile(buffer, options = {}) {
211
350
  );
212
351
  }
213
352
  async function deleteImage(urlOrPublicId, options = {}) {
214
- ensureConfigured();
353
+ var _a;
354
+ await ensureConfigured();
215
355
  const { resourceType = "image" } = options;
356
+ if (isProxyMode()) {
357
+ const result = await gatewayPost("/api/v1/delete", { urls: [urlOrPublicId], options: { resourceType } });
358
+ return ((_a = result == null ? void 0 : result.results) == null ? void 0 : _a[0]) || result;
359
+ }
360
+ const cloudinary = await loadLegacyCloudinary();
216
361
  const publicId = isCloudinaryUrl(urlOrPublicId) ? extractPublicId(urlOrPublicId) : urlOrPublicId;
217
362
  if (!publicId) throw new CloudinaryError("Cannot extract public ID", "deleteImage", { urlOrPublicId });
218
363
  return withRetry(
@@ -221,14 +366,22 @@ async function deleteImage(urlOrPublicId, options = {}) {
221
366
  );
222
367
  }
223
368
  async function deleteImages(urls, options = {}) {
224
- ensureConfigured();
369
+ await ensureConfigured();
225
370
  const { fast = false, resourceType = "image" } = options;
226
- if (!Array.isArray(urls) || urls.length === 0) return { success: true, total: 0, successful: 0, failed: 0 };
371
+ if (!Array.isArray(urls) || urls.length === 0) {
372
+ return { success: true, total: 0, successful: 0, failed: 0 };
373
+ }
374
+ if (isProxyMode()) {
375
+ return gatewayPost("/api/v1/delete", { urls, options: { fast, resourceType } });
376
+ }
377
+ const cloudinary = await loadLegacyCloudinary();
227
378
  const BATCH_SIZE = fast ? 15 : 10;
228
379
  const DELAY = fast ? 100 : 500;
229
380
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
230
381
  const publicIds = urls.map((u) => isCloudinaryUrl(u) ? extractPublicId(u) : u).filter(Boolean);
231
- if (publicIds.length === 0) return { success: true, total: urls.length, successful: 0, failed: 0, invalidUrls: urls.length };
382
+ if (publicIds.length === 0) {
383
+ return { success: true, total: urls.length, successful: 0, failed: 0, invalidUrls: urls.length };
384
+ }
232
385
  const allResults = [];
233
386
  for (let i = 0; i < publicIds.length; i += BATCH_SIZE) {
234
387
  const batch = publicIds.slice(i, i + BATCH_SIZE);
@@ -250,9 +403,115 @@ async function deleteImages(urls, options = {}) {
250
403
  return { success: failed === 0, total: urls.length, processed: allResults.length, successful, failed };
251
404
  }
252
405
  async function pingCloudinary() {
253
- ensureConfigured();
406
+ await ensureConfigured();
407
+ if (isProxyMode()) return gatewayGet("/api/v1/ping");
408
+ const cloudinary = await loadLegacyCloudinary();
254
409
  return cloudinary.api.ping();
255
410
  }
411
+ async function getCloudinaryUsage(options = {}) {
412
+ await ensureConfigured();
413
+ const { resourceType } = options;
414
+ if (isProxyMode()) return gatewayGet("/api/v1/usage", resourceType ? { resourceType } : void 0);
415
+ const cloudinary = await loadLegacyCloudinary();
416
+ return cloudinary.api.usage(resourceType ? { resource_type: resourceType } : {});
417
+ }
418
+ async function getCloudinarySdkVersion() {
419
+ if (isProxyMode()) return "gateway";
420
+ const cloudinary = await loadLegacyCloudinary();
421
+ return cloudinary.CLOUDINARY_VERSION;
422
+ }
423
+
424
+ // src/cloudinaryDiagnostics.js
425
+ import crypto from "crypto";
426
+ function testCloudinaryWebhookSignature(apiSecret, options = {}) {
427
+ try {
428
+ if (!apiSecret) return { success: false, error: "apiSecret is required" };
429
+ const testPayload = { test: "data", timestamp: Math.floor(Date.now() / 1e3) };
430
+ const testPayloadString = JSON.stringify(testPayload);
431
+ const testTimestamp = testPayload.timestamp.toString();
432
+ const signatureData = testTimestamp + testPayloadString;
433
+ const signature = crypto.createHmac("sha1", apiSecret).update(signatureData).digest("hex");
434
+ return {
435
+ success: true,
436
+ testPayload,
437
+ testTimestamp,
438
+ signature,
439
+ headers: { "X-Cld-Timestamp": testTimestamp, "X-Cld-Signature": signature },
440
+ testEndpoint: options.testEndpoint || "/api/webhooks/cloudinary/debug"
441
+ };
442
+ } catch (error) {
443
+ return { success: false, error: error.message };
444
+ }
445
+ }
446
+ async function runCloudinaryDiagnostics(options = {}) {
447
+ if (isProxyMode()) {
448
+ return gatewayPost("/api/v1/diagnostics", options || {});
449
+ }
450
+ const { testWebhook = false, webhookSecret, testFolder = "diagnostics" } = options;
451
+ const report = {
452
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
453
+ cloudinaryVersion: await getCloudinarySdkVersion(),
454
+ nodeVersion: process.version,
455
+ mode: "legacy",
456
+ configuration: {
457
+ cloudName: process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME ? "set" : "missing",
458
+ apiKey: process.env.CLOUDINARY_API_KEY ? "set" : "missing",
459
+ apiSecret: process.env.CLOUDINARY_API_SECRET ? "set" : "missing",
460
+ uploadPreset: process.env.CLOUDINARY_UPLOAD_PRESET ? "set" : "missing"
461
+ },
462
+ tests: {}
463
+ };
464
+ report.tests.ping = await pingCloudinary().then(
465
+ (result) => ({ success: true, result }),
466
+ (error) => ({ success: false, error: error.message })
467
+ );
468
+ const testBuffer = Buffer.from("Cloudinary diagnostic test");
469
+ report.tests.upload = await uploadImage(testBuffer, {
470
+ folder: testFolder,
471
+ resourceType: "raw",
472
+ tags: ["test", "diagnostics"],
473
+ publicId: `diagnostic_${Date.now()}`,
474
+ overwrite: true
475
+ }).then(
476
+ (result) => ({ success: true, publicId: result.public_id, url: result.secure_url, bytes: result.bytes }),
477
+ (error) => ({ success: false, error: error.message, httpCode: error.http_code, name: error.name })
478
+ );
479
+ report.tests.videoLimits = await getCloudinaryUsage({ resourceType: "video" }).then(
480
+ (result) => ({ success: true, result }),
481
+ (error) => ({ success: false, error: error.message })
482
+ );
483
+ if (testWebhook) {
484
+ report.tests.webhook = testCloudinaryWebhookSignature(webhookSecret);
485
+ }
486
+ return report;
487
+ }
488
+
489
+ // src/webhookSignature.js
490
+ import crypto2 from "crypto";
491
+ function verifySanvikaWebhookSignature({ headers, rawBody, secret, toleranceSec = 300 } = {}) {
492
+ const getHeader = (name) => {
493
+ if (!headers) return "";
494
+ if (typeof headers.get === "function") return headers.get(name) || headers.get(name.toLowerCase()) || "";
495
+ return headers[name] || headers[name.toLowerCase()] || "";
496
+ };
497
+ const sig = String(getHeader("x-sanvika-signature") || "").trim();
498
+ const ts = String(getHeader("x-sanvika-timestamp") || "").trim();
499
+ const token = secret || process.env.SANVIKA_CLOUDINARY_TOKEN;
500
+ if (!token) return { valid: false, reason: "missing_secret" };
501
+ if (!sig) return { valid: false, reason: "missing_signature" };
502
+ if (!ts) return { valid: false, reason: "missing_timestamp" };
503
+ const tsNum = Number(ts);
504
+ if (!Number.isFinite(tsNum)) return { valid: false, reason: "invalid_timestamp" };
505
+ const nowSec = Math.floor(Date.now() / 1e3);
506
+ if (Math.abs(nowSec - tsNum) > toleranceSec) return { valid: false, reason: "stale_timestamp" };
507
+ const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : String(rawBody || "");
508
+ const expected = crypto2.createHmac("sha256", token).update(ts + body).digest("hex");
509
+ const a = Buffer.from(sig, "hex");
510
+ const b = Buffer.from(expected, "hex");
511
+ if (a.length !== b.length) return { valid: false, reason: "length_mismatch" };
512
+ const ok = crypto2.timingSafeEqual(a, b);
513
+ return { valid: ok, reason: ok ? void 0 : "signature_mismatch" };
514
+ }
256
515
  export {
257
516
  CloudinaryError,
258
517
  TRANSFORM_PRESETS,
@@ -260,16 +519,25 @@ export {
260
519
  deleteImage,
261
520
  deleteImages,
262
521
  extractPublicId,
522
+ gatewayGet,
523
+ gatewayPost,
524
+ gatewayUpload,
263
525
  getAppName,
526
+ getCloudinarySdkVersion,
527
+ getCloudinaryUsage,
264
528
  getFolderPath,
265
529
  getOptimizedUrl,
266
530
  isCloudinaryUrl,
531
+ isProxyMode,
267
532
  isRetriableError,
268
533
  pingCloudinary,
534
+ runCloudinaryDiagnostics,
535
+ testCloudinaryWebhookSignature,
269
536
  uploadImage,
270
537
  uploadImages,
271
538
  uploadRawFile,
272
539
  uploadVideo,
273
540
  validatePublicId,
541
+ verifySanvikaWebhookSignature,
274
542
  withRetry
275
543
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanvika/cloudinary",
3
- "version": "0.1.3",
4
- "description": "Centralized Cloudinary SDK for the Sanvika ecosystem — upload, delete, transform, and manage media across 50+ projects",
3
+ "version": "0.2.0",
4
+ "description": "Centralized Cloudinary SDK for the Sanvika ecosystem — proxy gateway mode (zero credentials) + legacy direct mode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -34,7 +34,8 @@
34
34
  "license": "MIT",
35
35
  "peerDependencies": {
36
36
  "react": ">=18.0.0",
37
- "react-dom": ">=18.0.0"
37
+ "react-dom": ">=18.0.0",
38
+ "cloudinary": "^2.5.0"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "react": {
@@ -42,6 +43,9 @@
42
43
  },
43
44
  "react-dom": {
44
45
  "optional": true
46
+ },
47
+ "cloudinary": {
48
+ "optional": true
45
49
  }
46
50
  },
47
51
  "engines": {