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.
- package/index.js +8 -18
- package/lib/cli.js +642 -0
- package/{public → lib/public}/bucketadmin.js +1 -1
- package/{src → lib/src}/app.js +37 -50
- package/{src → lib/src}/cli/auth-helper.js +24 -103
- package/lib/src/config/index.js +108 -0
- package/{src → lib/src}/config/validation.js +14 -30
- package/{src → lib/src}/controllers/api.controller.js +85 -176
- package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
- package/{src → lib/src}/controllers/info.controller.js +17 -20
- package/{src → lib/src}/controllers/view.controller.js +23 -70
- package/{src → lib/src}/middleware/async-handler.js +1 -16
- package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
- package/{src → lib/src}/middleware/error-handler.js +6 -14
- package/{src → lib/src}/middleware/rate-limiter.js +2 -10
- package/{src → lib/src}/middleware/security.js +1 -17
- package/lib/src/routes/api.routes.js +40 -0
- package/{src → lib/src}/routes/index.js +1 -9
- package/{src → lib/src}/routes/info.routes.js +2 -3
- package/{src → lib/src}/routes/view.routes.js +3 -8
- package/{src → lib/src}/services/s3.service.js +157 -349
- package/lib/src/utils/errors.js +45 -0
- package/lib/src/utils/helpers.js +251 -0
- package/package.json +18 -3
- package/.github/PACKAGE.md +0 -19
- package/.github/workflows/publish.yml +0 -88
- package/cli.js +0 -977
- package/index.d.ts +0 -444
- package/lib/index.js +0 -17
- package/src/config/index.js +0 -133
- package/src/routes/api.routes.js +0 -66
- package/src/utils/errors.js +0 -76
- package/src/utils/helpers.js +0 -197
- package/test/config-validation.test.js +0 -116
- /package/{public → lib/public}/bucketadmin.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
- /package/{public → lib/public}/mbkbucket-helper.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper.js +0 -0
- /package/{src → lib/src}/utils/logger.js +0 -0
package/{src → lib/src}/app.js
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
3
4
|
import { fileURLToPath } from "url";
|
|
4
5
|
import { engine } from "express-handlebars";
|
|
5
6
|
import compression from "compression";
|
|
@@ -8,34 +9,25 @@ import { generalLimiter } from "./middleware/rate-limiter.js";
|
|
|
8
9
|
import { notFoundHandler, globalErrorHandler } from "./middleware/error-handler.js";
|
|
9
10
|
import bucketRoutes from "./routes/index.js";
|
|
10
11
|
import { packageJson } from "./config/index.js";
|
|
11
|
-
import { createLogger } from "
|
|
12
|
+
import { createLogger } from "#logger";
|
|
12
13
|
import { commonHandlebarsHelpers } from "./utils/helpers.js";
|
|
13
14
|
|
|
14
15
|
const debugServer = createLogger('server');
|
|
15
|
-
|
|
16
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
17
|
const __dirname = path.dirname(__filename);
|
|
18
18
|
const isDevMode = process.env.NODE_ENV === "dev";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Creates and configures the Express application.
|
|
22
|
-
* @returns {express.Application}
|
|
23
|
-
*/
|
|
24
20
|
export function createApp() {
|
|
25
21
|
const server = express();
|
|
26
22
|
server.set('trust proxy', 1);
|
|
27
23
|
|
|
28
|
-
// Compression
|
|
29
24
|
server.use(compression());
|
|
30
|
-
|
|
31
|
-
// Body parsing
|
|
32
25
|
server.use(express.json({ limit: '10mb' }));
|
|
33
26
|
server.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
34
27
|
|
|
35
|
-
// Static
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
res.set('Cache-Control', dashboardAssetCacheControl);
|
|
28
|
+
// Static dashboard & helper assets
|
|
29
|
+
const serveAsset = (filename) => (_req, res) => {
|
|
30
|
+
res.set('Cache-Control', 'public, max-age=300, must-revalidate');
|
|
39
31
|
res.sendFile(path.join(__dirname, '..', 'public', filename), (err) => {
|
|
40
32
|
if (err) res.status(err.status || 404).end();
|
|
41
33
|
});
|
|
@@ -43,71 +35,66 @@ export function createApp() {
|
|
|
43
35
|
|
|
44
36
|
server.get('/mbkbucket/bucketadmin.css', serveAsset('bucketadmin.css'));
|
|
45
37
|
server.get('/mbkbucket/bucketadmin.js', serveAsset('bucketadmin.js'));
|
|
46
|
-
|
|
47
|
-
// Frontend helper utilities (drop-in for any HTML page)
|
|
48
38
|
server.get('/mbkbucket/mbkbucket-helper.css', serveAsset('mbkbucket-helper.css'));
|
|
49
39
|
server.get('/mbkbucket/mbkbucket-helper.js', serveAsset('mbkbucket-helper.js'));
|
|
50
|
-
|
|
51
|
-
// Helper component demo page (no auth — purely static HTML)
|
|
52
40
|
server.get('/mbkbucket/helper-demo', serveAsset('mbkbucket-helper-demo.html'));
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
const rootDir = path.resolve(__dirname, "..", "..");
|
|
43
|
+
const libDir = path.resolve(__dirname, "..");
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
|
|
46
|
+
const viewDirs = [
|
|
47
|
+
path.join(rootDir, "views"),
|
|
48
|
+
path.join(libDir, "views"),
|
|
49
|
+
path.join(rootDir, "node_modules", "mbkauthe", "views"),
|
|
50
|
+
path.resolve(cwd, "views"),
|
|
51
|
+
path.resolve(cwd, "node_modules", "mbkauthe", "views"),
|
|
52
|
+
].filter(dir => fs.existsSync(dir));
|
|
53
|
+
|
|
54
|
+
const partialsDir = [
|
|
55
|
+
path.join(rootDir, "views", "templates"),
|
|
56
|
+
path.join(rootDir, "views", "templates", "notice"),
|
|
57
|
+
path.join(rootDir, "views"),
|
|
58
|
+
path.join(rootDir, "views", "partial"),
|
|
59
|
+
path.join(libDir, "views", "templates"),
|
|
60
|
+
path.join(libDir, "views", "templates", "notice"),
|
|
61
|
+
path.join(libDir, "views"),
|
|
62
|
+
path.join(libDir, "views", "partial"),
|
|
63
|
+
path.join(rootDir, "node_modules", "mbkauthe", "views"),
|
|
64
|
+
path.resolve(cwd, "node_modules", "mbkauthe", "views"),
|
|
65
|
+
].filter(dir => fs.existsSync(dir));
|
|
66
|
+
|
|
67
|
+
// Handlebars template engine
|
|
55
68
|
server.engine("handlebars", engine({
|
|
56
69
|
extname: ".handlebars",
|
|
57
70
|
defaultLayout: false,
|
|
58
|
-
partialsDir: [
|
|
59
|
-
path.join(__dirname, "..", "views", "templates"),
|
|
60
|
-
path.join(__dirname, "..", "views", "templates", "notice"),
|
|
61
|
-
path.join(__dirname, "..", "views"),
|
|
62
|
-
path.join(__dirname, "..", "views", "partial"),
|
|
63
|
-
path.join(__dirname, "..", "node_modules", "mbkauthe", "views"),
|
|
64
|
-
path.join(__dirname, "..", "..", "mbkauthe", "views")
|
|
65
|
-
],
|
|
71
|
+
partialsDir: [...new Set(partialsDir)],
|
|
66
72
|
cache: process.env.NODE_ENV === "production",
|
|
67
73
|
helpers: {
|
|
68
74
|
...commonHandlebarsHelpers,
|
|
69
|
-
mbkbucket_cachebuster:
|
|
70
|
-
return "?v=" + packageJson.version;
|
|
71
|
-
}
|
|
75
|
+
mbkbucket_cachebuster: () => `?v=${packageJson.version}`
|
|
72
76
|
}
|
|
73
77
|
}));
|
|
74
78
|
|
|
75
79
|
server.set("view engine", "handlebars");
|
|
76
|
-
server.set("views", [
|
|
77
|
-
path.join(__dirname, "..", "views"),
|
|
78
|
-
path.join(__dirname, "..", "node_modules", "mbkauthe", "views"),
|
|
79
|
-
path.join(__dirname, "..", "..", "mbkauthe", "views")
|
|
80
|
-
]);
|
|
80
|
+
server.set("views", [...new Set(viewDirs)]);
|
|
81
81
|
|
|
82
|
-
// Auth routes
|
|
83
82
|
server.use(mbkautheRoutes);
|
|
84
|
-
|
|
85
|
-
// Rate limiting
|
|
86
83
|
server.use(generalLimiter);
|
|
87
|
-
|
|
88
|
-
// Bucket routes
|
|
89
84
|
server.use(bucketRoutes);
|
|
90
85
|
|
|
91
|
-
// Dev-mode request timing
|
|
92
86
|
if (isDevMode) {
|
|
93
|
-
debugServer("Dev mode is enabled. Starting server in dev mode.");
|
|
94
|
-
|
|
95
87
|
server.use((req, res, next) => {
|
|
96
|
-
const start =
|
|
88
|
+
const start = Date.now();
|
|
97
89
|
res.on('finish', () => {
|
|
98
|
-
|
|
99
|
-
const ms = diff[0] * 1000 + diff[1] / 1e6;
|
|
100
|
-
debugServer("[%s] %s %s %s - %s ms", new Date().toISOString(), req.method, req.originalUrl, res.statusCode, ms.toFixed(3));
|
|
90
|
+
debugServer('%s %s %s (%sms)', req.method, req.originalUrl, res.statusCode, Date.now() - start);
|
|
101
91
|
});
|
|
102
92
|
next();
|
|
103
93
|
});
|
|
104
94
|
|
|
105
|
-
server.get("/",
|
|
106
|
-
res.redirect("/mbkbucket");
|
|
107
|
-
});
|
|
95
|
+
server.get("/", (_req, res) => res.redirect("/mbkbucket"));
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
// Error handlers
|
|
111
98
|
server.use(notFoundHandler);
|
|
112
99
|
server.use(globalErrorHandler);
|
|
113
100
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Authentication Helper
|
|
3
|
-
*
|
|
4
3
|
* Handles mbkauthe device-flow login for the mbkbucket CLI.
|
|
5
4
|
* Stores credentials in ~/.mbkbucket/config.json
|
|
6
5
|
*/
|
|
@@ -8,39 +7,27 @@
|
|
|
8
7
|
import fs from 'node:fs';
|
|
9
8
|
import path from 'node:path';
|
|
10
9
|
import os from 'node:os';
|
|
11
|
-
import { createLogger } from '
|
|
10
|
+
import { createLogger } from '#logger';
|
|
12
11
|
|
|
13
12
|
const debugCli = createLogger('cli:auth');
|
|
14
13
|
|
|
15
14
|
export const CONFIG_DIR = path.join(os.homedir(), '.mbkbucket');
|
|
16
15
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
17
|
-
const POLL_INTERVAL_MS = 5000;
|
|
18
16
|
const MAX_POLL_TIME_MS = 15 * 60 * 1000; // 15 minutes
|
|
17
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
19
18
|
|
|
20
|
-
/**
|
|
21
|
-
* Read the stored CLI config.
|
|
22
|
-
* @returns {{ token?: string, tokenPrefix?: string, username?: string, serverUrl?: string, profile?: object }}
|
|
23
|
-
*/
|
|
24
19
|
export function readConfig() {
|
|
25
20
|
try {
|
|
26
|
-
|
|
27
|
-
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
28
|
-
return JSON.parse(raw);
|
|
21
|
+
return fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) : {};
|
|
29
22
|
} catch (err) {
|
|
30
23
|
debugCli('Failed to read config: %s', err.message);
|
|
31
24
|
return {};
|
|
32
25
|
}
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
/**
|
|
36
|
-
* Write config to disk.
|
|
37
|
-
* @param {object} config
|
|
38
|
-
*/
|
|
39
28
|
export function writeConfig(config) {
|
|
40
29
|
try {
|
|
41
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
42
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
43
|
-
}
|
|
30
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
31
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
45
32
|
} catch (err) {
|
|
46
33
|
console.error(`❌ Failed to write config: ${err.message}`);
|
|
@@ -48,14 +35,9 @@ export function writeConfig(config) {
|
|
|
48
35
|
}
|
|
49
36
|
}
|
|
50
37
|
|
|
51
|
-
/**
|
|
52
|
-
* Clear stored credentials (logout).
|
|
53
|
-
*/
|
|
54
38
|
export function clearConfig() {
|
|
55
39
|
try {
|
|
56
|
-
if (fs.existsSync(CONFIG_FILE))
|
|
57
|
-
fs.unlinkSync(CONFIG_FILE);
|
|
58
|
-
}
|
|
40
|
+
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
59
41
|
return true;
|
|
60
42
|
} catch (err) {
|
|
61
43
|
debugCli('Failed to clear config: %s', err.message);
|
|
@@ -63,49 +45,27 @@ export function clearConfig() {
|
|
|
63
45
|
}
|
|
64
46
|
}
|
|
65
47
|
|
|
66
|
-
/**
|
|
67
|
-
* Merge partial config into stored config.
|
|
68
|
-
* @param {object} partial
|
|
69
|
-
*/
|
|
70
48
|
export function updateConfig(partial) {
|
|
71
|
-
const
|
|
72
|
-
const updated = { ...current, ...partial };
|
|
49
|
+
const updated = { ...readConfig(), ...partial };
|
|
73
50
|
writeConfig(updated);
|
|
74
51
|
return updated;
|
|
75
52
|
}
|
|
76
53
|
|
|
77
|
-
/**
|
|
78
|
-
* Check if the user is logged in (has a stored token).
|
|
79
|
-
*/
|
|
80
54
|
export function isLoggedIn() {
|
|
81
|
-
const
|
|
82
|
-
return
|
|
55
|
+
const { token, serverUrl } = readConfig();
|
|
56
|
+
return Boolean(token && serverUrl);
|
|
83
57
|
}
|
|
84
58
|
|
|
85
|
-
/**
|
|
86
|
-
* Get the stored API token.
|
|
87
|
-
*/
|
|
88
59
|
export function getStoredToken() {
|
|
89
60
|
return readConfig().token || null;
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
/**
|
|
93
|
-
* Get the stored server URL.
|
|
94
|
-
*/
|
|
95
63
|
export function getStoredServerUrl() {
|
|
96
64
|
return readConfig().serverUrl || null;
|
|
97
65
|
}
|
|
98
66
|
|
|
99
|
-
/**
|
|
100
|
-
* Perform a device-flow login against an mbkauthe-powered server.
|
|
101
|
-
*
|
|
102
|
-
* @param {{ serverUrl: string, profileKey?: string }} options
|
|
103
|
-
* @returns {Promise<{ token: string, tokenPrefix: string, username: string, profile: object }>}
|
|
104
|
-
*/
|
|
105
67
|
export async function deviceFlowLogin({ serverUrl, profileKey }) {
|
|
106
68
|
const baseUrl = String(serverUrl).replace(/\/+$/, '');
|
|
107
|
-
|
|
108
|
-
// --- Step 1: Request a device code ---
|
|
109
69
|
console.log(`🔑 Requesting device login from ${baseUrl} ...`);
|
|
110
70
|
|
|
111
71
|
let deviceResp;
|
|
@@ -115,53 +75,32 @@ export async function deviceFlowLogin({ serverUrl, profileKey }) {
|
|
|
115
75
|
headers: { 'Content-Type': 'application/json' },
|
|
116
76
|
body: JSON.stringify({
|
|
117
77
|
clientName: `mbkbucket-cli-${os.hostname()}`,
|
|
118
|
-
...(profileKey
|
|
78
|
+
...(profileKey && { profileKey }),
|
|
119
79
|
}),
|
|
120
80
|
});
|
|
121
|
-
|
|
122
81
|
deviceResp = await res.json();
|
|
123
|
-
|
|
124
|
-
if (!deviceResp.success) {
|
|
125
|
-
throw new Error(deviceResp.message || 'Failed to initiate device login');
|
|
126
|
-
}
|
|
82
|
+
if (!deviceResp.success) throw new Error(deviceResp.message || 'Failed to initiate device login');
|
|
127
83
|
} catch (err) {
|
|
128
84
|
if (err.message.includes('Failed to initiate') || err.message.includes('fetch')) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Could not reach mbkauthe server at ${baseUrl}/api/cli/device.\n` +
|
|
131
|
-
`Verify the server URL and that CLI_AUTH_ENABLED=true on the server.`
|
|
132
|
-
);
|
|
85
|
+
throw new Error(`Could not reach mbkauthe server at ${baseUrl}/api/cli/device.\nVerify the server URL and that CLI_AUTH_ENABLED=true on the server.`);
|
|
133
86
|
}
|
|
134
87
|
throw err;
|
|
135
88
|
}
|
|
136
89
|
|
|
137
|
-
const {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
deviceCode,
|
|
141
|
-
expiresIn,
|
|
142
|
-
interval,
|
|
143
|
-
profile,
|
|
144
|
-
} = deviceResp;
|
|
145
|
-
|
|
146
|
-
// --- Step 2: Prompt user to approve ---
|
|
147
|
-
console.log('');
|
|
148
|
-
console.log('┌─────────────────────────────────────────────────────────┐');
|
|
90
|
+
const { verificationUrl, userCode, deviceCode, expiresIn, interval, profile } = deviceResp;
|
|
91
|
+
|
|
92
|
+
console.log('\n┌─────────────────────────────────────────────────────────┐');
|
|
149
93
|
console.log('│ │');
|
|
150
|
-
console.log(
|
|
94
|
+
console.log('│ 🌐 Open this URL in your browser to approve: │');
|
|
151
95
|
console.log(`│ ${verificationUrl.padEnd(53)}│`);
|
|
152
|
-
console.log(
|
|
96
|
+
console.log('│ │');
|
|
153
97
|
console.log(`│ 🔢 Or enter this code: ${userCode.padEnd(30)}│`);
|
|
154
98
|
console.log('│ │');
|
|
155
|
-
console.log('
|
|
156
|
-
console.log('');
|
|
99
|
+
console.log('└─────────────────────────────────────────────────────────┘\n');
|
|
157
100
|
|
|
158
|
-
if (profile) {
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
console.log(` Waiting for approval (expires in ${Math.floor(expiresIn / 60)} minutes)...`);
|
|
162
|
-
console.log('');
|
|
101
|
+
if (profile) console.log(` Profile: ${profile.name} (scope: ${profile.scope || 'full'})`);
|
|
102
|
+
console.log(` Waiting for approval (expires in ${Math.floor(expiresIn / 60)} minutes)...\n`);
|
|
163
103
|
|
|
164
|
-
// --- Step 3: Poll for token ---
|
|
165
104
|
const pollInterval = (interval || 5) * 1000;
|
|
166
105
|
const deadline = Date.now() + MAX_POLL_TIME_MS;
|
|
167
106
|
let attempts = 0;
|
|
@@ -190,34 +129,16 @@ export async function deviceFlowLogin({ serverUrl, profileKey }) {
|
|
|
190
129
|
};
|
|
191
130
|
}
|
|
192
131
|
|
|
193
|
-
if (pollBody.status === 'denied')
|
|
194
|
-
|
|
195
|
-
|
|
132
|
+
if (pollBody.status === 'denied') throw new Error('Login request was denied.');
|
|
133
|
+
if (pollBody.status === 'expired') throw new Error('Login request expired. Please try again.');
|
|
134
|
+
if (pollBody.status === 'completed') throw new Error('This login session has already been completed.');
|
|
196
135
|
|
|
197
|
-
if (
|
|
198
|
-
throw new Error('Login request expired. Please try again.');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (pollBody.status === 'completed') {
|
|
202
|
-
throw new Error('This login session has already been completed.');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Still pending — show a dot every 5 polls
|
|
206
|
-
if (attempts % 5 === 0) {
|
|
207
|
-
process.stdout.write('.');
|
|
208
|
-
}
|
|
136
|
+
if (attempts % 5 === 0) process.stdout.write('.');
|
|
209
137
|
} catch (err) {
|
|
210
|
-
if (
|
|
211
|
-
throw err;
|
|
212
|
-
}
|
|
213
|
-
// Network errors during poll — retry
|
|
138
|
+
if (['denied', 'expired', 'completed'].some(k => err.message.includes(k))) throw err;
|
|
214
139
|
debugCli('Poll error (will retry): %s', err.message);
|
|
215
140
|
}
|
|
216
141
|
}
|
|
217
142
|
|
|
218
143
|
throw new Error('Login timed out. Please try again.');
|
|
219
144
|
}
|
|
220
|
-
|
|
221
|
-
function sleep(ms) {
|
|
222
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
223
|
-
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import { parseAndValidateMbkbucketVar, parseAndValidateBucketConnection } from "./validation.js";
|
|
4
|
+
import { createLogger } from "#logger";
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
const debugConfig = createLogger('config');
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// Load mbkbucket package.json
|
|
11
|
+
let packageJson;
|
|
12
|
+
try {
|
|
13
|
+
packageJson = require("mbkbucket/package.json");
|
|
14
|
+
} catch {
|
|
15
|
+
packageJson = require("../../../package.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Parent project version
|
|
19
|
+
let appVersion = "unknown";
|
|
20
|
+
try {
|
|
21
|
+
appVersion = require("../../../../package.json")?.version ?? require(`${process.cwd()}/package.json`)?.version ?? "unknown";
|
|
22
|
+
} catch {
|
|
23
|
+
try {
|
|
24
|
+
appVersion = require(`${process.cwd()}/package.json`)?.version ?? "unknown";
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function compareVersions(a = '', b = '') {
|
|
29
|
+
const left = String(a).split(/[.-]/).map(p => parseInt(p, 10) || 0);
|
|
30
|
+
const right = String(b).split(/[.-]/).map(p => parseInt(p, 10) || 0);
|
|
31
|
+
const len = Math.max(left.length, right.length);
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < len; i++) {
|
|
34
|
+
const diff = (left[i] || 0) - (right[i] || 0);
|
|
35
|
+
if (diff !== 0) return diff;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getLatestVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkbucket/master/package.json');
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
debugConfig('GitHub API responded with status %s', res.status);
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return data.version;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
debugConfig('Error fetching latest version from GitHub: %s', err.message);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function checkVersion() {
|
|
56
|
+
try {
|
|
57
|
+
const latest = await getLatestVersion();
|
|
58
|
+
if (!latest) return;
|
|
59
|
+
const diff = compareVersions(latest, packageJson.version);
|
|
60
|
+
if (diff > 0) {
|
|
61
|
+
console.log(`[mbkbucket] ⚠️ Update available: v${latest} (current: v${packageJson.version})`);
|
|
62
|
+
} else {
|
|
63
|
+
debugConfig('Running latest version (%s).', packageJson.version);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
debugConfig('Failed to check for updates: %s', err.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateConfiguration() {
|
|
71
|
+
try {
|
|
72
|
+
return parseAndValidateMbkbucketVar(process.env.mbkbucketVar);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(`[mbkbucket] Configuration Validation Failed:\n - ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validateBucketConnection() {
|
|
79
|
+
if (!process.env.BucketConnection) {
|
|
80
|
+
console.warn(`[mbkbucket] ⚠️ BucketConnection not configured. You need to set this environment variable.`);
|
|
81
|
+
console.warn(`[mbkbucket] Example format:\n[mbkbucket] BucketConnection={"r2":{"BUCKET_NAME":"my-bucket","ACCESS_KEY_ID":"key","SECRET_ACCESS_KEY":"secret","ENDPOINT":"https://..."}}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
parseAndValidateBucketConnection(process.env.BucketConnection);
|
|
86
|
+
debugConfig('BucketConnection validated');
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`[mbkbucket] BucketConnection validation failed:\n - ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function validateAllConfiguration() {
|
|
93
|
+
try {
|
|
94
|
+
validateConfiguration();
|
|
95
|
+
validateBucketConnection();
|
|
96
|
+
return true;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(err.message);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const mbkbucketVar = validateConfiguration();
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
packageJson,
|
|
107
|
+
appVersion
|
|
108
|
+
};
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
|
|
2
2
|
const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
|
|
3
3
|
|
|
4
|
-
function normalizeBooleanLike(value) {
|
|
4
|
+
export function normalizeBooleanLike(value) {
|
|
5
5
|
if (typeof value === 'boolean') return value;
|
|
6
6
|
if (typeof value === 'string') {
|
|
7
|
-
const
|
|
8
|
-
if (TRUE_VALUES.has(
|
|
9
|
-
if (FALSE_VALUES.has(
|
|
7
|
+
const norm = value.trim().toLowerCase();
|
|
8
|
+
if (TRUE_VALUES.has(norm)) return true;
|
|
9
|
+
if (FALSE_VALUES.has(norm)) return false;
|
|
10
10
|
}
|
|
11
11
|
return value;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function parseAndValidateMbkbucketVar(rawValue) {
|
|
15
|
-
const errors = [];
|
|
16
15
|
let parsed;
|
|
17
|
-
|
|
18
16
|
try {
|
|
19
17
|
parsed = rawValue ? JSON.parse(rawValue) : {};
|
|
20
18
|
} catch {
|
|
@@ -25,14 +23,10 @@ export function parseAndValidateMbkbucketVar(rawValue) {
|
|
|
25
23
|
throw new Error('mbkbucketVar must be a valid object');
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
const defaults = {
|
|
29
|
-
p_view_inline: true,
|
|
30
|
-
publiView_enabled: false
|
|
31
|
-
};
|
|
32
|
-
|
|
26
|
+
const defaults = { p_view_inline: true, publiView_enabled: false };
|
|
33
27
|
for (const [key, defaultVal] of Object.entries(defaults)) {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
28
|
+
const val = parsed[key];
|
|
29
|
+
if (val === undefined || (typeof val === 'string' && val.trim() === '')) {
|
|
36
30
|
parsed[key] = defaultVal;
|
|
37
31
|
}
|
|
38
32
|
}
|
|
@@ -40,24 +34,16 @@ export function parseAndValidateMbkbucketVar(rawValue) {
|
|
|
40
34
|
parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
|
|
41
35
|
parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (errors.length) {
|
|
51
|
-
throw new Error(errors.join(' | '));
|
|
52
|
-
}
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (typeof parsed.p_view_inline !== 'boolean') errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
|
|
39
|
+
if (typeof parsed.publiView_enabled !== 'boolean') errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
|
|
40
|
+
if (errors.length) throw new Error(errors.join(' | '));
|
|
53
41
|
|
|
54
42
|
return parsed;
|
|
55
43
|
}
|
|
56
44
|
|
|
57
45
|
export function parseAndValidateBucketConnection(rawValue) {
|
|
58
|
-
if (!rawValue)
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
46
|
+
if (!rawValue) return null;
|
|
61
47
|
|
|
62
48
|
let parsed;
|
|
63
49
|
try {
|
|
@@ -66,7 +52,7 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
66
52
|
throw new Error('BucketConnection is not valid JSON');
|
|
67
53
|
}
|
|
68
54
|
|
|
69
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.keys(parsed).length
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Object.keys(parsed).length) {
|
|
70
56
|
throw new Error('BucketConnection must be a non-empty object');
|
|
71
57
|
}
|
|
72
58
|
|
|
@@ -77,7 +63,7 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
77
63
|
throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
const missingFields = requiredFields.filter(
|
|
66
|
+
const missingFields = requiredFields.filter(field => !config[field]);
|
|
81
67
|
if (missingFields.length) {
|
|
82
68
|
throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
|
|
83
69
|
}
|
|
@@ -85,5 +71,3 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
85
71
|
|
|
86
72
|
return parsed;
|
|
87
73
|
}
|
|
88
|
-
|
|
89
|
-
export { normalizeBooleanLike };
|