nodio-cli 1.1.2 → 1.1.4
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/package.json +1 -1
- package/src/server/index.js +19 -2
- package/src/server/routes.js +74 -0
- package/src/user/commands.js +91 -30
- package/src/user/index.js +18 -1
package/package.json
CHANGED
package/src/server/index.js
CHANGED
|
@@ -5,7 +5,8 @@ const mongoose = require('mongoose');
|
|
|
5
5
|
const { getServerConfig } = require('./config');
|
|
6
6
|
const { buildRoutes } = require('./routes');
|
|
7
7
|
const authRoutes = require('./routes/auth');
|
|
8
|
-
const { NodeModel } = require('./models');
|
|
8
|
+
const { NodeModel, FileModel } = require('./models');
|
|
9
|
+
const verifyToken = require('./middleware/verifyToken');
|
|
9
10
|
const { markNodeOfflineAndRecover } = require('./services');
|
|
10
11
|
const { getWalletBalance } = require('../../services/filecoin');
|
|
11
12
|
|
|
@@ -18,7 +19,10 @@ async function startServer() {
|
|
|
18
19
|
const corsAllowlist = new Set([
|
|
19
20
|
'https://nodio.me',
|
|
20
21
|
'https://drive.nodio.me',
|
|
21
|
-
'https://effective-space-rotary-phone-wrv6xg64p7w72wj-3000.app.github.dev'
|
|
22
|
+
'https://effective-space-rotary-phone-wrv6xg64p7w72wj-3000.app.github.dev',
|
|
23
|
+
'https://cautious-sniffle-wrv6xg64pg7jhq7x-5173.app.github.dev',
|
|
24
|
+
'http://localhost:5173',
|
|
25
|
+
'http://127.0.0.1:5173'
|
|
22
26
|
]);
|
|
23
27
|
|
|
24
28
|
const corsOptions = {
|
|
@@ -40,6 +44,19 @@ async function startServer() {
|
|
|
40
44
|
app.use('/api/auth', authRoutes);
|
|
41
45
|
app.use('/api', buildRoutes(config));
|
|
42
46
|
|
|
47
|
+
app.get('/api/files', verifyToken, async (req, res, next) => {
|
|
48
|
+
try {
|
|
49
|
+
const files = await FileModel.find({ userId: req.userId })
|
|
50
|
+
.sort({ createdAt: -1 })
|
|
51
|
+
.select('fileId originalName sizeBytes createdAt filecoinBackedUp filecoinCid')
|
|
52
|
+
.lean();
|
|
53
|
+
|
|
54
|
+
res.json({ files });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
next(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
43
60
|
app.use((error, _req, res, _next) => {
|
|
44
61
|
const status = error.statusCode || 500;
|
|
45
62
|
const message = error.message || 'internal server error';
|
package/src/server/routes.js
CHANGED
|
@@ -57,6 +57,19 @@ function buildRoutes(config) {
|
|
|
57
57
|
res.json({ ok: true, service: 'nodio-server' });
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
router.get('/files', verifyToken, async (req, res, next) => {
|
|
61
|
+
try {
|
|
62
|
+
const files = await FileModel.find({ userId: req.userId })
|
|
63
|
+
.sort({ createdAt: -1 })
|
|
64
|
+
.select('fileId originalName sizeBytes createdAt filecoinBackedUp filecoinCid')
|
|
65
|
+
.lean();
|
|
66
|
+
|
|
67
|
+
res.json({ files });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
next(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
router.post('/nodes/register', async (req, res, next) => {
|
|
61
74
|
try {
|
|
62
75
|
const { nodeId, deviceKey, nodeKey, knownNodeIds, url, capacityBytes, freeBytes } = req.body;
|
|
@@ -559,6 +572,67 @@ function buildRoutes(config) {
|
|
|
559
572
|
}
|
|
560
573
|
});
|
|
561
574
|
|
|
575
|
+
router.post('/files/:fileId/store-key', verifyToken, async (req, res, next) => {
|
|
576
|
+
try {
|
|
577
|
+
const { fileId } = req.params;
|
|
578
|
+
const encryptedAESKey = String(req.body?.encryptedAESKey || '').trim();
|
|
579
|
+
|
|
580
|
+
if (!fileId) {
|
|
581
|
+
return res.status(400).json({ error: 'fileId is required' });
|
|
582
|
+
}
|
|
583
|
+
if (!encryptedAESKey) {
|
|
584
|
+
return res.status(400).json({ error: 'encryptedAESKey is required' });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const file = await FileModel.findOne({ fileId });
|
|
588
|
+
if (!file) {
|
|
589
|
+
return res.status(404).json({ error: 'file not found' });
|
|
590
|
+
}
|
|
591
|
+
if (file.userId !== req.userId) {
|
|
592
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await FileModel.findOneAndUpdate(
|
|
596
|
+
{ fileId },
|
|
597
|
+
{
|
|
598
|
+
$set: {
|
|
599
|
+
encryptedAESKey
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{ new: true }
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
res.json({ ok: true });
|
|
606
|
+
} catch (error) {
|
|
607
|
+
next(error);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
router.get('/files/:fileId/key', verifyToken, async (req, res, next) => {
|
|
612
|
+
try {
|
|
613
|
+
const { fileId } = req.params;
|
|
614
|
+
|
|
615
|
+
if (!fileId) {
|
|
616
|
+
return res.status(400).json({ error: 'fileId is required' });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const file = await FileModel.findOne({ fileId }).lean();
|
|
620
|
+
if (!file) {
|
|
621
|
+
return res.status(404).json({ error: 'file not found' });
|
|
622
|
+
}
|
|
623
|
+
if (file.userId !== req.userId) {
|
|
624
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
625
|
+
}
|
|
626
|
+
if (!file.encryptedAESKey) {
|
|
627
|
+
return res.status(404).json({ error: 'encrypted key not found' });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
res.json({ encryptedAESKey: file.encryptedAESKey });
|
|
631
|
+
} catch (error) {
|
|
632
|
+
next(error);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
562
636
|
router.post('/files/:fileId/filecoin', verifyToken, async (req, res, next) => {
|
|
563
637
|
try {
|
|
564
638
|
const { fileId } = req.params;
|
package/src/user/commands.js
CHANGED
|
@@ -13,7 +13,6 @@ const {
|
|
|
13
13
|
clearSession,
|
|
14
14
|
deriveMasterKey,
|
|
15
15
|
encryptMasterKey,
|
|
16
|
-
decryptMasterKey
|
|
17
16
|
} = require('../cli/session');
|
|
18
17
|
|
|
19
18
|
function promptInput(promptText) {
|
|
@@ -32,9 +31,7 @@ function promptHiddenInput(promptText) {
|
|
|
32
31
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
33
32
|
rl.stdoutMuted = true;
|
|
34
33
|
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
35
|
-
if (rl.stdoutMuted) {
|
|
36
|
-
rl.output.write('*');
|
|
37
|
-
} else {
|
|
34
|
+
if (!rl.stdoutMuted) {
|
|
38
35
|
rl.output.write(stringToWrite);
|
|
39
36
|
}
|
|
40
37
|
};
|
|
@@ -50,7 +47,7 @@ function promptHiddenInput(promptText) {
|
|
|
50
47
|
async function requireSession() {
|
|
51
48
|
const session = await loadSession();
|
|
52
49
|
if (!session) {
|
|
53
|
-
console.log('Please login
|
|
50
|
+
console.log('Please login: nodio login');
|
|
54
51
|
process.exit(1);
|
|
55
52
|
}
|
|
56
53
|
return session;
|
|
@@ -58,7 +55,8 @@ async function requireSession() {
|
|
|
58
55
|
|
|
59
56
|
function attachSessionToken(api, session) {
|
|
60
57
|
if (session?.apiToken) {
|
|
61
|
-
api.defaults.headers
|
|
58
|
+
api.defaults.headers.common.Authorization = `Bearer ${session.apiToken}`;
|
|
59
|
+
api.defaults.headers.common['x-api-token'] = session.apiToken;
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
62
|
|
|
@@ -92,8 +90,8 @@ async function buildSessionPayload(authResponse, password) {
|
|
|
92
90
|
|
|
93
91
|
async function login(options) {
|
|
94
92
|
const serverUrl = options.server;
|
|
95
|
-
const email = await promptInput('
|
|
96
|
-
const password = await promptHiddenInput('
|
|
93
|
+
const email = await promptInput('Enter your email: ');
|
|
94
|
+
const password = await promptHiddenInput('Enter your account password: ');
|
|
97
95
|
|
|
98
96
|
const api = createApiClient(serverUrl);
|
|
99
97
|
const response = await api.post('/auth/login', { email, password });
|
|
@@ -104,8 +102,8 @@ async function login(options) {
|
|
|
104
102
|
|
|
105
103
|
async function register(options) {
|
|
106
104
|
const serverUrl = options.server;
|
|
107
|
-
const email = await promptInput('
|
|
108
|
-
const password = await promptHiddenInput('
|
|
105
|
+
const email = await promptInput('Enter your email: ');
|
|
106
|
+
const password = await promptHiddenInput('Enter your account password: ');
|
|
109
107
|
|
|
110
108
|
const api = createApiClient(serverUrl);
|
|
111
109
|
const response = await api.post('/auth/register', { email, password });
|
|
@@ -129,6 +127,30 @@ async function whoami(options) {
|
|
|
129
127
|
console.log(`userId: ${response.data?.userId || session.userId || 'unknown'}`);
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
async function listFiles(options) {
|
|
131
|
+
const serverUrl = options.server;
|
|
132
|
+
const session = await requireSession();
|
|
133
|
+
const api = createApiClient(serverUrl);
|
|
134
|
+
attachSessionToken(api, session);
|
|
135
|
+
const response = await api.get('/files');
|
|
136
|
+
const files = response.data?.files || [];
|
|
137
|
+
|
|
138
|
+
if (files.length === 0) {
|
|
139
|
+
console.log('No files found for this account.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
console.log(`fileId: ${file.fileId}`);
|
|
145
|
+
console.log(`name: ${file.originalName}`);
|
|
146
|
+
console.log(`sizeBytes: ${file.sizeBytes}`);
|
|
147
|
+
console.log(`createdAt: ${file.createdAt}`);
|
|
148
|
+
console.log(`filecoinBackedUp: ${Boolean(file.filecoinBackedUp)}`);
|
|
149
|
+
console.log(`filecoinCid: ${file.filecoinCid || ''}`);
|
|
150
|
+
console.log('---');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
132
154
|
function splitBuffer(buffer, shardSizeBytes) {
|
|
133
155
|
if (shardSizeBytes <= 0) {
|
|
134
156
|
throw new Error('shard size must be greater than zero');
|
|
@@ -384,7 +406,6 @@ async function uploadFile(options) {
|
|
|
384
406
|
const relayFirst = Boolean(options.relayFirst);
|
|
385
407
|
|
|
386
408
|
const session = await requireSession();
|
|
387
|
-
const masterKey = decryptMasterKey(session.encryptedMasterKey, session.apiToken);
|
|
388
409
|
|
|
389
410
|
if (!Number.isFinite(shardSizeMb) || shardSizeMb <= 0) {
|
|
390
411
|
throw new Error('shard-size-mb must be greater than 0');
|
|
@@ -402,8 +423,6 @@ async function uploadFile(options) {
|
|
|
402
423
|
|
|
403
424
|
const keyBuffer = options.keyBase64 ? parseAesKey(options.keyBase64) : crypto.randomBytes(32);
|
|
404
425
|
const keyBase64 = keyBuffer.toString('base64');
|
|
405
|
-
const encryptedKeyPayload = encryptAes256Gcm(keyBuffer, masterKey);
|
|
406
|
-
const encryptedAESKey = packEncryptedKey(encryptedKeyPayload);
|
|
407
426
|
|
|
408
427
|
const api = createApiClient(serverUrl);
|
|
409
428
|
attachSessionToken(api, session);
|
|
@@ -416,7 +435,6 @@ async function uploadFile(options) {
|
|
|
416
435
|
sizeBytes: plainBuffer.length,
|
|
417
436
|
shardCount: chunks.length,
|
|
418
437
|
cipher: 'aes-256-gcm',
|
|
419
|
-
encryptedAESKey,
|
|
420
438
|
metadata: {
|
|
421
439
|
encryption: {
|
|
422
440
|
algorithm: 'aes-256-gcm',
|
|
@@ -544,7 +562,6 @@ async function uploadFile(options) {
|
|
|
544
562
|
sizeBytes: plainBuffer.length,
|
|
545
563
|
shardCount: chunks.length,
|
|
546
564
|
cipher: 'aes-256-gcm',
|
|
547
|
-
encryptedAESKey,
|
|
548
565
|
metadata: {
|
|
549
566
|
encryption: {
|
|
550
567
|
algorithm: 'aes-256-gcm',
|
|
@@ -560,13 +577,35 @@ async function uploadFile(options) {
|
|
|
560
577
|
console.log(`[filecoin] uploading to Filecoin via central server (this may take a while)...`);
|
|
561
578
|
await requestFilecoinBackup(api, encryptedFileBuffer, fileId);
|
|
562
579
|
|
|
563
|
-
console.log(`Upload complete`);
|
|
580
|
+
console.log(`Upload complete: fileId ${fileId}`);
|
|
564
581
|
console.log(`fileId: ${fileId}`);
|
|
565
582
|
console.log(`originalName: ${originalName}`);
|
|
566
583
|
console.log(`sizeBytes: ${plainBuffer.length}`);
|
|
567
584
|
console.log(`shardCount: ${chunks.length}`);
|
|
568
585
|
console.log(`replicasPerShard: ${replicas}`);
|
|
569
|
-
|
|
586
|
+
|
|
587
|
+
if (session?.argon2Salt && session?.apiToken) {
|
|
588
|
+
console.log('Master password required to save the file key.');
|
|
589
|
+
const masterPassword = await promptHiddenInput('Enter your master password now: ');
|
|
590
|
+
try {
|
|
591
|
+
const masterKey = await deriveMasterKey(masterPassword, session.argon2Salt);
|
|
592
|
+
const encryptedKeyPayload = encryptAes256Gcm(keyBuffer, masterKey);
|
|
593
|
+
const encryptedAESKey = packEncryptedKey(encryptedKeyPayload);
|
|
594
|
+
|
|
595
|
+
await api.post(`/files/${fileId}/store-key`, {
|
|
596
|
+
encryptedAESKey
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
console.log('Key saved securely ✅');
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (String(error?.message || '').includes('unsupported state or unable to authenticate data')) {
|
|
602
|
+
throw new Error('Wrong master password');
|
|
603
|
+
}
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
console.log(`aes256KeyBase64: ${keyBase64}`);
|
|
608
|
+
}
|
|
570
609
|
}
|
|
571
610
|
|
|
572
611
|
async function downloadFile(options) {
|
|
@@ -577,7 +616,6 @@ async function downloadFile(options) {
|
|
|
577
616
|
const relayFirst = Boolean(options.relayFirst);
|
|
578
617
|
|
|
579
618
|
const session = await requireSession();
|
|
580
|
-
const masterKey = decryptMasterKey(session.encryptedMasterKey, session.apiToken);
|
|
581
619
|
|
|
582
620
|
if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
|
|
583
621
|
throw new Error('direct-timeout-ms must be greater than 0');
|
|
@@ -593,16 +631,36 @@ async function downloadFile(options) {
|
|
|
593
631
|
let keyBuffer = null;
|
|
594
632
|
if (options.keyBase64) {
|
|
595
633
|
keyBuffer = parseAesKey(options.keyBase64);
|
|
596
|
-
} else
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
634
|
+
} else {
|
|
635
|
+
console.log('Fetching key from account...');
|
|
636
|
+
console.log('Master password required to decrypt the file key.');
|
|
637
|
+
const masterPassword = await promptHiddenInput('Enter your master password now: ');
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
const masterKey = await deriveMasterKey(masterPassword, session.argon2Salt);
|
|
641
|
+
const keyResponse = await api.get(`/files/${fileId}/key`);
|
|
642
|
+
const encryptedAESKey = keyResponse.data?.encryptedAESKey;
|
|
643
|
+
|
|
644
|
+
if (!encryptedAESKey) {
|
|
645
|
+
throw new Error('missing stored key');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const encryptedKeyPayload = unpackEncryptedKey(encryptedAESKey);
|
|
649
|
+
keyBuffer = decryptAes256Gcm(
|
|
650
|
+
encryptedKeyPayload.cipherText,
|
|
651
|
+
masterKey,
|
|
652
|
+
encryptedKeyPayload.iv,
|
|
653
|
+
encryptedKeyPayload.authTag
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (!Buffer.isBuffer(keyBuffer) || keyBuffer.length !== 32) {
|
|
657
|
+
throw new Error('invalid decrypted AES key length');
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (String(error?.message || '').includes('unsupported state or unable to authenticate data')) {
|
|
661
|
+
throw new Error('Wrong master password');
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
606
664
|
}
|
|
607
665
|
}
|
|
608
666
|
|
|
@@ -614,6 +672,8 @@ async function downloadFile(options) {
|
|
|
614
672
|
throw new Error('missing or unsupported encryption metadata');
|
|
615
673
|
}
|
|
616
674
|
|
|
675
|
+
console.log('Downloading and decrypting...');
|
|
676
|
+
|
|
617
677
|
const shardMetaMap = new Map((encryption.shards || []).map((entry) => [entry.shardId, entry]));
|
|
618
678
|
|
|
619
679
|
const orderedShards = [...(manifest.shards || [])].sort((a, b) => a.order - b.order);
|
|
@@ -708,7 +768,7 @@ async function downloadFile(options) {
|
|
|
708
768
|
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
709
769
|
await fs.writeFile(output, reconstructed);
|
|
710
770
|
|
|
711
|
-
console.log(
|
|
771
|
+
console.log('Done ✅');
|
|
712
772
|
console.log(`fileId: ${fileId}`);
|
|
713
773
|
console.log(`output: ${output}`);
|
|
714
774
|
console.log(`sizeBytes: ${reconstructed.length}`);
|
|
@@ -741,5 +801,6 @@ module.exports = {
|
|
|
741
801
|
login,
|
|
742
802
|
register,
|
|
743
803
|
logout,
|
|
744
|
-
whoami
|
|
804
|
+
whoami,
|
|
805
|
+
listFiles
|
|
745
806
|
};
|
package/src/user/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { Command } = require('commander');
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
uploadFile,
|
|
5
|
+
downloadFile,
|
|
6
|
+
deleteFile,
|
|
7
|
+
login,
|
|
8
|
+
logout,
|
|
9
|
+
register,
|
|
10
|
+
whoami,
|
|
11
|
+
listFiles
|
|
12
|
+
} = require('./commands');
|
|
4
13
|
|
|
5
14
|
const program = new Command();
|
|
6
15
|
|
|
@@ -74,6 +83,14 @@ program
|
|
|
74
83
|
await whoami(options);
|
|
75
84
|
});
|
|
76
85
|
|
|
86
|
+
program
|
|
87
|
+
.command('files')
|
|
88
|
+
.description('List files uploaded by the authenticated user')
|
|
89
|
+
.option('--server <url>', 'central server URL', 'https://api.nodio.me')
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
await listFiles(options);
|
|
92
|
+
});
|
|
93
|
+
|
|
77
94
|
program.parseAsync(process.argv).catch((error) => {
|
|
78
95
|
const apiErrorMessage = error.response?.data?.error;
|
|
79
96
|
if (apiErrorMessage) {
|