mbkbucket 4.1.0 → 4.2.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,120 +1,121 @@
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
+ /**
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, getFolderPath } from "#helpers";
8
+ import { getBaseName, trimLeadingSlashes, trimSlashes } from "mbkutils";
9
+
10
+ export class StorageItem {
11
+ constructor(props = {}) {
12
+ const rawPath = String(props.path || props.key || props.Key || props.prefix || props.Prefix || '');
13
+ this.path = props.type === 'folder'
14
+ ? (rawPath ? (rawPath.endsWith('/') ? rawPath : `${rawPath}/`) : '')
15
+ : trimLeadingSlashes(rawPath);
16
+
17
+ this.type = props.type || (this.path.endsWith('/') ? 'folder' : 'file');
18
+ this.name = props.name || (this.type === 'folder' ? trimSlashes(getBaseName(this.path.replace(/\/+$/, ''))) : getBaseName(this.path));
19
+ this.parentPath = props.parentPath ?? (this.type === 'folder'
20
+ ? trimSlashes(getFolderPath(this.path.replace(/\/+$/, '')))
21
+ : trimSlashes(getFolderPath(this.path)));
22
+
23
+ this.id = props.id || props.nativeId || this.path;
24
+ this.nativeId = props.nativeId || props.id || null;
25
+ this.provider = props.provider || props.storageClass || props.StorageClass || 'generic';
26
+
27
+ this.size = this.type === 'folder' ? 0 : Number(props.size ?? props.Size ?? 0);
28
+ this.mimeType = props.mimeType || props.contentType || props.ContentType || (this.type === 'folder' ? 'application/x-directory' : 'application/octet-stream');
29
+ this.etag = props.etag || props.ETag || '';
30
+
31
+ const modDate = props.lastModified || props.LastModified || props.updatedAt;
32
+ this.lastModified = modDate instanceof Date ? modDate : new Date(modDate || Date.now());
33
+
34
+ if (props.createdAt || props.createdTime) {
35
+ const createDate = props.createdAt || props.createdTime;
36
+ this.createdAt = createDate instanceof Date ? createDate : new Date(createDate);
37
+ }
38
+
39
+ this.metadata = Object.freeze({ ...(props.metadata || {}) });
40
+ }
41
+
42
+ get isFolder() {
43
+ return this.type === 'folder';
44
+ }
45
+
46
+ get isFile() {
47
+ return this.type === 'file';
48
+ }
49
+ }
50
+
51
+ export class StorageFile extends StorageItem {
52
+ constructor(props = {}) {
53
+ const key = props.key || props.Key || props.path || '';
54
+ super({
55
+ ...props,
56
+ type: 'file',
57
+ path: key,
58
+ size: props.size ?? props.Size ?? 0,
59
+ mimeType: props.mimeType || props.contentType || props.ContentType,
60
+ lastModified: props.lastModified || props.LastModified,
61
+ etag: props.etag || props.ETag,
62
+ provider: props.storageClass || props.StorageClass || props.provider || 'STANDARD',
63
+ id: props.id,
64
+ nativeId: props.id || props.nativeId
65
+ });
66
+
67
+ // Backward-compatible properties for S3 consumers
68
+ this.Key = this.path;
69
+ this.Size = this.size;
70
+ this.LastModified = this.lastModified;
71
+ this.ETag = this.etag;
72
+ this.ContentType = this.mimeType;
73
+ this.StorageClass = this.provider;
74
+
75
+ if (props.id) this.id = props.id;
76
+ if (props.mimeType) this.mimeType = props.mimeType;
77
+ if (props.webViewLink) this.webViewLink = props.webViewLink;
78
+ if (props.webContentLink) this.webContentLink = props.webContentLink;
79
+ }
80
+ }
81
+
82
+ export class StorageFolder extends StorageItem {
83
+ constructor(prefixOrProps, id) {
84
+ const props = typeof prefixOrProps === 'object' && prefixOrProps !== null
85
+ ? prefixOrProps
86
+ : { prefix: prefixOrProps, id };
87
+
88
+ const rawPrefix = String(props.prefix || props.Prefix || props.path || '');
89
+ const cleanPrefix = rawPrefix ? (rawPrefix.endsWith('/') ? rawPrefix : `${rawPrefix}/`) : '';
90
+
91
+ super({
92
+ ...props,
93
+ type: 'folder',
94
+ path: cleanPrefix,
95
+ id: props.id || id,
96
+ nativeId: props.id || id
97
+ });
98
+
99
+ // Backward-compatible property for S3 consumers
100
+ this.Prefix = this.path;
101
+ if (this.id) this.id = this.id;
102
+ }
103
+ }
104
+
105
+ export class StorageListResult {
106
+ constructor(props = {}) {
107
+ this.Contents = props.contents || props.Contents || [];
108
+ this.CommonPrefixes = props.commonPrefixes || props.CommonPrefixes || [];
109
+ this.NextContinuationToken = props.nextToken ?? props.NextContinuationToken ?? null;
110
+ this.nextToken = this.NextContinuationToken;
111
+ this.IsTruncated = Boolean(props.isTruncated ?? props.IsTruncated ?? Boolean(this.NextContinuationToken));
112
+ this.hasMore = this.IsTruncated;
113
+ this.KeyCount = this.Contents.length;
114
+ this.totalFiles = props.totalFiles ?? this.Contents.length;
115
+ this.requestedAt = nowIso();
116
+
117
+ this.files = this.Contents;
118
+ this.folders = this.CommonPrefixes;
119
+ this.items = [...this.CommonPrefixes, ...this.Contents];
120
+ }
121
+ }
@@ -1,78 +1,85 @@
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
- }
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 "mbkutils";
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
+ let envAppName = null;
29
+ if (process.env.mbkautheVar) {
30
+ try {
31
+ const parsed = typeof process.env.mbkautheVar === 'string' ? JSON.parse(process.env.mbkautheVar) : process.env.mbkautheVar;
32
+ if (parsed?.APP_NAME) envAppName = parsed.APP_NAME;
33
+ } catch {}
34
+ }
35
+ const app = envAppName || process.env.MBKAUTHE_APP_NAME || process.env.APP_NAME || mbkautheVar?.APP_NAME;
36
+ return typeof app === 'string' && app.trim() ? app.trim() : '';
37
+ }
38
+
39
+ static isRootModeApp(appName) {
40
+ return ['portal', 'mbkbucket'].includes(String(appName || '').toLowerCase());
41
+ }
42
+
43
+ static validateSafety(str, isKey = false) {
44
+ if (isKey && !str) throw new StorageValidationError('Key is required');
45
+ if (str && (str.includes('../') || str.includes('..\\') || /\.\.[\\/]/.test(str))) {
46
+ throw new StorageValidationError(`Path traversal detected: invalid ${isKey ? 'key' : 'prefix'}`);
47
+ }
48
+ if (str && /[\x00-\x1f\x7f]/.test(str)) {
49
+ throw new StorageValidationError(`Invalid characters in ${isKey ? 'key' : 'prefix'}`);
50
+ }
51
+ }
52
+
53
+ static applyKeyPrefix(key = '') {
54
+ if (Array.isArray(key)) key = key.join('/');
55
+ this.validateSafety(key, true);
56
+
57
+ const app = this.getAppName();
58
+ const cleaned = trimLeadingSlashes(key);
59
+ if (this.isRootModeApp(app)) return cleaned;
60
+ if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
61
+
62
+ const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
63
+ return cleaned.startsWith(`${app}/`) ? cleaned.replace(appPrefixRegex, `${app}/`) : `${app}/${cleaned}`;
64
+ }
65
+
66
+ static applyPrefix(prefix = '') {
67
+ if (Array.isArray(prefix)) prefix = prefix.join('/');
68
+ this.validateSafety(prefix, false);
69
+
70
+ const app = this.getAppName();
71
+ const p = trimLeadingSlashes(prefix);
72
+ if (this.isRootModeApp(app)) return p;
73
+ if (!app) throw new StorageValidationError('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
74
+ if (!p || p === app || p === `${app}/`) return p || app;
75
+
76
+ return p.startsWith(`${app}/`)
77
+ ? p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`)
78
+ : `${app}/${p}`;
79
+ }
80
+
81
+ static normalizeFolderPrefix(prefix = '') {
82
+ const p = this.applyPrefix(trimSlashes(prefix));
83
+ return p ? `${p}/` : '';
84
+ }
85
+ }