@sanvika/cloudinary 0.1.4 → 0.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.
Files changed (2) hide show
  1. package/dist/index.js +207 -56
  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,37 +108,128 @@ 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.CLOUDINARY_GATEWAY_URL && process.env.CLOUDINARY_TOKEN);
114
+ }
115
+ function requireProxyEnv() {
116
+ const base = process.env.CLOUDINARY_GATEWAY_URL;
117
+ const token = process.env.CLOUDINARY_TOKEN;
118
+ if (!base || !token) {
119
+ throw new CloudinaryError(
120
+ "Proxy gateway env missing. Set CLOUDINARY_GATEWAY_URL and 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.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 (CLOUDINARY_GATEWAY_URL + 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.CLOUDINARY_APP_NAME || "sanvika-app";
218
+ _configured = true;
219
+ return;
220
+ }
221
+ if (process.env.CLOUDINARY_APP_NAME) {
222
+ await configureSanvikaCloudinary({ appName: process.env.CLOUDINARY_APP_NAME });
223
+ return;
137
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",
@@ -158,6 +246,28 @@ async function uploadImage(fileOrBuffer, options = {}) {
158
246
  timeout,
159
247
  maxAttempts = 3
160
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();
161
271
  const uploadFolder = getFolderPath(_appName, folder);
162
272
  const uploadOpts = {
163
273
  folder: uploadFolder,
@@ -166,7 +276,6 @@ async function uploadImage(fileOrBuffer, options = {}) {
166
276
  tags: [_appName, ...tags]
167
277
  };
168
278
  if (publicId) uploadOpts.public_id = publicId;
169
- const eagerValue = eager || transforms;
170
279
  if (eagerValue) uploadOpts.eager = eagerValue;
171
280
  if (eagerAsync) uploadOpts.eager_async = true;
172
281
  if (eagerNotificationUrl) uploadOpts.eager_notification_url = eagerNotificationUrl;
@@ -174,9 +283,7 @@ async function uploadImage(fileOrBuffer, options = {}) {
174
283
  if (timeout) uploadOpts.timeout = timeout;
175
284
  const fileSize = Buffer.isBuffer(fileOrBuffer) ? fileOrBuffer.length : null;
176
285
  const useChunked = chunked || resourceType === "video" && fileSize !== null && fileSize > 10 * 1024 * 1024;
177
- if (useChunked) {
178
- uploadOpts.chunk_size = chunkSize;
179
- }
286
+ if (useChunked) uploadOpts.chunk_size = chunkSize;
180
287
  return withRetry(
181
288
  () => {
182
289
  if (Buffer.isBuffer(fileOrBuffer)) {
@@ -210,11 +317,18 @@ async function uploadImages(files, options = {}) {
210
317
  return Promise.all(files.map((file) => uploadImage(file, options)));
211
318
  }
212
319
  async function uploadRawFile(buffer, options = {}) {
213
- ensureConfigured();
320
+ await ensureConfigured();
214
321
  const { folder, filename, tags = [] } = options;
215
322
  if (!buffer || !Buffer.isBuffer(buffer)) throw new Error("Buffer is required for uploadRawFile");
216
- const uploadFolder = getFolderPath(_appName, folder);
217
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);
218
332
  return withRetry(
219
333
  () => new Promise((resolve, reject) => {
220
334
  const stream = cloudinary.uploader.upload_stream(
@@ -236,8 +350,14 @@ async function uploadRawFile(buffer, options = {}) {
236
350
  );
237
351
  }
238
352
  async function deleteImage(urlOrPublicId, options = {}) {
239
- ensureConfigured();
353
+ var _a;
354
+ await ensureConfigured();
240
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();
241
361
  const publicId = isCloudinaryUrl(urlOrPublicId) ? extractPublicId(urlOrPublicId) : urlOrPublicId;
242
362
  if (!publicId) throw new CloudinaryError("Cannot extract public ID", "deleteImage", { urlOrPublicId });
243
363
  return withRetry(
@@ -246,14 +366,22 @@ async function deleteImage(urlOrPublicId, options = {}) {
246
366
  );
247
367
  }
248
368
  async function deleteImages(urls, options = {}) {
249
- ensureConfigured();
369
+ await ensureConfigured();
250
370
  const { fast = false, resourceType = "image" } = options;
251
- 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();
252
378
  const BATCH_SIZE = fast ? 15 : 10;
253
379
  const DELAY = fast ? 100 : 500;
254
380
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
255
381
  const publicIds = urls.map((u) => isCloudinaryUrl(u) ? extractPublicId(u) : u).filter(Boolean);
256
- 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
+ }
257
385
  const allResults = [];
258
386
  for (let i = 0; i < publicIds.length; i += BATCH_SIZE) {
259
387
  const batch = publicIds.slice(i, i + BATCH_SIZE);
@@ -275,15 +403,21 @@ async function deleteImages(urls, options = {}) {
275
403
  return { success: failed === 0, total: urls.length, processed: allResults.length, successful, failed };
276
404
  }
277
405
  async function pingCloudinary() {
278
- ensureConfigured();
406
+ await ensureConfigured();
407
+ if (isProxyMode()) return gatewayGet("/api/v1/ping");
408
+ const cloudinary = await loadLegacyCloudinary();
279
409
  return cloudinary.api.ping();
280
410
  }
281
411
  async function getCloudinaryUsage(options = {}) {
282
- ensureConfigured();
412
+ await ensureConfigured();
283
413
  const { resourceType } = options;
414
+ if (isProxyMode()) return gatewayGet("/api/v1/usage", resourceType ? { resourceType } : void 0);
415
+ const cloudinary = await loadLegacyCloudinary();
284
416
  return cloudinary.api.usage(resourceType ? { resource_type: resourceType } : {});
285
417
  }
286
- function getCloudinarySdkVersion() {
418
+ async function getCloudinarySdkVersion() {
419
+ if (isProxyMode()) return "gateway";
420
+ const cloudinary = await loadLegacyCloudinary();
287
421
  return cloudinary.CLOUDINARY_VERSION;
288
422
  }
289
423
 
@@ -291,9 +425,7 @@ function getCloudinarySdkVersion() {
291
425
  import crypto from "crypto";
292
426
  function testCloudinaryWebhookSignature(apiSecret, options = {}) {
293
427
  try {
294
- if (!apiSecret) {
295
- return { success: false, error: "apiSecret is required" };
296
- }
428
+ if (!apiSecret) return { success: false, error: "apiSecret is required" };
297
429
  const testPayload = { test: "data", timestamp: Math.floor(Date.now() / 1e3) };
298
430
  const testPayloadString = JSON.stringify(testPayload);
299
431
  const testTimestamp = testPayload.timestamp.toString();
@@ -304,10 +436,7 @@ function testCloudinaryWebhookSignature(apiSecret, options = {}) {
304
436
  testPayload,
305
437
  testTimestamp,
306
438
  signature,
307
- headers: {
308
- "X-Cld-Timestamp": testTimestamp,
309
- "X-Cld-Signature": signature
310
- },
439
+ headers: { "X-Cld-Timestamp": testTimestamp, "X-Cld-Signature": signature },
311
440
  testEndpoint: options.testEndpoint || "/api/webhooks/cloudinary/debug"
312
441
  };
313
442
  } catch (error) {
@@ -315,15 +444,15 @@ function testCloudinaryWebhookSignature(apiSecret, options = {}) {
315
444
  }
316
445
  }
317
446
  async function runCloudinaryDiagnostics(options = {}) {
318
- const {
319
- testWebhook = false,
320
- webhookSecret,
321
- testFolder = "diagnostics"
322
- } = options;
447
+ if (isProxyMode()) {
448
+ return gatewayPost("/api/v1/diagnostics", options || {});
449
+ }
450
+ const { testWebhook = false, webhookSecret, testFolder = "diagnostics" } = options;
323
451
  const report = {
324
452
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
325
- cloudinaryVersion: getCloudinarySdkVersion(),
453
+ cloudinaryVersion: await getCloudinarySdkVersion(),
326
454
  nodeVersion: process.version,
455
+ mode: "legacy",
327
456
  configuration: {
328
457
  cloudName: process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME ? "set" : "missing",
329
458
  apiKey: process.env.CLOUDINARY_API_KEY ? "set" : "missing",
@@ -344,18 +473,8 @@ async function runCloudinaryDiagnostics(options = {}) {
344
473
  publicId: `diagnostic_${Date.now()}`,
345
474
  overwrite: true
346
475
  }).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
- })
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 })
359
478
  );
360
479
  report.tests.videoLimits = await getCloudinaryUsage({ resourceType: "video" }).then(
361
480
  (result) => ({ success: true, result }),
@@ -366,6 +485,33 @@ async function runCloudinaryDiagnostics(options = {}) {
366
485
  }
367
486
  return report;
368
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.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
+ }
369
515
  export {
370
516
  CloudinaryError,
371
517
  TRANSFORM_PRESETS,
@@ -373,12 +519,16 @@ export {
373
519
  deleteImage,
374
520
  deleteImages,
375
521
  extractPublicId,
522
+ gatewayGet,
523
+ gatewayPost,
524
+ gatewayUpload,
376
525
  getAppName,
377
526
  getCloudinarySdkVersion,
378
527
  getCloudinaryUsage,
379
528
  getFolderPath,
380
529
  getOptimizedUrl,
381
530
  isCloudinaryUrl,
531
+ isProxyMode,
382
532
  isRetriableError,
383
533
  pingCloudinary,
384
534
  runCloudinaryDiagnostics,
@@ -388,5 +538,6 @@ export {
388
538
  uploadRawFile,
389
539
  uploadVideo,
390
540
  validatePublicId,
541
+ verifySanvikaWebhookSignature,
391
542
  withRetry
392
543
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanvika/cloudinary",
3
- "version": "0.1.4",
4
- "description": "Centralized Cloudinary SDK for the Sanvika ecosystem — upload, delete, transform, and manage media across 50+ projects",
3
+ "version": "0.2.1",
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": {