mbkbucket 3.6.0 → 3.7.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.
@@ -7,32 +7,52 @@
7
7
 
8
8
  import express from "express";
9
9
  import { sessRole } from "mbkauthe";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
10
12
  import { bucketResolver } from "../middleware/bucket-resolver.js";
11
13
  import { renderBucketDashboard } from "../controllers/dashboard.controller.js";
12
14
  import infoRoutes from "./info.routes.js";
13
15
  import { createViewRoutes } from "./view.routes.js";
14
16
  import { createApiRoutes } from "./api.routes.js";
15
17
 
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+ const publicDir = path.resolve(__dirname, "..", "..", "public");
21
+
22
+ const serveAsset = (filename) => (_req, res) => {
23
+ res.set("Cache-Control", "public, max-age=300, must-revalidate");
24
+ res.sendFile(path.join(publicDir, filename), (err) => {
25
+ if (err) res.status(err.status || 404).end();
26
+ });
27
+ };
28
+
16
29
  export function createBucketRouter({ authorization } = {}) {
17
- const router = express.Router();
18
- const defaultAuthorization = sessRole('superadmin');
19
- const authorizations = typeof authorization === 'function'
20
- ? { view: authorization, upload: authorization, delete: authorization }
21
- : authorization || {};
22
- const viewAuthorization = authorizations.view || defaultAuthorization;
23
- const uploadAuthorization = authorizations.upload || defaultAuthorization;
24
- const deleteAuthorization = authorizations.delete || defaultAuthorization;
25
-
26
- router.get('/mbkbucket', viewAuthorization, renderBucketDashboard);
27
- router.use(
28
- '/mbkbucket',
29
- bucketResolver,
30
- infoRoutes,
31
- createViewRoutes({ authorization: viewAuthorization }),
32
- createApiRoutes({ viewAuthorization, uploadAuthorization, deleteAuthorization }),
33
- );
34
-
35
- return router;
30
+ const router = express.Router();
31
+ const defaultAuth = sessRole('superadmin');
32
+ const auths = typeof authorization === 'function'
33
+ ? { view: authorization, upload: authorization, delete: authorization }
34
+ : authorization ?? {};
35
+ const viewAuth = auths.view ?? defaultAuth;
36
+ const uploadAuth = auths.upload ?? defaultAuth;
37
+ const deleteAuth = auths.delete ?? defaultAuth;
38
+
39
+ // Static dashboard & helper assets
40
+ router.get('/mbkbucket/bucketadmin.css', serveAsset('bucketadmin.css'));
41
+ router.get('/mbkbucket/bucketadmin.js', serveAsset('bucketadmin.js'));
42
+ router.get('/mbkbucket/mbkbucket-helper.css', serveAsset('mbkbucket-helper.css'));
43
+ router.get('/mbkbucket/mbkbucket-helper.js', serveAsset('mbkbucket-helper.js'));
44
+ router.get('/mbkbucket/helper-demo', serveAsset('mbkbucket-helper-demo.html'));
45
+
46
+ router.get('/mbkbucket', viewAuth, renderBucketDashboard);
47
+ router.use(
48
+ '/mbkbucket',
49
+ bucketResolver,
50
+ infoRoutes,
51
+ createViewRoutes({ authorization: viewAuth }),
52
+ createApiRoutes({ viewAuthorization: viewAuth, uploadAuthorization: uploadAuth, deleteAuthorization: deleteAuth })
53
+ );
54
+
55
+ return router;
36
56
  }
37
57
 
38
58
  const router = createBucketRouter();
@@ -12,7 +12,7 @@ function parseJsonEnv(envVar, fallback = {}) {
12
12
  try {
13
13
  return JSON.parse(envVar);
14
14
  } catch (e) {
15
- console.error(`❌ Error parsing JSON for envVar: ${envVar ? String(envVar).slice(0, 200) : 'undefined'} - ${e.message}`);
15
+ console.error(`❌ Error parsing JSON for envVar: ${String(envVar).slice(0, 200)} - ${e.message}`);
16
16
  console.error(`💡 Tip: Check that inner objects are NOT quoted as strings. Use {"key":{"field":"value"}} not {"key":"{"field":"value"}"}`);
17
17
  return fallback;
18
18
  }
@@ -24,13 +24,9 @@ const hasBucketConfigs = availableBucketNames.length > 0;
24
24
 
25
25
  const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
26
 
27
- export function getAvailableBucketNames() {
28
- return availableBucketNames;
29
- }
27
+ export const getAvailableBucketNames = () => availableBucketNames;
30
28
 
31
- function getDefaultBucketName() {
32
- return mbkautheVar?.bucket || (availableBucketNames.length > 0 ? availableBucketNames[0] : null);
33
- }
29
+ const getDefaultBucketName = () => mbkautheVar?.bucket || (availableBucketNames.length > 0 ? availableBucketNames[0] : null);
34
30
 
35
31
  export function resolveBucketName(bucketName) {
36
32
  const raw = typeof bucketName === 'string' ? bucketName.trim() : bucketName;
@@ -53,10 +49,7 @@ export function getAppName() {
53
49
  return typeof app === 'string' && app.trim() ? app.trim() : '';
54
50
  }
55
51
 
56
- function isRootModeApp(appName) {
57
- const app = String(appName || '').toLowerCase();
58
- return app === 'portal' || app === 'mbkbucket';
59
- }
52
+ const isRootModeApp = appName => ['portal', 'mbkbucket'].includes(String(appName || '').toLowerCase());
60
53
 
61
54
  function validateKeySafety(str, isKey = false) {
62
55
  if (isKey && !str) throw new Error('Key is required');
@@ -73,7 +66,7 @@ export function ensureKeyHasAppPrefix(key = '') {
73
66
  validateKeySafety(key, true);
74
67
 
75
68
  const app = getAppName();
76
- let cleaned = trimLeadingSlashes(key);
69
+ const cleaned = trimLeadingSlashes(key);
77
70
  if (isRootModeApp(app)) return cleaned;
78
71
  if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
79
72
 
@@ -86,16 +79,14 @@ export function ensurePrefix(prefix = '') {
86
79
  validateKeySafety(prefix, false);
87
80
 
88
81
  const app = getAppName();
89
- let p = trimLeadingSlashes(prefix);
82
+ const p = trimLeadingSlashes(prefix);
90
83
  if (isRootModeApp(app)) return p;
91
84
  if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
92
- if (!p) return app;
93
- if (p === app || p === `${app}/`) return p;
85
+ if (!p || p === app || p === `${app}/`) return p || app;
94
86
 
95
- if (p.startsWith(`${app}/`)) {
96
- return p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`);
97
- }
98
- return `${app}/${p}`;
87
+ return p.startsWith(`${app}/`)
88
+ ? p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`)
89
+ : `${app}/${p}`;
99
90
  }
100
91
 
101
92
  // ---------------------------------------------------------------------------
@@ -26,23 +26,15 @@ function matchError(rules, error, fallbackMessage) {
26
26
  return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
27
27
  }
28
28
 
29
- export function classifyApiError(error, fallbackMessage = 'Request failed') {
30
- return matchError(API_ERROR_RULES, error, fallbackMessage);
31
- }
32
-
33
- export function classifyViewError(error, fallbackMessage = 'Request failed') {
34
- return matchError(VIEW_ERROR_RULES, error, fallbackMessage);
35
- }
29
+ export const classifyApiError = (error, fallbackMessage = 'Request failed') => matchError(API_ERROR_RULES, error, fallbackMessage);
30
+ export const classifyViewError = (error, fallbackMessage = 'Request failed') => matchError(VIEW_ERROR_RULES, error, fallbackMessage);
36
31
 
37
32
  export function sendApiError(res, error, fallbackMessage = 'Request failed') {
38
33
  const { status, code, message } = classifyApiError(error, fallbackMessage);
39
34
  return res.status(status).json({
40
35
  success: false,
41
36
  code,
42
- error: {
43
- code,
44
- message,
45
- },
37
+ error: { code, message },
46
38
  message,
47
39
  timestamp: new Date().toISOString(),
48
40
  });
@@ -4,14 +4,12 @@
4
4
 
5
5
  const HTML_ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
6
6
 
7
- export function escapeHtml(value = '') {
8
- return String(value ?? '').replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c]);
9
- }
7
+ export const escapeHtml = (value = '') => String(value ?? '').replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c]);
10
8
 
11
9
  export function getBaseName(path = '') {
12
10
  const value = String(path ?? '');
13
- const lastSlashIndex = value.lastIndexOf('/');
14
- return lastSlashIndex === -1 ? value : value.slice(lastSlashIndex + 1);
11
+ const lastSlash = value.lastIndexOf('/');
12
+ return lastSlash === -1 ? value : value.slice(lastSlash + 1);
15
13
  }
16
14
 
17
15
  export function getFileExt(fileName = '') {
@@ -20,13 +18,8 @@ export function getFileExt(fileName = '') {
20
18
  return dotIndex === -1 ? '' : name.slice(dotIndex + 1).toLowerCase();
21
19
  }
22
20
 
23
- export function trimSlashes(str = '') {
24
- return String(str ?? '').replace(/^\/+/, '').replace(/\/+$/, '');
25
- }
26
-
27
- export function trimLeadingSlashes(str = '') {
28
- return String(str ?? '').replace(/^\/+/, '');
29
- }
21
+ export const trimSlashes = (str = '') => String(str ?? '').replace(/^\/+|\/+$/g, '');
22
+ export const trimLeadingSlashes = (str = '') => String(str ?? '').replace(/^\/+/, '');
30
23
 
31
24
  export function getFolderPath(key = '') {
32
25
  const s = String(key ?? '');
@@ -34,9 +27,7 @@ export function getFolderPath(key = '') {
34
27
  return lastSlash === -1 ? '' : s.slice(0, lastSlash + 1);
35
28
  }
36
29
 
37
- export function normalizeKeyParam(rawKey) {
38
- return Array.isArray(rawKey) ? rawKey.join('/') : String(rawKey || '');
39
- }
30
+ export const normalizeKeyParam = rawKey => Array.isArray(rawKey) ? rawKey.join('/') : String(rawKey || '');
40
31
 
41
32
  export function buildKey(prefix, fileName) {
42
33
  const cleanPrefix = trimSlashes(prefix);
@@ -44,15 +35,13 @@ export function buildKey(prefix, fileName) {
44
35
  return cleanPrefix ? `${cleanPrefix}/${cleanName}` : cleanName;
45
36
  }
46
37
 
47
- export function nowIso() {
48
- return new Date().toISOString();
49
- }
38
+ export const nowIso = () => new Date().toISOString();
50
39
 
51
40
  export function formatBytes(bytes) {
52
- if (!bytes || bytes === 0) return '0 B';
41
+ if (!bytes) return '0 B';
53
42
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
54
43
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
55
- return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
44
+ return `${(bytes / 1024 ** i).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
56
45
  }
57
46
 
58
47
  export function formatDate(iso) {
@@ -67,25 +56,18 @@ export async function streamToBuffer(stream) {
67
56
  if (Buffer.isBuffer(stream)) return stream;
68
57
  const chunks = [];
69
58
  if (stream?.[Symbol.asyncIterator] || stream?.transformToWeb) {
70
- for await (const chunk of stream) {
71
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
72
- }
73
- return Buffer.concat(chunks);
74
- }
75
- if (stream?.getReader) {
59
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
60
+ } else if (stream?.getReader) {
76
61
  const reader = stream.getReader();
77
- while (true) {
78
- const { done, value } = await reader.read();
79
- if (done) break;
80
- chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value));
62
+ for (let res = await reader.read(); !res.done; res = await reader.read()) {
63
+ chunks.push(Buffer.isBuffer(res.value) ? res.value : Buffer.from(res.value));
81
64
  }
82
- return Buffer.concat(chunks);
83
65
  }
84
66
  return Buffer.concat(chunks);
85
67
  }
86
68
 
87
69
  export function destroyStream(stream) {
88
- if (stream && typeof stream.destroy === 'function') {
70
+ if (typeof stream?.destroy === 'function') {
89
71
  stream.removeAllListeners?.();
90
72
  stream.destroy();
91
73
  }
@@ -111,17 +93,10 @@ export const VIDEO_TYPES = new Set(['mp4', 'webm', 'ogg', 'avi', 'mov']);
111
93
  export const AUDIO_TYPES = new Set(['mp3', 'wav', 'flac', 'aac', 'm4a']);
112
94
 
113
95
  export const MIME_TYPES = {
114
- // Images
115
- jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
116
- gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
117
- bmp: 'image/bmp', ico: 'image/x-icon',
118
- // Videos
119
- mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg',
120
- avi: 'video/x-msvideo', mov: 'video/quicktime',
121
- // Audio
122
- mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac',
123
- aac: 'audio/aac', m4a: 'audio/mp4',
124
- // Text / Code
96
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
97
+ webp: 'image/webp', svg: 'image/svg+xml', bmp: 'image/bmp', ico: 'image/x-icon',
98
+ mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg', avi: 'video/x-msvideo', mov: 'video/quicktime',
99
+ mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4',
125
100
  txt: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8',
126
101
  json: 'application/json; charset=utf-8', xml: 'application/xml; charset=utf-8',
127
102
  csv: 'text/csv; charset=utf-8', log: 'text/plain; charset=utf-8',
@@ -137,14 +112,10 @@ export const MIME_TYPES = {
137
112
  ps1: 'text/x-powershell; charset=utf-8', yaml: 'text/x-yaml; charset=utf-8',
138
113
  yml: 'text/x-yaml; charset=utf-8', toml: 'text/x-toml; charset=utf-8',
139
114
  ini: 'text/x-ini; charset=utf-8', conf: 'text/x-conf; charset=utf-8',
140
- // Archives & Documents
141
- zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',
142
- pdf: 'application/pdf'
115
+ zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar', pdf: 'application/pdf'
143
116
  };
144
117
 
145
- export function getMimeType(fileName, fallback = 'application/octet-stream') {
146
- return MIME_TYPES[getFileExt(fileName)] || fallback;
147
- }
118
+ export const getMimeType = (fileName, fallback = 'application/octet-stream') => MIME_TYPES[getFileExt(fileName)] || fallback;
148
119
 
149
120
  export function isSensitiveTextType(contentType = '') {
150
121
  const type = String(contentType ?? '').toLowerCase();
@@ -154,8 +125,7 @@ export function isSensitiveTextType(contentType = '') {
154
125
  export function buildCacheControl(contentType, { isStaticAsset = false, supportsRanges = false, publicCache = false } = {}) {
155
126
  if (publicCache) {
156
127
  if (isStaticAsset) return 'public, max-age=3600, immutable';
157
- if (supportsRanges) return 'public, max-age=86400, must-revalidate';
158
- return 'public, max-age=300, must-revalidate';
128
+ return supportsRanges ? 'public, max-age=86400, must-revalidate' : 'public, max-age=300, must-revalidate';
159
129
  }
160
130
  if (isSensitiveTextType(contentType)) return 'private, no-store';
161
131
  return supportsRanges ? 'private, max-age=1800, must-revalidate' : 'private, max-age=300, must-revalidate';
@@ -185,15 +155,11 @@ export function parseRangeHeader(rangeHeader, totalSize) {
185
155
  } else {
186
156
  start = parseInt(startStr, 10);
187
157
  if (isNaN(start) || start < 0) return null;
188
- if (endStr === '') {
189
- end = Math.min(start + DEFAULT_CHUNK_SIZE - 1, totalSize - 1);
190
- } else {
191
- end = parseInt(endStr, 10);
192
- if (isNaN(end) || end < start) return null;
193
- }
158
+ end = endStr === '' ? Math.min(start + DEFAULT_CHUNK_SIZE - 1, totalSize - 1) : parseInt(endStr, 10);
159
+ if (isNaN(end) || end < start) return null;
194
160
  }
195
161
 
196
- if (start >= totalSize || start > end || end >= totalSize) return null;
162
+ if (start >= totalSize || end >= totalSize) return null;
197
163
  return { start, end, total: totalSize, size: end - start + 1 };
198
164
  }
199
165
 
@@ -201,8 +167,7 @@ export function parseRangeHeader(rangeHeader, totalSize) {
201
167
  // Player page HTML template
202
168
  // ---------------------------------------------------------------------------
203
169
 
204
- export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
205
- return `<!doctype html>
170
+ export const renderPlayerPage = (safeFileName, encodedKey, bucketQuery) => `<!doctype html>
206
171
  <html lang="en">
207
172
  <head>
208
173
  <meta charset="utf-8">
@@ -225,7 +190,6 @@ export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
225
190
  </div>
226
191
  </body>
227
192
  </html>`;
228
- }
229
193
 
230
194
  // ---------------------------------------------------------------------------
231
195
  // Common Handlebars helpers
@@ -236,16 +200,13 @@ export const commonHandlebarsHelpers = {
236
200
  neq: (a, b) => a !== b,
237
201
  or: (...args) => args.slice(0, -1).some(Boolean),
238
202
  and: (...args) => args.slice(0, -1).every(Boolean),
239
- not: (val) => !val,
203
+ not: val => !val,
240
204
  gt: (a, b) => a > b,
241
205
  gte: (a, b) => a >= b,
242
206
  lt: (a, b) => a < b,
243
207
  lte: (a, b) => a <= b,
244
208
  includes: (arr, val) => Array.isArray(arr) && arr.includes(val),
245
- json: (obj) => JSON.stringify(obj, null, 2),
246
- jsonStringify: (obj) => JSON.stringify(obj),
247
- getInitials: (username) => {
248
- if (!username) return '?';
249
- return username.split(/[._]/).map(p => p.charAt(0)).join('').toUpperCase().slice(0, 2);
250
- },
209
+ json: obj => JSON.stringify(obj, null, 2),
210
+ jsonStringify: obj => JSON.stringify(obj),
211
+ getInitials: username => username ? username.split(/[._]/).map(p => p.charAt(0)).join('').toUpperCase().slice(0, 2) : '?',
251
212
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "Flexible S3/R2 bucket management — library, Express router, and CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a command-line interface with built-in configuration validation and health checks",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
@@ -20,15 +20,15 @@
20
20
  "#logger": "./lib/src/utils/logger.js"
21
21
  },
22
22
  "dependencies": {
23
- "@aws-sdk/client-s3": "^3.968.0",
24
- "@aws-sdk/s3-request-presigner": "^3.1000.0",
23
+ "@aws-sdk/client-s3": "^3.1127.0",
24
+ "@aws-sdk/s3-request-presigner": "^3.1127.0",
25
25
  "compression": "^1.8.0",
26
26
  "debug": "^4.4.3",
27
27
  "dotenv": "^16.6.1",
28
28
  "express": "^5.1.0",
29
29
  "express-handlebars": "^9.0.1",
30
- "express-rate-limit": "^7.5.0",
31
- "mbkauthe": "^5.8.0",
30
+ "express-rate-limit": "^7.5.1",
31
+ "mbkauthe": "^6.0.0",
32
32
  "multer": "^2.0.2",
33
33
  "pg": "^8.16.3"
34
34
  },