mbkbucket 4.0.1 → 4.1.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.
@@ -1,80 +1,120 @@
1
- /**
2
- * MBKBucket - Normalized Storage Data Models
3
- * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import { nowIso } from "#helpers";
8
-
9
- /**
10
- * Standardized file item representation across all providers.
11
- */
12
- export class StorageFile {
13
- /**
14
- * @param {object} props
15
- * @param {string} props.key - Normalized virtual key / path
16
- * @param {number} [props.size=0] - File size in bytes
17
- * @param {Date|string} [props.lastModified] - Last modified timestamp
18
- * @param {string} [props.etag] - Entity tag / checksum
19
- * @param {string} [props.contentType] - MIME content type
20
- * @param {string} [props.storageClass] - Storage tier / provider type
21
- * @param {string} [props.id] - Native provider ID (e.g. Google Drive file ID)
22
- * @param {string} [props.webViewLink] - Web preview link
23
- * @param {string} [props.webContentLink] - Direct download link
24
- * @param {object} [props.metadata={}] - Custom user metadata
25
- */
26
- constructor(props) {
27
- this.Key = props.key || props.Key || '';
28
- this.Size = Number(props.size ?? props.Size ?? 0);
29
- this.LastModified = props.lastModified instanceof Date
30
- ? props.lastModified
31
- : new Date(props.lastModified || props.LastModified || Date.now());
32
- this.ETag = props.etag || props.ETag || '';
33
- this.ContentType = props.contentType || props.ContentType || 'application/octet-stream';
34
- this.StorageClass = props.storageClass || props.StorageClass || 'STANDARD';
35
- if (props.id) this.id = props.id;
36
- if (props.mimeType) this.mimeType = props.mimeType;
37
- if (props.webViewLink) this.webViewLink = props.webViewLink;
38
- if (props.webContentLink) this.webContentLink = props.webContentLink;
39
- if (props.metadata) this.metadata = props.metadata;
40
- }
41
- }
42
-
43
- /**
44
- * Standardized folder representation.
45
- */
46
- export class StorageFolder {
47
- /**
48
- * @param {string} prefix - Folder prefix with trailing slash
49
- * @param {string} [id] - Native provider folder ID
50
- */
51
- constructor(prefix, id) {
52
- this.Prefix = String(prefix || '').endsWith('/') ? String(prefix) : `${prefix}/`;
53
- if (id) this.id = id;
54
- }
55
- }
56
-
57
- /**
58
- * Standardized paginated listing result.
59
- */
60
- export class StorageListResult {
61
- /**
62
- * @param {object} props
63
- * @param {StorageFile[]} [props.contents=[]]
64
- * @param {StorageFolder[]} [props.commonPrefixes=[]]
65
- * @param {string|null} [props.nextToken=null]
66
- * @param {boolean} [props.isTruncated=false]
67
- * @param {number} [props.totalFiles]
68
- */
69
- constructor(props = {}) {
70
- this.Contents = props.contents || props.Contents || [];
71
- this.CommonPrefixes = props.commonPrefixes || props.CommonPrefixes || [];
72
- this.NextContinuationToken = props.nextToken ?? props.NextContinuationToken ?? null;
73
- this.nextToken = this.NextContinuationToken;
74
- this.IsTruncated = Boolean(props.isTruncated ?? props.IsTruncated ?? Boolean(this.NextContinuationToken));
75
- this.hasMore = this.IsTruncated;
76
- this.KeyCount = this.Contents.length;
77
- this.totalFiles = props.totalFiles ?? this.Contents.length;
78
- this.requestedAt = nowIso();
79
- }
80
- }
1
+ /**
2
+ * MBKBucket - Normalized Storage Data Models
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { nowIso, getBaseName, getFolderPath, trimLeadingSlashes, trimSlashes } from "#helpers";
8
+
9
+ export class StorageItem {
10
+ constructor(props = {}) {
11
+ const rawPath = String(props.path || props.key || props.Key || props.prefix || props.Prefix || '');
12
+ this.path = props.type === 'folder'
13
+ ? (rawPath ? (rawPath.endsWith('/') ? rawPath : `${rawPath}/`) : '')
14
+ : trimLeadingSlashes(rawPath);
15
+
16
+ this.type = props.type || (this.path.endsWith('/') ? 'folder' : 'file');
17
+ this.name = props.name || (this.type === 'folder' ? trimSlashes(getBaseName(this.path.replace(/\/+$/, ''))) : getBaseName(this.path));
18
+ this.parentPath = props.parentPath ?? (this.type === 'folder'
19
+ ? trimSlashes(getFolderPath(this.path.replace(/\/+$/, '')))
20
+ : trimSlashes(getFolderPath(this.path)));
21
+
22
+ this.id = props.id || props.nativeId || this.path;
23
+ this.nativeId = props.nativeId || props.id || null;
24
+ this.provider = props.provider || props.storageClass || props.StorageClass || 'generic';
25
+
26
+ this.size = this.type === 'folder' ? 0 : Number(props.size ?? props.Size ?? 0);
27
+ this.mimeType = props.mimeType || props.contentType || props.ContentType || (this.type === 'folder' ? 'application/x-directory' : 'application/octet-stream');
28
+ this.etag = props.etag || props.ETag || '';
29
+
30
+ const modDate = props.lastModified || props.LastModified || props.updatedAt;
31
+ this.lastModified = modDate instanceof Date ? modDate : new Date(modDate || Date.now());
32
+
33
+ if (props.createdAt || props.createdTime) {
34
+ const createDate = props.createdAt || props.createdTime;
35
+ this.createdAt = createDate instanceof Date ? createDate : new Date(createDate);
36
+ }
37
+
38
+ this.metadata = Object.freeze({ ...(props.metadata || {}) });
39
+ }
40
+
41
+ get isFolder() {
42
+ return this.type === 'folder';
43
+ }
44
+
45
+ get isFile() {
46
+ return this.type === 'file';
47
+ }
48
+ }
49
+
50
+ export class StorageFile extends StorageItem {
51
+ constructor(props = {}) {
52
+ const key = props.key || props.Key || props.path || '';
53
+ super({
54
+ ...props,
55
+ type: 'file',
56
+ path: key,
57
+ size: props.size ?? props.Size ?? 0,
58
+ mimeType: props.mimeType || props.contentType || props.ContentType,
59
+ lastModified: props.lastModified || props.LastModified,
60
+ etag: props.etag || props.ETag,
61
+ provider: props.storageClass || props.StorageClass || props.provider || 'STANDARD',
62
+ id: props.id,
63
+ nativeId: props.id || props.nativeId
64
+ });
65
+
66
+ // Backward-compatible properties for S3 consumers
67
+ this.Key = this.path;
68
+ this.Size = this.size;
69
+ this.LastModified = this.lastModified;
70
+ this.ETag = this.etag;
71
+ this.ContentType = this.mimeType;
72
+ this.StorageClass = this.provider;
73
+
74
+ if (props.id) this.id = props.id;
75
+ if (props.mimeType) this.mimeType = props.mimeType;
76
+ if (props.webViewLink) this.webViewLink = props.webViewLink;
77
+ if (props.webContentLink) this.webContentLink = props.webContentLink;
78
+ }
79
+ }
80
+
81
+ export class StorageFolder extends StorageItem {
82
+ constructor(prefixOrProps, id) {
83
+ const props = typeof prefixOrProps === 'object' && prefixOrProps !== null
84
+ ? prefixOrProps
85
+ : { prefix: prefixOrProps, id };
86
+
87
+ const rawPrefix = String(props.prefix || props.Prefix || props.path || '');
88
+ const cleanPrefix = rawPrefix ? (rawPrefix.endsWith('/') ? rawPrefix : `${rawPrefix}/`) : '';
89
+
90
+ super({
91
+ ...props,
92
+ type: 'folder',
93
+ path: cleanPrefix,
94
+ id: props.id || id,
95
+ nativeId: props.id || id
96
+ });
97
+
98
+ // Backward-compatible property for S3 consumers
99
+ this.Prefix = this.path;
100
+ if (this.id) this.id = this.id;
101
+ }
102
+ }
103
+
104
+ export class StorageListResult {
105
+ constructor(props = {}) {
106
+ this.Contents = props.contents || props.Contents || [];
107
+ this.CommonPrefixes = props.commonPrefixes || props.CommonPrefixes || [];
108
+ this.NextContinuationToken = props.nextToken ?? props.NextContinuationToken ?? null;
109
+ this.nextToken = this.NextContinuationToken;
110
+ this.IsTruncated = Boolean(props.isTruncated ?? props.IsTruncated ?? Boolean(this.NextContinuationToken));
111
+ this.hasMore = this.IsTruncated;
112
+ this.KeyCount = this.Contents.length;
113
+ this.totalFiles = props.totalFiles ?? this.Contents.length;
114
+ this.requestedAt = nowIso();
115
+
116
+ this.files = this.Contents;
117
+ this.folders = this.CommonPrefixes;
118
+ this.items = [...this.CommonPrefixes, ...this.Contents];
119
+ }
120
+ }
@@ -1,65 +1,78 @@
1
- /**
2
- * MBKBucket - Path & Key Strategy
3
- * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import { mbkautheVar } from "mbkauthe";
8
- import { StorageValidationError } from "./errors.js";
9
- import { trimLeadingSlashes, trimSlashes } from "#helpers";
10
-
11
- const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
-
13
- export class PathStrategy {
14
- static getAppName() {
15
- const app = mbkautheVar?.APP_NAME;
16
- return typeof app === 'string' && app.trim() ? app.trim() : '';
17
- }
18
-
19
- static isRootModeApp(appName) {
20
- return ['portal', 'mbkbucket'].includes(String(appName || '').toLowerCase());
21
- }
22
-
23
- static validateSafety(str, isKey = false) {
24
- if (isKey && !str) throw new StorageValidationError('Key is required');
25
- if (str && (str.includes('../') || str.includes('..\\') || /\.\.[\\/]/.test(str))) {
26
- throw new StorageValidationError(`Path traversal detected: invalid ${isKey ? 'key' : 'prefix'}`);
27
- }
28
- if (str && /[\x00-\x1f\x7f]/.test(str)) {
29
- throw new StorageValidationError(`Invalid characters in ${isKey ? 'key' : 'prefix'}`);
30
- }
31
- }
32
-
33
- static applyKeyPrefix(key = '') {
34
- if (Array.isArray(key)) key = key.join('/');
35
- this.validateSafety(key, true);
36
-
37
- const app = this.getAppName();
38
- const cleaned = trimLeadingSlashes(key);
39
- if (this.isRootModeApp(app)) return cleaned;
40
- if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
41
-
42
- const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
43
- return cleaned.startsWith(`${app}/`) ? cleaned.replace(appPrefixRegex, `${app}/`) : `${app}/${cleaned}`;
44
- }
45
-
46
- static applyPrefix(prefix = '') {
47
- if (Array.isArray(prefix)) prefix = prefix.join('/');
48
- this.validateSafety(prefix, false);
49
-
50
- const app = this.getAppName();
51
- const p = trimLeadingSlashes(prefix);
52
- if (this.isRootModeApp(app)) return p;
53
- if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
54
- if (!p || p === app || p === `${app}/`) return p || app;
55
-
56
- return p.startsWith(`${app}/`)
57
- ? p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`)
58
- : `${app}/${p}`;
59
- }
60
-
61
- static normalizeFolderPrefix(prefix = '') {
62
- const p = this.applyPrefix(trimSlashes(prefix));
63
- return p ? `${p}/` : '';
64
- }
65
- }
1
+ /**
2
+ * MBKBucket - Path & Key Strategy
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { mbkautheVar } from "mbkauthe";
8
+ import { StorageValidationError } from "./errors.js";
9
+ import { trimLeadingSlashes, trimSlashes } from "#helpers";
10
+
11
+ const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+
13
+ let _configuredAppName = null;
14
+
15
+ export class PathStrategy {
16
+ static setAppName(name) {
17
+ _configuredAppName = name ? String(name).trim() : null;
18
+ }
19
+
20
+ static resetAppName() {
21
+ _configuredAppName = null;
22
+ }
23
+
24
+ static getAppName() {
25
+ if (_configuredAppName !== null) {
26
+ return _configuredAppName;
27
+ }
28
+ const app = mbkautheVar?.APP_NAME || process.env.MBKAUTHE_APP_NAME || process.env.APP_NAME;
29
+ return typeof app === 'string' && app.trim() ? app.trim() : '';
30
+ }
31
+
32
+ static isRootModeApp(appName) {
33
+ return ['portal', 'mbkbucket'].includes(String(appName || '').toLowerCase());
34
+ }
35
+
36
+ static validateSafety(str, isKey = false) {
37
+ if (isKey && !str) throw new StorageValidationError('Key is required');
38
+ if (str && (str.includes('../') || str.includes('..\\') || /\.\.[\\/]/.test(str))) {
39
+ throw new StorageValidationError(`Path traversal detected: invalid ${isKey ? 'key' : 'prefix'}`);
40
+ }
41
+ if (str && /[\x00-\x1f\x7f]/.test(str)) {
42
+ throw new StorageValidationError(`Invalid characters in ${isKey ? 'key' : 'prefix'}`);
43
+ }
44
+ }
45
+
46
+ static applyKeyPrefix(key = '') {
47
+ if (Array.isArray(key)) key = key.join('/');
48
+ this.validateSafety(key, true);
49
+
50
+ const app = this.getAppName();
51
+ const cleaned = trimLeadingSlashes(key);
52
+ if (this.isRootModeApp(app)) return cleaned;
53
+ if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
54
+
55
+ const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
56
+ return cleaned.startsWith(`${app}/`) ? cleaned.replace(appPrefixRegex, `${app}/`) : `${app}/${cleaned}`;
57
+ }
58
+
59
+ static applyPrefix(prefix = '') {
60
+ if (Array.isArray(prefix)) prefix = prefix.join('/');
61
+ this.validateSafety(prefix, false);
62
+
63
+ const app = this.getAppName();
64
+ const p = trimLeadingSlashes(prefix);
65
+ if (this.isRootModeApp(app)) return p;
66
+ if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
67
+ if (!p || p === app || p === `${app}/`) return p || app;
68
+
69
+ return p.startsWith(`${app}/`)
70
+ ? p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`)
71
+ : `${app}/${p}`;
72
+ }
73
+
74
+ static normalizeFolderPrefix(prefix = '') {
75
+ const p = this.applyPrefix(trimSlashes(prefix));
76
+ return p ? `${p}/` : '';
77
+ }
78
+ }