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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodio-cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Nodio distributed storage network",
5
5
  "main": "src/server/index.js",
6
6
  "type": "commonjs",
@@ -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';
@@ -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;
@@ -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 first using: npx nodio-cli nodio 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['x-api-token'] = session.apiToken;
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('Email: ');
96
- const password = await promptHiddenInput('Password: ');
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('Email: ');
108
- const password = await promptHiddenInput('Password: ');
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
- console.log(`aes256KeyBase64: ${keyBase64}`);
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 if (manifest.file?.encryptedAESKey) {
597
- const encryptedKeyPayload = unpackEncryptedKey(manifest.file.encryptedAESKey);
598
- keyBuffer = decryptAes256Gcm(
599
- encryptedKeyPayload.cipherText,
600
- masterKey,
601
- encryptedKeyPayload.iv,
602
- encryptedKeyPayload.authTag
603
- );
604
- if (!Buffer.isBuffer(keyBuffer) || keyBuffer.length !== 32) {
605
- throw new Error('invalid decrypted AES key length');
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(`Download complete`);
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 { uploadFile, downloadFile, deleteFile, login, logout, register, whoami } = require('./commands');
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) {