@sanvika/cloudinary 0.2.1 → 0.2.3

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 +111 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -228,6 +228,13 @@ async function getAppName() {
228
228
  await ensureConfigured();
229
229
  return _appName;
230
230
  }
231
+ function stripAppPrefix(folder, appName) {
232
+ if (!folder || !appName) return folder || "";
233
+ const prefix = `${appName}/`;
234
+ if (folder === appName) return "";
235
+ if (folder.startsWith(prefix)) return folder.slice(prefix.length);
236
+ return folder;
237
+ }
231
238
  async function uploadImage(fileOrBuffer, options = {}) {
232
239
  await ensureConfigured();
233
240
  const {
@@ -247,8 +254,9 @@ async function uploadImage(fileOrBuffer, options = {}) {
247
254
  maxAttempts = 3
248
255
  } = options;
249
256
  const eagerValue = eager || transforms;
257
+ const normalizedFolder = stripAppPrefix(folder || "", _appName);
250
258
  const uploadOptions = {
251
- folder: folder || "",
259
+ folder: normalizedFolder,
252
260
  resourceType,
253
261
  tags: [_appName, ...tags],
254
262
  eager: eagerValue,
@@ -268,7 +276,7 @@ async function uploadImage(fileOrBuffer, options = {}) {
268
276
  });
269
277
  }
270
278
  const cloudinary = await loadLegacyCloudinary();
271
- const uploadFolder = getFolderPath(_appName, folder);
279
+ const uploadFolder = getFolderPath(_appName, normalizedFolder);
272
280
  const uploadOpts = {
273
281
  folder: uploadFolder,
274
282
  resource_type: resourceType,
@@ -321,14 +329,15 @@ async function uploadRawFile(buffer, options = {}) {
321
329
  const { folder, filename, tags = [] } = options;
322
330
  if (!buffer || !Buffer.isBuffer(buffer)) throw new Error("Buffer is required for uploadRawFile");
323
331
  const baseName = filename ? filename.replace(/\.[^.]+$/, "") : "file";
332
+ const normalizedFolder = stripAppPrefix(folder || "", _appName);
324
333
  if (isProxyMode()) {
325
334
  return withRetry(
326
- () => gatewayUpload(buffer, { folder: folder || "", resourceType: "raw", tags: ["raw", _appName, ...tags], publicId: baseName, overwrite: true, filename }),
335
+ () => gatewayUpload(buffer, { folder: normalizedFolder, resourceType: "raw", tags: ["raw", _appName, ...tags], publicId: baseName, overwrite: true, filename }),
327
336
  { operationName: "uploadRawFile", maxAttempts: 3 }
328
337
  );
329
338
  }
330
339
  const cloudinary = await loadLegacyCloudinary();
331
- const uploadFolder = getFolderPath(_appName, folder);
340
+ const uploadFolder = getFolderPath(_appName, normalizedFolder);
332
341
  return withRetry(
333
342
  () => new Promise((resolve, reject) => {
334
343
  const stream = cloudinary.uploader.upload_stream(
@@ -512,10 +521,103 @@ function verifySanvikaWebhookSignature({ headers, rawBody, secret, toleranceSec
512
521
  const ok = crypto2.timingSafeEqual(a, b);
513
522
  return { valid: ok, reason: ok ? void 0 : "signature_mismatch" };
514
523
  }
524
+
525
+ // src/cloudinaryValidation.js
526
+ var MAX_IMAGE_SIZE = 5 * 1024 * 1024;
527
+ var MAX_PROFILE_PIC_SIZE = 6 * 1024 * 1024;
528
+ var MAX_VIDEO_SIZE = 100 * 1024 * 1024;
529
+ var SUPPORTED_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".webp"];
530
+ var SUPPORTED_PROFILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png"];
531
+ var SUPPORTED_VIDEO_FORMATS = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
532
+ function splitFileName(name) {
533
+ const safe = typeof name === "string" && name.trim() ? name.trim() : "";
534
+ const dotIdx = safe.lastIndexOf(".");
535
+ if (dotIdx <= 0) return { ext: "", base: safe || "asset" };
536
+ return { ext: safe.slice(dotIdx).toLowerCase(), base: safe.slice(0, dotIdx) };
537
+ }
538
+ function normalizeFolderPath(value, fallback) {
539
+ if (typeof value !== "string") return fallback;
540
+ const sanitized = value.trim().replace(/\\/g, "/").split("/").filter(Boolean).join("/");
541
+ return sanitized || fallback;
542
+ }
543
+ function createScopedPublicId({ baseName, userPrefix, timestamp }) {
544
+ const safeBaseName = baseName && baseName.trim() ? baseName.trim() : "asset";
545
+ const safePrefix = userPrefix && userPrefix.trim() ? userPrefix.trim() : "anon";
546
+ const safeTimestamp = timestamp && timestamp.trim() ? timestamp.trim() : Date.now().toString().slice(-6);
547
+ return `${safePrefix}_${safeTimestamp}_${safeBaseName}`;
548
+ }
549
+ function validateImageFile(file) {
550
+ const { ext: extFromName } = splitFileName(file.name || "");
551
+ const mimeExt = file.type ? `.${file.type.split("/")[1]}`.toLowerCase() : "";
552
+ const finalExt = extFromName || mimeExt;
553
+ if (!SUPPORTED_IMAGE_FORMATS.includes(finalExt)) {
554
+ return {
555
+ valid: false,
556
+ status: 415,
557
+ error: "INVALID_FORMAT",
558
+ message: `Unsupported format: ${finalExt}. Allowed: ${SUPPORTED_IMAGE_FORMATS.join(", ")}`
559
+ };
560
+ }
561
+ if (file.size > MAX_IMAGE_SIZE) {
562
+ return {
563
+ valid: false,
564
+ status: 413,
565
+ error: "FILE_TOO_LARGE",
566
+ message: `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds the ${MAX_IMAGE_SIZE / 1024 / 1024}MB limit`
567
+ };
568
+ }
569
+ return { valid: true };
570
+ }
571
+ function validateProfilePicFile(file) {
572
+ if (!SUPPORTED_PROFILE_MIME_TYPES.includes(file.type)) {
573
+ return {
574
+ valid: false,
575
+ status: 415,
576
+ error: "INVALID_FORMAT",
577
+ message: "Unsupported format. Only JPG, JPEG, and PNG are allowed."
578
+ };
579
+ }
580
+ if (file.size > MAX_PROFILE_PIC_SIZE) {
581
+ return {
582
+ valid: false,
583
+ status: 413,
584
+ error: "FILE_TOO_LARGE",
585
+ message: `File size too large. Maximum is ${MAX_PROFILE_PIC_SIZE / 1024 / 1024}MB.`
586
+ };
587
+ }
588
+ return { valid: true };
589
+ }
590
+ function validateVideoFile(file) {
591
+ const { ext } = splitFileName(file.name || "");
592
+ if (!SUPPORTED_VIDEO_FORMATS.includes(ext)) {
593
+ return {
594
+ valid: false,
595
+ status: 415,
596
+ error: "INVALID_FORMAT",
597
+ message: `Unsupported format: ${ext}. Allowed: ${SUPPORTED_VIDEO_FORMATS.join(", ")}`
598
+ };
599
+ }
600
+ if (file.size > MAX_VIDEO_SIZE) {
601
+ return {
602
+ valid: false,
603
+ status: 413,
604
+ error: "FILE_TOO_LARGE",
605
+ message: `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds the ${MAX_VIDEO_SIZE / 1024 / 1024}MB limit`
606
+ };
607
+ }
608
+ return { valid: true };
609
+ }
515
610
  export {
516
611
  CloudinaryError,
612
+ MAX_IMAGE_SIZE,
613
+ MAX_PROFILE_PIC_SIZE,
614
+ MAX_VIDEO_SIZE,
615
+ SUPPORTED_IMAGE_FORMATS,
616
+ SUPPORTED_PROFILE_MIME_TYPES,
617
+ SUPPORTED_VIDEO_FORMATS,
517
618
  TRANSFORM_PRESETS,
518
619
  configureSanvikaCloudinary,
620
+ createScopedPublicId,
519
621
  deleteImage,
520
622
  deleteImages,
521
623
  extractPublicId,
@@ -530,14 +632,19 @@ export {
530
632
  isCloudinaryUrl,
531
633
  isProxyMode,
532
634
  isRetriableError,
635
+ normalizeFolderPath,
533
636
  pingCloudinary,
534
637
  runCloudinaryDiagnostics,
638
+ splitFileName,
535
639
  testCloudinaryWebhookSignature,
536
640
  uploadImage,
537
641
  uploadImages,
538
642
  uploadRawFile,
539
643
  uploadVideo,
644
+ validateImageFile,
645
+ validateProfilePicFile,
540
646
  validatePublicId,
647
+ validateVideoFile,
541
648
  verifySanvikaWebhookSignature,
542
649
  withRetry
543
650
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanvika/cloudinary",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
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",