mbkbucket 3.3.0 → 3.3.2

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 (39) hide show
  1. package/index.js +8 -18
  2. package/lib/cli.js +642 -0
  3. package/{public → lib/public}/bucketadmin.js +1 -1
  4. package/{src → lib/src}/app.js +37 -50
  5. package/{src → lib/src}/cli/auth-helper.js +24 -103
  6. package/lib/src/config/index.js +108 -0
  7. package/{src → lib/src}/config/validation.js +14 -30
  8. package/{src → lib/src}/controllers/api.controller.js +85 -176
  9. package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
  10. package/{src → lib/src}/controllers/info.controller.js +17 -20
  11. package/{src → lib/src}/controllers/view.controller.js +23 -70
  12. package/{src → lib/src}/middleware/async-handler.js +1 -16
  13. package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
  14. package/{src → lib/src}/middleware/error-handler.js +6 -14
  15. package/{src → lib/src}/middleware/rate-limiter.js +2 -10
  16. package/{src → lib/src}/middleware/security.js +1 -17
  17. package/lib/src/routes/api.routes.js +40 -0
  18. package/{src → lib/src}/routes/index.js +1 -9
  19. package/{src → lib/src}/routes/info.routes.js +2 -3
  20. package/{src → lib/src}/routes/view.routes.js +3 -8
  21. package/{src → lib/src}/services/s3.service.js +157 -349
  22. package/lib/src/utils/errors.js +45 -0
  23. package/lib/src/utils/helpers.js +251 -0
  24. package/package.json +18 -3
  25. package/.github/PACKAGE.md +0 -19
  26. package/.github/workflows/publish.yml +0 -88
  27. package/cli.js +0 -977
  28. package/index.d.ts +0 -444
  29. package/lib/index.js +0 -17
  30. package/src/config/index.js +0 -133
  31. package/src/routes/api.routes.js +0 -66
  32. package/src/utils/errors.js +0 -76
  33. package/src/utils/helpers.js +0 -197
  34. package/test/config-validation.test.js +0 -116
  35. /package/{public → lib/public}/bucketadmin.css +0 -0
  36. /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
  37. /package/{public → lib/public}/mbkbucket-helper.css +0 -0
  38. /package/{public → lib/public}/mbkbucket-helper.js +0 -0
  39. /package/{src → lib/src}/utils/logger.js +0 -0
@@ -1,76 +0,0 @@
1
- /**
2
- * Centralized error classification for API routes.
3
- * Maps error messages to HTTP status codes and machine-readable error codes.
4
- */
5
-
6
- /**
7
- * Classify an API-level error into { status, code, message }.
8
- * @param {Error} error
9
- * @param {string} fallbackMessage
10
- * @returns {{ status: number, code: string, message: string }}
11
- */
12
- export function classifyApiError(error, fallbackMessage = 'Request failed') {
13
- const message = String(error?.message || fallbackMessage);
14
- const lower = message.toLowerCase();
15
-
16
- if (lower.includes('bucketconnection environment variable is not set')) {
17
- return { status: 503, code: 'BUCKET_CONFIG_ERROR', message };
18
- }
19
- if (lower.includes('no bucket selected') || lower.includes('not found in bucketconnection')) {
20
- return { status: 400, code: 'INVALID_BUCKET', message };
21
- }
22
- if (lower.includes('already exists')) {
23
- return { status: 409, code: 'CONFLICT', message };
24
- }
25
- if (lower.includes('not found')) {
26
- return { status: 404, code: 'NOT_FOUND', message };
27
- }
28
- if (lower.includes('access denied')) {
29
- return { status: 403, code: 'ACCESS_DENIED', message };
30
- }
31
- if (lower.includes('required') || lower.includes('invalid') || lower.includes('must be')) {
32
- return { status: 400, code: 'VALIDATION_ERROR', message };
33
- }
34
- return { status: 500, code: 'INTERNAL_ERROR', message };
35
- }
36
-
37
- /**
38
- * Classify a view-level error into { status, code, message }.
39
- * @param {Error} error
40
- * @param {string} fallbackMessage
41
- * @returns {{ status: number, code: string, message: string }}
42
- */
43
- export function classifyViewError(error, fallbackMessage = 'Request failed') {
44
- const message = String(error?.message || fallbackMessage);
45
- const lower = message.toLowerCase();
46
-
47
- if (lower.includes('no bucket selected') || lower.includes('not found in bucketconnection')) {
48
- return { status: 400, code: 'INVALID_BUCKET', message };
49
- }
50
- if (lower.includes('access denied')) {
51
- return { status: 403, code: 'ACCESS_DENIED', message };
52
- }
53
- if (lower.includes('not found')) {
54
- return { status: 404, code: 'NOT_FOUND', message };
55
- }
56
- if (lower.includes('invalid') || lower.includes('required')) {
57
- return { status: 400, code: 'VALIDATION_ERROR', message };
58
- }
59
- return { status: 500, code: 'INTERNAL_ERROR', message };
60
- }
61
-
62
- /**
63
- * Send a classified API error as JSON.
64
- */
65
- export function sendApiError(res, error, fallbackMessage = 'Request failed') {
66
- const mapped = classifyApiError(error, fallbackMessage);
67
- return res.status(mapped.status).json({ success: false, code: mapped.code, error: mapped.message });
68
- }
69
-
70
- /**
71
- * Send a classified view error as JSON.
72
- */
73
- export function sendViewError(res, error, fallbackMessage = 'Request failed') {
74
- const mapped = classifyViewError(error, fallbackMessage);
75
- return res.status(mapped.status).json({ code: mapped.code, message: mapped.message });
76
- }
@@ -1,197 +0,0 @@
1
- /**
2
- * Shared helper utilities used across controllers and routes.
3
- */
4
-
5
- /**
6
- * HTML-escape a string value to prevent XSS.
7
- */
8
- export function escapeHtml(value = '') {
9
- return String(value)
10
- .replace(/&/g, '&')
11
- .replace(/</g, '&lt;')
12
- .replace(/>/g, '&gt;')
13
- .replace(/"/g, '&quot;')
14
- .replace(/'/g, '&#39;');
15
- }
16
-
17
- /**
18
- * Extract the base filename from a path (everything after the last '/').
19
- */
20
- export function getBaseName(path = '') {
21
- const value = String(path || '');
22
- const lastSlashIndex = value.lastIndexOf('/');
23
- return lastSlashIndex === -1 ? value : value.substring(lastSlashIndex + 1);
24
- }
25
-
26
- // ---------------------------------------------------------------------------
27
- // MIME type & file extension constants (used by view controller)
28
- // ---------------------------------------------------------------------------
29
-
30
- export const VIEWABLE_TYPES = new Set([
31
- 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
32
- 'mp4', 'webm', 'ogg', 'avi', 'mov',
33
- 'mp3', 'wav', 'flac', 'aac', 'm4a',
34
- 'txt', 'md', 'json', 'xml', 'csv', 'log',
35
- 'js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h',
36
- 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml',
37
- 'toml', 'ini', 'conf', 'pdf'
38
- ]);
39
-
40
- export const VIEWABLE_TYPES_SORTED = Array.from(VIEWABLE_TYPES).sort();
41
-
42
- export const STATIC_ASSET_TYPES = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'css', 'js']);
43
- export const VIDEO_TYPES = new Set(['mp4', 'webm', 'ogg', 'avi', 'mov']);
44
- export const AUDIO_TYPES = new Set(['mp3', 'wav', 'flac', 'aac', 'm4a']);
45
-
46
- export const MIME_TYPES = {
47
- // Images
48
- 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
49
- 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
50
- 'bmp': 'image/bmp', 'ico': 'image/x-icon',
51
- // Videos
52
- 'mp4': 'video/mp4', 'webm': 'video/webm', 'ogg': 'video/ogg',
53
- 'avi': 'video/x-msvideo', 'mov': 'video/quicktime',
54
- // Audio
55
- 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'flac': 'audio/flac',
56
- 'aac': 'audio/aac', 'm4a': 'audio/mp4',
57
- // Text / Code
58
- 'txt': 'text/plain; charset=utf-8', 'md': 'text/markdown; charset=utf-8',
59
- 'json': 'application/json; charset=utf-8', 'xml': 'application/xml; charset=utf-8',
60
- 'csv': 'text/csv; charset=utf-8', 'log': 'text/plain; charset=utf-8',
61
- 'js': 'text/javascript; charset=utf-8', 'ts': 'text/typescript; charset=utf-8',
62
- 'html': 'text/html; charset=utf-8', 'htm': 'text/html; charset=utf-8',
63
- 'css': 'text/css; charset=utf-8', 'php': 'text/x-php; charset=utf-8',
64
- 'py': 'text/x-python; charset=utf-8', 'java': 'text/x-java-source; charset=utf-8',
65
- 'cpp': 'text/x-c++src; charset=utf-8', 'c': 'text/x-csrc; charset=utf-8',
66
- 'h': 'text/x-chdr; charset=utf-8', 'cs': 'text/x-csharp; charset=utf-8',
67
- 'rb': 'text/x-ruby; charset=utf-8', 'go': 'text/x-go; charset=utf-8',
68
- 'rs': 'text/x-rust; charset=utf-8', 'sql': 'text/x-sql; charset=utf-8',
69
- 'sh': 'text/x-shellscript; charset=utf-8', 'bat': 'text/x-msdos-batch; charset=utf-8',
70
- 'ps1': 'text/x-powershell; charset=utf-8', 'yaml': 'text/x-yaml; charset=utf-8',
71
- 'yml': 'text/x-yaml; charset=utf-8', 'toml': 'text/x-toml; charset=utf-8',
72
- 'ini': 'text/x-ini; charset=utf-8', 'conf': 'text/x-conf; charset=utf-8',
73
- // Documents
74
- 'pdf': 'application/pdf'
75
- };
76
-
77
- /**
78
- * Check if a content type is sensitive (text-based) and should not be cached.
79
- */
80
- export function isSensitiveTextType(contentType = '') {
81
- const type = String(contentType || '').toLowerCase();
82
- return type.startsWith('text/')
83
- || type.includes('json')
84
- || type.includes('xml')
85
- || type.includes('javascript')
86
- || type.includes('typescript');
87
- }
88
-
89
- /**
90
- * Build a Cache-Control header value based on content type and flags.
91
- */
92
- export function buildCacheControl(contentType, { isStaticAsset = false, supportsRanges = false, publicCache = false } = {}) {
93
- if (publicCache) {
94
- if (isStaticAsset) return 'public, max-age=3600, immutable';
95
- if (supportsRanges) return 'public, max-age=86400, must-revalidate';
96
- return 'public, max-age=300, must-revalidate';
97
- }
98
-
99
- if (isSensitiveTextType(contentType)) return 'private, no-store';
100
- if (supportsRanges) return 'private, max-age=1800, must-revalidate';
101
- return 'private, max-age=300, must-revalidate';
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Range header parsing (used by view controller for media streaming)
106
- // ---------------------------------------------------------------------------
107
-
108
- const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB
109
-
110
- /**
111
- * Parse an HTTP Range header (`bytes=start-end`) and compute validated byte offsets.
112
- * Returns `null` if the range is invalid or unsatisfiable (caller should send 416).
113
- */
114
- export function parseRangeHeader(rangeHeader, totalSize) {
115
- if (!totalSize || totalSize <= 0) return null;
116
-
117
- const match = /^bytes=(\d*)-(\d*)$/.exec(String(rangeHeader || '').trim());
118
- if (!match) return null;
119
-
120
- const [, startStr, endStr] = match;
121
- let start, end;
122
-
123
- if (startStr === '') {
124
- // suffix range: bytes=-N (last N bytes)
125
- const suffix = parseInt(endStr, 10);
126
- if (isNaN(suffix) || suffix <= 0) return null;
127
- start = Math.max(0, totalSize - suffix);
128
- end = totalSize - 1;
129
- } else {
130
- start = parseInt(startStr, 10);
131
- if (isNaN(start) || start < 0) return null;
132
- if (endStr === '') {
133
- end = Math.min(start + DEFAULT_CHUNK_SIZE - 1, totalSize - 1);
134
- } else {
135
- end = parseInt(endStr, 10);
136
- if (isNaN(end) || end < start) return null;
137
- }
138
- }
139
-
140
- if (start >= totalSize || start > end || end >= totalSize) return null;
141
-
142
- return { start, end, total: totalSize, size: end - start + 1 };
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // Player page HTML template (used by view controller)
147
- // ---------------------------------------------------------------------------
148
-
149
- export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
150
- return `<!doctype html>
151
- <html lang="en">
152
- <head>
153
- <meta charset="utf-8">
154
- <meta name="viewport" content="width=device-width,initial-scale=1">
155
- <title>Preview: ${safeFileName}</title>
156
- <style>
157
- * { margin: 0; padding: 0; box-sizing: border-box; }
158
- body { margin: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
159
- .player-container { width: 100%; max-width: 100vw; max-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
160
- video { width: 100%; height: auto; max-width: 100%; max-height: calc(100vh - 40px); object-fit: contain; background: #000; }
161
- .file-name { position: fixed; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.8); padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: 10; }
162
- </style>
163
- </head>
164
- <body>
165
- <div class="file-name">${safeFileName}</div>
166
- <div class="player-container">
167
- <video controls preload="metadata" src="/mbkbucket/view/${encodedKey}${bucketQuery}">
168
- Your browser does not support the video tag.
169
- </video>
170
- </div>
171
- </body>
172
- </html>`;
173
- }
174
-
175
- // ---------------------------------------------------------------------------
176
- // Common Handlebars helpers
177
- // ---------------------------------------------------------------------------
178
-
179
- export const commonHandlebarsHelpers = {
180
- eq: (a, b) => a === b,
181
- neq: (a, b) => a !== b,
182
- or: (...args) => args.slice(0, -1).some(Boolean),
183
- and: (...args) => args.slice(0, -1).every(Boolean),
184
- not: (val) => !val,
185
- gt: (a, b) => a > b,
186
- gte: (a, b) => a >= b,
187
- lt: (a, b) => a < b,
188
- lte: (a, b) => a <= b,
189
- includes: (arr, val) => (Array.isArray(arr) ? arr.includes(val) : false),
190
- json: (obj) => JSON.stringify(obj, null, 2),
191
- jsonStringify: (obj) => JSON.stringify(obj),
192
- getInitials: (username) => {
193
- if (!username) return '?';
194
- const parts = username.split(/[._]/);
195
- return parts.map(p => p.charAt(0)).join('').toUpperCase().substring(0, 2);
196
- },
197
- };
@@ -1,116 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { parseAndValidateMbkbucketVar, parseAndValidateBucketConnection, normalizeBooleanLike } from '../src/config/validation.js';
4
-
5
- test('parseAndValidateMbkbucketVar applies defaults when empty', () => {
6
- const cfg = parseAndValidateMbkbucketVar(undefined);
7
- assert.equal(cfg.p_view_inline, true);
8
- assert.equal(cfg.publiView_enabled, false);
9
- });
10
-
11
- test('parseAndValidateMbkbucketVar normalizes boolean-like strings', () => {
12
- const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
13
- p_view_inline: 'false',
14
- publiView_enabled: 'true'
15
- }));
16
-
17
- assert.equal(cfg.p_view_inline, false);
18
- assert.equal(cfg.publiView_enabled, true);
19
- });
20
-
21
- test('parseAndValidateMbkbucketVar rejects invalid JSON', () => {
22
- assert.throws(() => parseAndValidateMbkbucketVar('{oops'), /Invalid JSON/);
23
- });
24
-
25
- test('parseAndValidateMbkbucketVar rejects invalid boolean values', () => {
26
- assert.throws(() => parseAndValidateMbkbucketVar(JSON.stringify({
27
- p_view_inline: 'not-bool'
28
- })), /must be a boolean/);
29
- });
30
-
31
- test('parseAndValidateMbkbucketVar accepts common boolean aliases', () => {
32
- const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
33
- p_view_inline: ' ON ',
34
- publiView_enabled: '0'
35
- }));
36
-
37
- assert.equal(cfg.p_view_inline, true);
38
- assert.equal(cfg.publiView_enabled, false);
39
- });
40
-
41
- test('parseAndValidateMbkbucketVar rejects non-object values', () => {
42
- assert.throws(() => parseAndValidateMbkbucketVar('null'), /must be a valid object/);
43
- assert.throws(() => parseAndValidateMbkbucketVar('[]'), /must be a valid object/);
44
- });
45
-
46
- test('normalizeBooleanLike returns untouched for unsupported values', () => {
47
- assert.equal(normalizeBooleanLike('maybe'), 'maybe');
48
- assert.equal(normalizeBooleanLike(123), 123);
49
- });
50
-
51
- test('parseAndValidateBucketConnection accepts multi-bucket config', () => {
52
- const raw = JSON.stringify({
53
- R2_Bucket: {
54
- BUCKET_NAME: 'my-r2-bucket',
55
- ACCESS_KEY_ID: 'abc',
56
- SECRET_ACCESS_KEY: 'def',
57
- ENDPOINT: 'https://example.r2.cloudflarestorage.com'
58
- },
59
- S3_Bucket: {
60
- BUCKET_NAME: 'my-s3-bucket',
61
- ACCESS_KEY_ID: 'ghi',
62
- SECRET_ACCESS_KEY: 'jkl',
63
- ENDPOINT: 'https://s3.ap-southeast-1.amazonaws.com'
64
- }
65
- });
66
-
67
- const cfg = parseAndValidateBucketConnection(raw);
68
- assert.deepEqual(Object.keys(cfg), ['R2_Bucket', 'S3_Bucket']);
69
- });
70
-
71
- test('parseAndValidateBucketConnection returns null when unset', () => {
72
- assert.equal(parseAndValidateBucketConnection(undefined), null);
73
- assert.equal(parseAndValidateBucketConnection(''), null);
74
- });
75
-
76
- test('parseAndValidateBucketConnection rejects invalid JSON', () => {
77
- assert.throws(() => parseAndValidateBucketConnection('{oops'), /not valid JSON/);
78
- });
79
-
80
- test('parseAndValidateBucketConnection rejects empty object and array', () => {
81
- assert.throws(() => parseAndValidateBucketConnection('{}'), /non-empty object/);
82
- assert.throws(() => parseAndValidateBucketConnection('[]'), /non-empty object/);
83
- });
84
-
85
- test('parseAndValidateBucketConnection rejects missing required fields', () => {
86
- const raw = JSON.stringify({
87
- bad: {
88
- BUCKET_NAME: 'x',
89
- ACCESS_KEY_ID: 'y',
90
- ENDPOINT: 'https://example.com'
91
- }
92
- });
93
-
94
- assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
95
- });
96
-
97
- test('parseAndValidateBucketConnection rejects quoted inner object shape', () => {
98
- const raw = JSON.stringify({
99
- bad: '{"BUCKET_NAME":"x"}'
100
- });
101
-
102
- assert.throws(() => parseAndValidateBucketConnection(raw), /must be an object/);
103
- });
104
-
105
- test('parseAndValidateBucketConnection rejects empty required fields', () => {
106
- const raw = JSON.stringify({
107
- bad: {
108
- BUCKET_NAME: '',
109
- ACCESS_KEY_ID: 'abc',
110
- SECRET_ACCESS_KEY: 'def',
111
- ENDPOINT: 'https://example.com'
112
- }
113
- });
114
-
115
- assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
116
- });
File without changes
File without changes
File without changes
File without changes