mbkbucket 3.8.1 ā 3.8.3
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/README.md +25 -49
- package/lib/cli.js +154 -565
- package/package.json +11 -12
- package/.mbkbucket.config.example.json +0 -14
- package/lib/src/cli/auth-helper.js +0 -141
package/README.md
CHANGED
|
@@ -111,65 +111,41 @@ The Express app includes:
|
|
|
111
111
|
|
|
112
112
|
## CLI Usage š»
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
mbkbucket includes a CLI for running the dashboard and API as a standalone Node.js server application.
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
### Setup
|
|
116
|
+
### Run Standalone Server
|
|
119
117
|
|
|
120
118
|
```bash
|
|
121
|
-
#
|
|
122
|
-
mbkbucket
|
|
123
|
-
|
|
124
|
-
# Set your API token profile key (one-time, optional)
|
|
125
|
-
mbkbucket config set profileKey your-profile-key
|
|
119
|
+
# Run using npx
|
|
120
|
+
npx mbkbucket
|
|
126
121
|
|
|
127
|
-
#
|
|
128
|
-
mbkbucket
|
|
122
|
+
# Or after installing globally / locally
|
|
123
|
+
mbkbucket
|
|
129
124
|
```
|
|
130
125
|
|
|
131
|
-
###
|
|
126
|
+
### CLI Options
|
|
132
127
|
|
|
133
|
-
|
|
|
128
|
+
| Option | Shorthand | Description |
|
|
134
129
|
|---|---|---|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
| | | |
|
|
139
|
-
|
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
| | | |
|
|
147
|
-
| `mbkbucket config` | `cfg` | Show all config values |
|
|
148
|
-
| `mbkbucket config get <key>` | ā | Get a specific config value |
|
|
149
|
-
| `mbkbucket config set <k> <v>` | ā | Set a config value |
|
|
150
|
-
| `mbkbucket config unset <key>` | ā | Remove a config value |
|
|
151
|
-
| `mbkbucket config path` | ā | Show config file location |
|
|
152
|
-
| `mbkbucket config reset` | ā | Reset to demo values |
|
|
153
|
-
| `mbkbucket config edit` | ā | Open config file in editor |
|
|
154
|
-
|
|
155
|
-
### Global Options
|
|
156
|
-
|
|
157
|
-
| Flag | Description |
|
|
158
|
-
|---|---|
|
|
159
|
-
| `--app`, `-a <name>` | Override `APP_NAME` for key prefix isolation |
|
|
160
|
-
| `--bucket`, `-b <name>` | Override default bucket name |
|
|
161
|
-
| `--help`, `-h` | Show help |
|
|
162
|
-
|
|
163
|
-
### Login Options
|
|
130
|
+
| `--port <number>` | `-p` | Port to listen on (default: `3004` or `$PORT`) |
|
|
131
|
+
| `--host <host>` | `-H` | Host address to bind to (default: `0.0.0.0` or `$HOST`) |
|
|
132
|
+
| `--app <name>` | `-a` | Override `APP_NAME` for bucket key prefix isolation |
|
|
133
|
+
| `--bucket <name>` | `-b` | Override default bucket name |
|
|
134
|
+
| `--env <path>` | `-e` | Load environment variables from a custom `.env` file |
|
|
135
|
+
| `--dev` | `-d` | Run in development mode (`NODE_ENV=dev`) |
|
|
136
|
+
| `--open` | `-o` | Automatically open dashboard in default browser |
|
|
137
|
+
| `--version` | `-v` | Show version number |
|
|
138
|
+
| `--help` | `-h` | Show help text |
|
|
139
|
+
|
|
140
|
+
### Examples
|
|
164
141
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `--profile-key <k>` | API token profile key |
|
|
169
|
-
|
|
170
|
-
### Config File
|
|
142
|
+
```bash
|
|
143
|
+
# Start server on custom port and open browser
|
|
144
|
+
mbkbucket -p 8080 --open
|
|
171
145
|
|
|
172
|
-
|
|
146
|
+
# Start with a specific .env file and application prefix
|
|
147
|
+
mbkbucket --env ./configs/.env.prod -a myapp
|
|
148
|
+
```
|
|
173
149
|
|
|
174
150
|
---
|
|
175
151
|
|
package/lib/cli.js
CHANGED
|
@@ -8,643 +8,232 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* mbkbucket CLI ā
|
|
11
|
+
* mbkbucket CLI ā Standalone server runner for MBKBucket web application.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import dotenv from 'dotenv';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import { createApp } from './src/app.js';
|
|
18
|
+
import { packageJson, checkVersion } from './src/config/index.js';
|
|
19
|
+
import { checkHealth, getAvailableBucketNames } from './src/services/s3.service.js';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// ANSI Color helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const c = {
|
|
25
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
26
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
28
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
29
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
30
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
};
|
|
21
32
|
|
|
22
33
|
// ---------------------------------------------------------------------------
|
|
23
34
|
// Parse CLI arguments
|
|
24
35
|
// ---------------------------------------------------------------------------
|
|
25
36
|
const args = process.argv.slice(2);
|
|
26
|
-
const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
|
|
27
37
|
|
|
28
|
-
function findFlag(name) {
|
|
38
|
+
function findFlag(name, shortChar) {
|
|
29
39
|
const long = `--${name}`;
|
|
30
|
-
const short = `-${name[0]}`;
|
|
40
|
+
const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
|
|
31
41
|
for (let i = 0; i < args.length; i++) {
|
|
32
42
|
if (args[i] === long || args[i] === short) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const hasFlag = name => args.includes(`--${name}`) || args.includes(`-${name[0]}`);
|
|
40
|
-
|
|
41
|
-
function getPositionalArgs() {
|
|
42
|
-
const positional = [];
|
|
43
|
-
for (let i = 0; i < args.length; i++) {
|
|
44
|
-
if (args[i].startsWith('-')) {
|
|
45
|
-
const flagName = args[i].replace(/^-+/, '');
|
|
46
|
-
if (VALUE_FLAGS.has(flagName) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
47
|
-
i++;
|
|
43
|
+
if (args[i + 1] !== undefined && !args[i + 1].startsWith('-')) {
|
|
44
|
+
return args[i + 1];
|
|
48
45
|
}
|
|
49
|
-
|
|
50
|
-
positional.push(args[i]);
|
|
46
|
+
return true;
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
|
-
return
|
|
49
|
+
return undefined;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
52
|
+
const hasFlag = (name, shortChar) => {
|
|
53
|
+
const long = `--${name}`;
|
|
54
|
+
const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
|
|
55
|
+
return args.includes(long) || args.includes(short);
|
|
56
|
+
};
|
|
59
57
|
|
|
60
58
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// Help
|
|
59
|
+
// Help & Version
|
|
62
60
|
// ---------------------------------------------------------------------------
|
|
63
61
|
function showHelp() {
|
|
64
62
|
console.log(`
|
|
65
|
-
mbkbucket
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
--
|
|
95
|
-
|
|
96
|
-
LOGIN OPTIONS:
|
|
97
|
-
--server <url> mbkauthe server URL (required for login)
|
|
98
|
-
--profile-key <k> API token profile key
|
|
99
|
-
|
|
100
|
-
SIGNED-URL OPTIONS:
|
|
101
|
-
--expires <sec> Expiration in seconds (default: 3600)
|
|
102
|
-
|
|
103
|
-
ENVIRONMENT:
|
|
104
|
-
BucketConnection JSON mapping of bucket name ā S3 credentials
|
|
105
|
-
APP_NAME Application name for key prefix isolation
|
|
106
|
-
PORT Server port (default: 3004)
|
|
63
|
+
${c.cyan(c.bold('ā” mbkbucket'))} ${c.dim(`v${packageJson.version}`)}
|
|
64
|
+
Standalone S3/R2 Bucket Management Server
|
|
65
|
+
|
|
66
|
+
${c.bold('USAGE:')}
|
|
67
|
+
mbkbucket [options]
|
|
68
|
+
npx mbkbucket [options]
|
|
69
|
+
|
|
70
|
+
${c.bold('OPTIONS:')}
|
|
71
|
+
-p, --port <number> Port to listen on (default: 3004 or $PORT)
|
|
72
|
+
-H, --host <host> Host address to bind (default: 0.0.0.0 or $HOST)
|
|
73
|
+
-a, --app <name> Override APP_NAME for bucket key prefix isolation
|
|
74
|
+
-b, --bucket <name> Override default bucket name (MBKAUTHE_BUCKET)
|
|
75
|
+
-e, --env <path> Load environment variables from a custom .env file
|
|
76
|
+
-d, --dev Run in development mode (NODE_ENV=dev)
|
|
77
|
+
-o, --open Automatically open the dashboard in default browser
|
|
78
|
+
-v, --version Show version number
|
|
79
|
+
-h, --help Show this help message
|
|
80
|
+
|
|
81
|
+
${c.bold('ENVIRONMENT VARIABLES:')}
|
|
82
|
+
PORT Server port (default: 3004)
|
|
83
|
+
HOST Server host (default: 0.0.0.0)
|
|
84
|
+
NODE_ENV Environment mode ('production' | 'dev')
|
|
85
|
+
BucketConnection JSON mapping of bucket name -> S3 credentials
|
|
86
|
+
mbkautheVar mbkauthe configuration JSON
|
|
87
|
+
mbkbucketVar mbkbucket configuration JSON
|
|
88
|
+
|
|
89
|
+
${c.bold('EXAMPLES:')}
|
|
90
|
+
$ mbkbucket
|
|
91
|
+
$ mbkbucket -p 8080 --open
|
|
92
|
+
$ mbkbucket --env ./configs/.env.prod -a myapp
|
|
107
93
|
`);
|
|
108
94
|
}
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (_s3Module) return _s3Module;
|
|
113
|
-
const appFlag = findFlag('app');
|
|
114
|
-
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
115
|
-
_s3Module = await import('./src/services/s3.service.js');
|
|
116
|
-
return _s3Module;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function walkDir(dir) {
|
|
120
|
-
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(dirent => {
|
|
121
|
-
const fullPath = path.join(dir, dirent.name);
|
|
122
|
-
return dirent.isDirectory() ? walkDir(fullPath) : [fullPath];
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function promptLine() {
|
|
127
|
-
return new Promise((resolve) => {
|
|
128
|
-
const { stdin, stdout } = process;
|
|
129
|
-
stdin.resume();
|
|
130
|
-
stdout.write('> ');
|
|
131
|
-
stdin.once('data', (data) => {
|
|
132
|
-
stdin.pause();
|
|
133
|
-
resolve(data.toString().trim());
|
|
134
|
-
});
|
|
135
|
-
});
|
|
96
|
+
function showVersion() {
|
|
97
|
+
console.log(`mbkbucket v${packageJson.version}`);
|
|
136
98
|
}
|
|
137
99
|
|
|
138
|
-
|
|
139
|
-
// Command Implementations
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
async function cmdLogin() {
|
|
143
|
-
const storedCfg = readConfig();
|
|
144
|
-
const server_url = findFlag('server') || storedCfg.server_url;
|
|
145
|
-
|
|
146
|
-
if (!server_url || typeof server_url !== 'string') {
|
|
147
|
-
console.error('ā --server <url> is required for login.\n Example: mbkbucket login --server https://myapp.example.com\n\nš” Tip: Set it once with:\n mbkbucket config set server_url https://your-server.com\n Then just run: mbkbucket login');
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const profile_key = findFlag('profile-key') || storedCfg.profile_key;
|
|
152
|
-
updateConfig({ server_url });
|
|
153
|
-
|
|
100
|
+
function getNetworkIp() {
|
|
154
101
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
updateConfig({
|
|
161
|
-
token: result.token,
|
|
162
|
-
token_prefix: result.token_prefix,
|
|
163
|
-
username: result.username,
|
|
164
|
-
server_url,
|
|
165
|
-
profile: result.profile || null,
|
|
166
|
-
logged_in_at: nowIso(),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
console.log('\nā
Credentials saved. You are now logged in.\n Run "mbkbucket whoami" to verify.');
|
|
170
|
-
} catch (err) {
|
|
171
|
-
const msg = err.message || '';
|
|
172
|
-
console.error(`ā Login failed: ${msg}`);
|
|
173
|
-
if (msg.includes('profile_key') || msg.includes('profile_id')) {
|
|
174
|
-
console.error('\nš” An API token profile is required. Create one in mbkauthe admin panel and pass --profile-key.');
|
|
175
|
-
} else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
176
|
-
console.error(`\nš” Could not reach server "${server_url}". Verify it is running and CLI_AUTH_ENABLED=true.`);
|
|
177
|
-
}
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function cmdLogout() {
|
|
183
|
-
if (!isLoggedIn()) return console.log('ā¹ļø Not logged in.');
|
|
184
|
-
const { username = 'user' } = readConfig();
|
|
185
|
-
clearConfig();
|
|
186
|
-
console.log(`š Logged out. Goodbye, ${username}!`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function cmdWhoami() {
|
|
190
|
-
if (!isLoggedIn()) return console.log('ā¹ļø Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
|
|
191
|
-
const cfg = readConfig();
|
|
192
|
-
console.log(`ā
Logged in as: ${cfg.username}`);
|
|
193
|
-
console.log(` Server: ${cfg.server_url}`);
|
|
194
|
-
console.log(` Token: ${cfg.token_prefix}...`);
|
|
195
|
-
if (cfg.profile) console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
|
|
196
|
-
console.log(` Since: ${formatDate(cfg.logged_in_at)}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function cmdList() {
|
|
200
|
-
const prefix = cmdArgs[0] || '';
|
|
201
|
-
const bucketName = findFlag('bucket');
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const s3 = await getS3();
|
|
205
|
-
const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
|
|
206
|
-
|
|
207
|
-
if (!result.Contents?.length) {
|
|
208
|
-
return console.log(`š No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
console.log(`\nš Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):\n` + 'ā'.repeat(80));
|
|
212
|
-
|
|
213
|
-
if (result.CommonPrefixes?.length) {
|
|
214
|
-
for (const cp of result.CommonPrefixes) {
|
|
215
|
-
const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
|
|
216
|
-
console.log(` š ${folderName}/`);
|
|
102
|
+
const nets = os.networkInterfaces();
|
|
103
|
+
for (const name of Object.keys(nets)) {
|
|
104
|
+
for (const net of nets[name] ?? []) {
|
|
105
|
+
if (net.family === 'IPv4' && !net.internal) return net.address;
|
|
217
106
|
}
|
|
218
|
-
if (result.Contents.length > 0) console.log('');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for (const obj of result.Contents) {
|
|
222
|
-
const name = obj.Key || '';
|
|
223
|
-
const displayName = name.length > 60 ? `...${name.slice(-57)}` : name;
|
|
224
|
-
console.log(` ${displayName.padEnd(62)} ${formatBytes(obj.Size).padStart(10)} ${formatDate(obj.LastModified)}`);
|
|
225
107
|
}
|
|
108
|
+
} catch {}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
226
111
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
112
|
+
function openBrowser(url) {
|
|
113
|
+
const platform = process.platform;
|
|
114
|
+
let cmd = '';
|
|
115
|
+
if (platform === 'darwin') cmd = `open "${url}"`;
|
|
116
|
+
else if (platform === 'win32') cmd = `start "" "${url}"`;
|
|
117
|
+
else cmd = `xdg-open "${url}"`;
|
|
118
|
+
import('node:child_process').then(({ exec }) => {
|
|
119
|
+
exec(cmd, () => {});
|
|
120
|
+
}).catch(() => {});
|
|
234
121
|
}
|
|
235
122
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Main Runner
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
async function main() {
|
|
127
|
+
if (hasFlag('help', 'h')) {
|
|
128
|
+
showHelp();
|
|
129
|
+
process.exit(0);
|
|
241
130
|
}
|
|
242
131
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
process.exit(1);
|
|
132
|
+
if (hasFlag('version', 'v')) {
|
|
133
|
+
showVersion();
|
|
134
|
+
process.exit(0);
|
|
247
135
|
}
|
|
248
136
|
|
|
249
|
-
|
|
250
|
-
|
|
137
|
+
// Load custom or default .env
|
|
138
|
+
const envPath = findFlag('env', 'e');
|
|
139
|
+
if (typeof envPath === 'string') {
|
|
140
|
+
dotenv.config({ path: path.resolve(process.cwd(), envPath) });
|
|
251
141
|
} else {
|
|
252
|
-
|
|
142
|
+
dotenv.config();
|
|
253
143
|
}
|
|
254
|
-
}
|
|
255
144
|
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const key = cmdArgs[1] || fileName;
|
|
261
|
-
const contentType = getMimeType(fileName);
|
|
262
|
-
|
|
263
|
-
console.log(`ā¬ļø Uploading: ${fileName} (${formatBytes(stat.size)}) ā ${key}`);
|
|
264
|
-
try {
|
|
265
|
-
const s3 = await getS3();
|
|
266
|
-
const result = await s3.uploadFile(key, fs.readFileSync(filePath), contentType, { bucketName });
|
|
267
|
-
console.log(`ā
Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error(`ā Upload failed: ${err.message}`);
|
|
270
|
-
process.exit(1);
|
|
145
|
+
// Handle environment overrides from flags
|
|
146
|
+
const appFlag = findFlag('app', 'a');
|
|
147
|
+
if (typeof appFlag === 'string') {
|
|
148
|
+
process.env.APP_NAME = appFlag;
|
|
271
149
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function uploadFolder(folderPath) {
|
|
275
|
-
const bucketName = findFlag('bucket');
|
|
276
|
-
const prefix = cmdArgs[1] || path.basename(folderPath);
|
|
277
|
-
const files = walkDir(folderPath);
|
|
278
|
-
|
|
279
|
-
if (!files.length) return console.log('š No files found in folder.');
|
|
280
|
-
|
|
281
|
-
console.log(`š Uploading folder: ${folderPath} (${files.length} files) ā ${prefix}/\n`);
|
|
282
|
-
let uploaded = 0;
|
|
283
|
-
let failed = 0;
|
|
284
150
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
const s3 = await getS3();
|
|
290
|
-
await s3.uploadFile(key, fs.readFileSync(file), getMimeType(file), { bucketName });
|
|
291
|
-
console.log(` ā
${rel}`);
|
|
292
|
-
uploaded++;
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.error(` ā ${rel}: ${err.message}`);
|
|
295
|
-
failed++;
|
|
296
|
-
}
|
|
151
|
+
const bucketFlag = findFlag('bucket', 'b');
|
|
152
|
+
if (typeof bucketFlag === 'string') {
|
|
153
|
+
process.env.MBKAUTHE_BUCKET = bucketFlag;
|
|
297
154
|
}
|
|
298
155
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function cmdDownload() {
|
|
304
|
-
const key = cmdArgs[0];
|
|
305
|
-
if (!key) {
|
|
306
|
-
console.error('ā Usage: mbkbucket download <key> [destination]');
|
|
307
|
-
process.exit(1);
|
|
156
|
+
if (hasFlag('dev', 'd')) {
|
|
157
|
+
process.env.NODE_ENV = 'dev';
|
|
308
158
|
}
|
|
309
159
|
|
|
310
|
-
const
|
|
311
|
-
const
|
|
160
|
+
const port = parseInt(findFlag('port', 'p') || process.env.PORT || '3004', 10);
|
|
161
|
+
const host = findFlag('host', 'H') || process.env.HOST || '0.0.0.0';
|
|
162
|
+
const shouldOpen = hasFlag('open', 'o');
|
|
312
163
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
|
|
164
|
+
// Check version in background
|
|
165
|
+
await checkVersion();
|
|
316
166
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const body = await streamToBuffer(result.Body);
|
|
322
|
-
const destDir = path.dirname(path.resolve(dest));
|
|
323
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
324
|
-
fs.writeFileSync(path.resolve(dest), body);
|
|
325
|
-
|
|
326
|
-
console.log(`ā
Downloaded: ${dest} (${formatBytes(body.length)})`);
|
|
167
|
+
// Test S3 connectivity
|
|
168
|
+
let healthResult = null;
|
|
169
|
+
try {
|
|
170
|
+
healthResult = await checkHealth();
|
|
327
171
|
} catch (err) {
|
|
328
|
-
|
|
329
|
-
process.exit(1);
|
|
172
|
+
healthResult = { status: 'unhealthy', error: err.message };
|
|
330
173
|
}
|
|
331
|
-
}
|
|
332
174
|
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
let token = null;
|
|
336
|
-
const resolvedDest = path.resolve(destDir);
|
|
175
|
+
// Create express application
|
|
176
|
+
const app = createApp();
|
|
337
177
|
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
token = result.IsTruncated ? result.NextContinuationToken : null;
|
|
344
|
-
} while (token);
|
|
178
|
+
const server = app.listen(port, host, () => {
|
|
179
|
+
const isAnyHost = host === '0.0.0.0' || host === '::' || host === '';
|
|
180
|
+
const displayHost = isAnyHost ? 'localhost' : host;
|
|
181
|
+
const localUrl = `http://${displayHost}:${port}/mbkbucket`;
|
|
345
182
|
|
|
346
|
-
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(` ${c.cyan(c.bold('ā” MBKBucket'))} ${c.dim(`v${packageJson.version}`)}`);
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(` ${c.green('ā')} ${c.bold('Local:')} ${c.cyan(localUrl)}`);
|
|
347
187
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
for (const fileKey of allFiles) {
|
|
353
|
-
const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
|
|
354
|
-
const filePath = path.join(resolvedDest, relativePath);
|
|
355
|
-
try {
|
|
356
|
-
const result = await s3.downloadFile(fileKey, { bucketName });
|
|
357
|
-
if (!result.notModified) {
|
|
358
|
-
const body = await streamToBuffer(result.Body);
|
|
359
|
-
const fileDir = path.dirname(filePath);
|
|
360
|
-
if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
|
|
361
|
-
fs.writeFileSync(filePath, body);
|
|
188
|
+
if (isAnyHost) {
|
|
189
|
+
const netIp = getNetworkIp();
|
|
190
|
+
if (netIp) {
|
|
191
|
+
console.log(` ${c.green('ā')} ${c.bold('Network:')} ${c.cyan(`http://${netIp}:${port}/mbkbucket`)}`);
|
|
362
192
|
}
|
|
363
|
-
console.log(` ā
${relativePath}`);
|
|
364
|
-
downloaded++;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
console.error(` ā ${relativePath}: ${err.message}`);
|
|
367
|
-
failed++;
|
|
368
193
|
}
|
|
369
|
-
}
|
|
370
194
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
195
|
+
const appName = process.env.APP_NAME || 'root';
|
|
196
|
+
console.log(` ${c.green('ā')} ${c.bold('App Name:')} ${appName}`);
|
|
374
197
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const s3 = await getS3();
|
|
384
|
-
console.log(`šļø Deleting: ${key}`);
|
|
385
|
-
const result = await s3.deleteFile(key, findFlag('bucket'));
|
|
386
|
-
console.log(`ā
Deleted: ${result.key}`);
|
|
387
|
-
} catch (err) {
|
|
388
|
-
console.error(`ā Delete failed: ${err.message}`);
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function cmdDeleteFolder() {
|
|
394
|
-
const prefix = cmdArgs[0];
|
|
395
|
-
if (!prefix) {
|
|
396
|
-
console.error('ā Usage: mbkbucket delete-folder <prefix>');
|
|
397
|
-
process.exit(1);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const cleaned = trimSlashes(prefix);
|
|
401
|
-
console.log(`ā ļø This will delete ALL files under "${cleaned}/".\n Are you sure? Type "yes" to confirm:`);
|
|
402
|
-
|
|
403
|
-
const confirmed = await promptLine();
|
|
404
|
-
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
const s3 = await getS3();
|
|
408
|
-
console.log(`šļø Deleting folder: ${cleaned}/`);
|
|
409
|
-
const result = await s3.deleteFolder(cleaned, findFlag('bucket'));
|
|
410
|
-
console.log(`ā
Deleted ${result.deletedCount} file(s) under "${cleaned}/".`);
|
|
411
|
-
} catch (err) {
|
|
412
|
-
console.error(`ā Delete folder failed: ${err.message}`);
|
|
413
|
-
process.exit(1);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function cmdInfo() {
|
|
418
|
-
const key = cmdArgs[0];
|
|
419
|
-
if (!key) {
|
|
420
|
-
console.error('ā Usage: mbkbucket info <key>');
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
198
|
+
let bucketNames = [];
|
|
199
|
+
try {
|
|
200
|
+
bucketNames = getAvailableBucketNames();
|
|
201
|
+
} catch {}
|
|
423
202
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const meta = await s3.getFileMetadata(key, findFlag('bucket'));
|
|
427
|
-
if (!meta.exists) {
|
|
428
|
-
console.log(`ā File not found: ${key}`);
|
|
429
|
-
process.exit(1);
|
|
203
|
+
if (bucketNames.length > 0) {
|
|
204
|
+
console.log(` ${c.green('ā')} ${c.bold('Buckets:')} ${bucketNames.join(', ')}`);
|
|
430
205
|
}
|
|
431
206
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
console.log(` ETag: ${meta.ETag || 'ā'}`);
|
|
437
|
-
console.log(` Cache-Control: ${meta.CacheControl || 'ā'}`);
|
|
438
|
-
if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
|
|
439
|
-
console.log(' Metadata:');
|
|
440
|
-
for (const [k, v] of Object.entries(meta.Metadata)) console.log(` ${k}: ${v}`);
|
|
207
|
+
if (healthResult?.status === 'healthy') {
|
|
208
|
+
console.log(` ${c.green('ā')} ${c.bold('Storage:')} ${c.green('ā Connected')} (${healthResult.bucket} in ${healthResult.responseTime}ms)`);
|
|
209
|
+
} else if (healthResult?.error) {
|
|
210
|
+
console.log(` ${c.yellow('ā')} ${c.bold('Storage:')} ${c.yellow('ā Connection warning')} (${healthResult.error})`);
|
|
441
211
|
}
|
|
442
|
-
console.log('');
|
|
443
|
-
} catch (err) {
|
|
444
|
-
console.error(`ā Info failed: ${err.message}`);
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async function cmdSignedUrl() {
|
|
450
|
-
const key = cmdArgs[0];
|
|
451
|
-
if (!key) {
|
|
452
|
-
console.error('ā Usage: mbkbucket signed-url <key> [--expires <seconds>]');
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const expires = parseInt(findFlag('expires'), 10) || 3600;
|
|
457
|
-
try {
|
|
458
|
-
const s3 = await getS3();
|
|
459
|
-
console.log(`š Generating signed URL (expires in ${expires}s)...`);
|
|
460
|
-
const result = await s3.generateSignedUrl(key, 'getObject', expires, findFlag('bucket'));
|
|
461
|
-
console.log(`\n${result.url || result}`);
|
|
462
|
-
} catch (err) {
|
|
463
|
-
console.error(`ā Signed URL failed: ${err.message}`);
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
|
-
// Config Commands
|
|
470
|
-
// ---------------------------------------------------------------------------
|
|
471
|
-
const DEMO_CONFIG = {
|
|
472
|
-
token: 'mbk_demo_token_replace_with_real_one_from_login',
|
|
473
|
-
token_prefix: 'mbk_demo',
|
|
474
|
-
username: 'demo-user',
|
|
475
|
-
server_url: 'https://your-app.example.com',
|
|
476
|
-
profile: { name: 'Default CLI Profile', scope: 'read-write', allowed_apps: ['mbkbucket'] },
|
|
477
|
-
logged_in_at: nowIso(),
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
const VALID_CONFIG_KEYS = ['token', 'token_prefix', 'username', 'server_url', 'profile', 'profile_key', 'logged_in_at'];
|
|
481
|
-
|
|
482
|
-
async function cmdConfig() {
|
|
483
|
-
const [sub, key, value] = cmdArgs;
|
|
484
|
-
switch (sub) {
|
|
485
|
-
case 'get': return cmdConfigGet(key);
|
|
486
|
-
case 'set': return cmdConfigSet(key, value);
|
|
487
|
-
case 'unset':
|
|
488
|
-
case 'del':
|
|
489
|
-
case 'delete': return cmdConfigUnset(key);
|
|
490
|
-
case 'path': return cmdConfigPath();
|
|
491
|
-
case 'reset': return cmdConfigReset();
|
|
492
|
-
case 'edit': return cmdConfigEdit();
|
|
493
|
-
default: return cmdConfigShow();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function cmdConfigShow() {
|
|
498
|
-
const cfg = readConfig();
|
|
499
|
-
const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
|
|
500
|
-
|
|
501
|
-
if (!entries.length) {
|
|
502
|
-
console.log(`š No config values set.\n File: ${CONFIG_FILE}\n Run "mbkbucket config reset" to populate demo values.`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
console.log(`\nš Config (${CONFIG_FILE}):\n` + 'ā'.repeat(60));
|
|
507
|
-
for (const k of entries.sort()) {
|
|
508
|
-
const v = cfg[k];
|
|
509
|
-
const display = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
|
|
510
|
-
const masked = (k === 'token' && display.length > 12)
|
|
511
|
-
? `${display.slice(0, 8)}...${display.slice(-4)}`
|
|
512
|
-
: display.length > 55 ? `${display.slice(0, 52)}...` : display;
|
|
513
|
-
console.log(` ${k.padEnd(14)} ${masked}`);
|
|
514
|
-
}
|
|
515
|
-
console.log('ā'.repeat(60) + '\n Run "mbkbucket config set <key> <value>" to change a value.\n Run "mbkbucket config edit" to open in your editor.\n');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function cmdConfigGet(key) {
|
|
519
|
-
if (!key || !VALID_CONFIG_KEYS.includes(key)) {
|
|
520
|
-
console.error(`ā Usage: mbkbucket config get <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
521
|
-
process.exit(1);
|
|
522
|
-
}
|
|
523
|
-
const cfg = readConfig();
|
|
524
|
-
if (!(key in cfg)) {
|
|
525
|
-
console.error(`ā Key not found: ${key}\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
const v = cfg[key];
|
|
529
|
-
console.log(typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function cmdConfigSet(key, value) {
|
|
533
|
-
if (!key || value === undefined || !VALID_CONFIG_KEYS.includes(key)) {
|
|
534
|
-
console.error(`ā Usage: mbkbucket config set <key> <value>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
let parsed = value;
|
|
539
|
-
if (/^[{\[].*[}\]]$/.test(value)) {
|
|
540
|
-
try { parsed = JSON.parse(value); } catch {}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
updateConfig({ [key]: parsed });
|
|
544
|
-
console.log(`ā
Set ${key} = ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}`);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function cmdConfigUnset(key) {
|
|
548
|
-
if (!key) {
|
|
549
|
-
console.error(`ā Usage: mbkbucket config unset <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
550
|
-
process.exit(1);
|
|
551
|
-
}
|
|
552
|
-
const cfg = readConfig();
|
|
553
|
-
if (!(key in cfg)) {
|
|
554
|
-
console.error(`ā Key not found: ${key}`);
|
|
555
|
-
process.exit(1);
|
|
556
|
-
}
|
|
557
|
-
delete cfg[key];
|
|
558
|
-
writeConfig(cfg);
|
|
559
|
-
console.log(`šļø Removed: ${key}`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function cmdConfigPath() {
|
|
563
|
-
console.log(`š Config directory: ${CONFIG_DIR}\nš Config file: ${CONFIG_FILE}`);
|
|
564
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
565
|
-
const stat = fs.statSync(CONFIG_FILE);
|
|
566
|
-
console.log(` Size: ${formatBytes(stat.size)}\n Modified: ${formatDate(stat.mtime.toISOString())}`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
async function cmdConfigReset() {
|
|
571
|
-
console.log('ā ļø This will overwrite your current config with demo values.\n Are you sure? Type "yes" to confirm:');
|
|
572
|
-
const confirmed = await promptLine();
|
|
573
|
-
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
574
|
-
writeConfig({ ...DEMO_CONFIG });
|
|
575
|
-
console.log('ā
Config reset to demo values.\n Run "mbkbucket config" to view.');
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function cmdConfigEdit() {
|
|
579
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
|
|
580
|
-
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
581
|
-
if (!fs.existsSync(CONFIG_FILE)) writeConfig({ ...DEMO_CONFIG });
|
|
582
212
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(` ${c.dim('Press Ctrl+C to stop the server')}`);
|
|
215
|
+
console.log();
|
|
586
216
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
else console.error(`ā ļø Editor exited with code ${code}.`);
|
|
591
|
-
resolve();
|
|
592
|
-
});
|
|
593
|
-
child.on('error', (err) => {
|
|
594
|
-
console.error(`ā Could not open editor (${editor}): ${err.message}`);
|
|
595
|
-
resolve();
|
|
596
|
-
});
|
|
217
|
+
if (shouldOpen) {
|
|
218
|
+
openBrowser(localUrl);
|
|
219
|
+
}
|
|
597
220
|
});
|
|
598
|
-
}
|
|
599
221
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
ls: cmdList,
|
|
609
|
-
upload: cmdUpload,
|
|
610
|
-
up: cmdUpload,
|
|
611
|
-
download: cmdDownload,
|
|
612
|
-
dl: cmdDownload,
|
|
613
|
-
delete: cmdDelete,
|
|
614
|
-
rm: cmdDelete,
|
|
615
|
-
'delete-folder': cmdDeleteFolder,
|
|
616
|
-
rmdir: cmdDeleteFolder,
|
|
617
|
-
info: cmdInfo,
|
|
618
|
-
stat: cmdInfo,
|
|
619
|
-
'signed-url': cmdSignedUrl,
|
|
620
|
-
sign: cmdSignedUrl,
|
|
621
|
-
config: cmdConfig,
|
|
622
|
-
cfg: cmdConfig,
|
|
623
|
-
help: showHelp,
|
|
624
|
-
'--help': showHelp,
|
|
625
|
-
'-h': showHelp,
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
async function main() {
|
|
629
|
-
if (hasFlag('help')) return showHelp();
|
|
630
|
-
|
|
631
|
-
const appFlag = findFlag('app');
|
|
632
|
-
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
633
|
-
|
|
634
|
-
const bucketFlag = findFlag('bucket');
|
|
635
|
-
if (bucketFlag && typeof bucketFlag === 'string') process.env.MBKAUTHE_BUCKET = bucketFlag;
|
|
636
|
-
|
|
637
|
-
const handler = COMMANDS[command];
|
|
638
|
-
if (!handler) {
|
|
639
|
-
console.error(`ā Unknown command: ${command}\n Run "mbkbucket --help" for usage information.`);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
222
|
+
const shutdown = (signal) => {
|
|
223
|
+
console.log(`\n ${c.dim(`Received ${signal}, shutting down...`)}`);
|
|
224
|
+
server.close(() => {
|
|
225
|
+
console.log(` ${c.green('ā')} ${c.dim('MBKBucket server stopped.')}\n`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
});
|
|
228
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
229
|
+
};
|
|
642
230
|
|
|
643
|
-
|
|
231
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
232
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
644
233
|
}
|
|
645
234
|
|
|
646
235
|
main().catch((err) => {
|
|
647
|
-
console.error(
|
|
236
|
+
console.error(`\n${c.red(c.bold('Error starting mbkbucket:'))} ${err.message}`);
|
|
648
237
|
if (process.env.DEBUG) console.error(err);
|
|
649
238
|
process.exit(1);
|
|
650
239
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "3.8.
|
|
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
|
|
3
|
+
"version": "3.8.3",
|
|
4
|
+
"description": "Flexible S3/R2 bucket management ā library, Express router, and standalone server CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a standalone server runner with built-in configuration validation and health checks",
|
|
5
5
|
"author": "Muhammad Bin Khalid",
|
|
6
6
|
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"mbkbucket": "./lib/cli.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
+
"start": "node lib/cli.js",
|
|
15
16
|
"dev": "cross-env NODE_ENV=dev nodemon index.js",
|
|
16
17
|
"publish": "node scripts/publish.js",
|
|
17
18
|
"test": "vitest run"
|
|
@@ -23,22 +24,22 @@
|
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@aws-sdk/client-s3": "^3.1127.0",
|
|
25
26
|
"@aws-sdk/s3-request-presigner": "^3.1127.0",
|
|
26
|
-
"compression": "^1.8.
|
|
27
|
+
"compression": "^1.8.2",
|
|
27
28
|
"debug": "^4.4.3",
|
|
28
|
-
"dotenv": "^
|
|
29
|
-
"express": "^5.1
|
|
29
|
+
"dotenv": "^18.0.5",
|
|
30
|
+
"express": "^5.2.1",
|
|
30
31
|
"express-handlebars": "^9.0.1",
|
|
31
32
|
"express-rate-limit": "^7.5.1",
|
|
32
|
-
"mbkauthe": "6.2.
|
|
33
|
-
"multer": "^2.0
|
|
34
|
-
"pg": "^8.16.3"
|
|
33
|
+
"mbkauthe": "^6.2.3",
|
|
34
|
+
"multer": "^2.4.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/supertest": "^7.2.1",
|
|
38
38
|
"cross-env": "^7.0.3",
|
|
39
39
|
"nodemon": "^3.1.14",
|
|
40
|
-
"supertest": "^7.
|
|
41
|
-
"
|
|
40
|
+
"supertest": "^7.3.1",
|
|
41
|
+
"vite": "^8.3.2",
|
|
42
|
+
"vitest": "5.0.3"
|
|
42
43
|
},
|
|
43
44
|
"overrides": {
|
|
44
45
|
"csurf": {
|
|
@@ -56,8 +57,6 @@
|
|
|
56
57
|
"lib",
|
|
57
58
|
"views",
|
|
58
59
|
".env.example",
|
|
59
|
-
".mbkbucket.config.example.json",
|
|
60
|
-
"cli.js",
|
|
61
60
|
"index.js",
|
|
62
61
|
"LICENSE",
|
|
63
62
|
"package.json",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_schema": "mbkbucket CLI config ā stored at ~/.mbkbucket/config.json",
|
|
3
|
-
|
|
4
|
-
"token": "mbk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
5
|
-
"tokenPrefix": "mbk_xxxx",
|
|
6
|
-
"username": "your-username",
|
|
7
|
-
"serverUrl": "https://your-app.example.com",
|
|
8
|
-
"profile": {
|
|
9
|
-
"name": "Read-Write CLI",
|
|
10
|
-
"scope": "read-write",
|
|
11
|
-
"allowedApps": ["mbkbucket"]
|
|
12
|
-
},
|
|
13
|
-
"loggedInAt": "2026-01-01T00:00:00.000Z"
|
|
14
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Authentication Helper
|
|
3
|
-
* Handles mbkauthe device-flow login for the mbkbucket CLI.
|
|
4
|
-
* Stores credentials in ~/.mbkbucket/config.json
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import { createLogger } from '#logger';
|
|
11
|
-
|
|
12
|
-
const debugCli = createLogger('cli:auth');
|
|
13
|
-
|
|
14
|
-
export const CONFIG_DIR = path.join(os.homedir(), '.mbkbucket');
|
|
15
|
-
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
16
|
-
const MAX_POLL_TIME_MS = 15 * 60 * 1000; // 15 minutes
|
|
17
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
-
|
|
19
|
-
export function readConfig() {
|
|
20
|
-
try {
|
|
21
|
-
return fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) : {};
|
|
22
|
-
} catch (err) {
|
|
23
|
-
debugCli('Failed to read config: %s', err.message);
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function writeConfig(config) {
|
|
29
|
-
try {
|
|
30
|
-
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error(`ā Failed to write config: ${err.message}`);
|
|
34
|
-
throw err;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function clearConfig() {
|
|
39
|
-
try {
|
|
40
|
-
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
41
|
-
return true;
|
|
42
|
-
} catch (err) {
|
|
43
|
-
debugCli('Failed to clear config: %s', err.message);
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function updateConfig(partial) {
|
|
49
|
-
const updated = { ...readConfig(), ...partial };
|
|
50
|
-
writeConfig(updated);
|
|
51
|
-
return updated;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isLoggedIn() {
|
|
55
|
-
const { token, server_url } = readConfig();
|
|
56
|
-
return Boolean(token && server_url);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const getStoredToken = () => readConfig().token || null;
|
|
60
|
-
export const getStoredServerUrl = () => readConfig().server_url || null;
|
|
61
|
-
|
|
62
|
-
export async function deviceFlowLogin({ server_url, profile_key }) {
|
|
63
|
-
const baseUrl = String(server_url).replace(/\/+$/, '');
|
|
64
|
-
console.log(`š Requesting device login from ${baseUrl} ...`);
|
|
65
|
-
|
|
66
|
-
let deviceResp;
|
|
67
|
-
try {
|
|
68
|
-
const res = await fetch(`${baseUrl}/api/cli/device`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: { 'Content-Type': 'application/json' },
|
|
71
|
-
body: JSON.stringify({
|
|
72
|
-
client_name: `mbkbucket-cli-${os.hostname()}`,
|
|
73
|
-
...(profile_key && { profile_key }),
|
|
74
|
-
}),
|
|
75
|
-
});
|
|
76
|
-
deviceResp = await res.json();
|
|
77
|
-
if (!deviceResp.success) throw new Error(deviceResp.message || 'Failed to initiate device login');
|
|
78
|
-
} catch (err) {
|
|
79
|
-
if (err.message.includes('Failed to initiate') || err.message.includes('fetch')) {
|
|
80
|
-
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.`);
|
|
81
|
-
}
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const { verification_url = '', user_code = '', device_code, expires_in = 900, interval = 5, profile } = deviceResp;
|
|
86
|
-
|
|
87
|
-
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
88
|
-
console.log('ā ā');
|
|
89
|
-
console.log('ā š Open this URL in your browser to approve: ā');
|
|
90
|
-
console.log(`ā ${verification_url.padEnd(53)}ā`);
|
|
91
|
-
console.log('ā ā');
|
|
92
|
-
console.log(`ā š¢ Or enter this code: ${user_code.padEnd(30)}ā`);
|
|
93
|
-
console.log('ā ā');
|
|
94
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
95
|
-
|
|
96
|
-
if (profile) console.log(` Profile: ${profile.name} (scope: ${profile.scope || 'full'})`);
|
|
97
|
-
console.log(` Waiting for approval (expires in ${Math.floor(expires_in / 60)} minutes)...\n`);
|
|
98
|
-
|
|
99
|
-
const pollInterval = interval * 1000;
|
|
100
|
-
const deadline = Date.now() + MAX_POLL_TIME_MS;
|
|
101
|
-
let attempts = 0;
|
|
102
|
-
|
|
103
|
-
while (Date.now() < deadline) {
|
|
104
|
-
await sleep(pollInterval);
|
|
105
|
-
attempts++;
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const pollRes = await fetch(`${baseUrl}/api/cli/device/token`, {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: { 'Content-Type': 'application/json' },
|
|
111
|
-
body: JSON.stringify({ device_code }),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const pollBody = await pollRes.json();
|
|
115
|
-
|
|
116
|
-
if (pollBody.status === 'approved' && pollBody.success) {
|
|
117
|
-
const token_prefix = pollBody.token_prefix || '';
|
|
118
|
-
console.log(`ā
Login approved! Welcome, ${pollBody.username}.`);
|
|
119
|
-
console.log(` Token prefix: ${token_prefix}...`);
|
|
120
|
-
return {
|
|
121
|
-
token: pollBody.token,
|
|
122
|
-
token_prefix,
|
|
123
|
-
username: pollBody.username,
|
|
124
|
-
profile: profile || null,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (pollBody.status === 'denied') throw new Error('Login request was denied.');
|
|
129
|
-
if (pollBody.status === 'expired') throw new Error('Login request expired. Please try again.');
|
|
130
|
-
if (pollBody.status === 'completed') throw new Error('This login session has already been completed.');
|
|
131
|
-
|
|
132
|
-
if (attempts % 5 === 0) process.stdout.write('.');
|
|
133
|
-
} catch (err) {
|
|
134
|
-
if (['denied', 'expired', 'completed'].some(k => err.message.includes(k))) throw err;
|
|
135
|
-
debugCli('Poll error (will retry): %s', err.message);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
throw new Error('Login timed out. Please try again.');
|
|
140
|
-
}
|
|
141
|
-
|