@tuskydp/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/src/commands/decrypt.d.ts.map +1 -1
- package/dist/src/commands/decrypt.js +53 -21
- package/dist/src/commands/decrypt.js.map +1 -1
- package/dist/src/commands/download.d.ts +1 -0
- package/dist/src/commands/download.d.ts.map +1 -1
- package/dist/src/commands/download.js +81 -11
- package/dist/src/commands/download.js.map +1 -1
- package/dist/src/commands/export.d.ts +6 -0
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +29 -17
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/files.d.ts.map +1 -1
- package/dist/src/commands/files.js +89 -10
- package/dist/src/commands/files.js.map +1 -1
- package/dist/src/commands/folder.d.ts +3 -0
- package/dist/src/commands/folder.d.ts.map +1 -0
- package/dist/src/commands/folder.js +151 -0
- package/dist/src/commands/folder.js.map +1 -0
- package/dist/src/commands/rehydrate.d.ts +1 -0
- package/dist/src/commands/rehydrate.d.ts.map +1 -1
- package/dist/src/commands/rehydrate.js +15 -7
- package/dist/src/commands/rehydrate.js.map +1 -1
- package/dist/src/commands/trash.d.ts +3 -0
- package/dist/src/commands/trash.d.ts.map +1 -0
- package/dist/src/commands/trash.js +109 -0
- package/dist/src/commands/trash.js.map +1 -0
- package/dist/src/commands/upload.d.ts +4 -0
- package/dist/src/commands/upload.d.ts.map +1 -1
- package/dist/src/commands/upload.js +104 -3
- package/dist/src/commands/upload.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +3 -0
- package/dist/src/commands/wallet.d.ts.map +1 -0
- package/dist/src/commands/wallet.js +126 -0
- package/dist/src/commands/wallet.js.map +1 -0
- package/dist/src/commands/webhook.d.ts +3 -0
- package/dist/src/commands/webhook.d.ts.map +1 -0
- package/dist/src/commands/webhook.js +172 -0
- package/dist/src/commands/webhook.js.map +1 -0
- package/dist/src/crypto.d.ts +16 -0
- package/dist/src/crypto.d.ts.map +1 -1
- package/dist/src/crypto.js +26 -0
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/index.js +17 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +2 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/files.d.ts.map +1 -1
- package/dist/src/mcp/tools/files.js +40 -5
- package/dist/src/mcp/tools/files.js.map +1 -1
- package/dist/src/seal.d.ts +16 -0
- package/dist/src/seal.d.ts.map +1 -1
- package/dist/src/seal.js +23 -0
- package/dist/src/seal.js.map +1 -1
- package/dist/src/tui/files-panel.d.ts +31 -1
- package/dist/src/tui/files-panel.d.ts.map +1 -1
- package/dist/src/tui/files-panel.js +118 -11
- package/dist/src/tui/files-panel.js.map +1 -1
- package/dist/src/tui/index.d.ts.map +1 -1
- package/dist/src/tui/index.js +272 -33
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/overview.d.ts.map +1 -1
- package/dist/src/tui/overview.js +24 -8
- package/dist/src/tui/overview.js.map +1 -1
- package/dist/src/tui/trash-screen.d.ts +4 -0
- package/dist/src/tui/trash-screen.d.ts.map +1 -0
- package/dist/src/tui/trash-screen.js +190 -0
- package/dist/src/tui/trash-screen.js.map +1 -0
- package/dist/src/tui/vaults-panel.d.ts +8 -0
- package/dist/src/tui/vaults-panel.d.ts.map +1 -1
- package/dist/src/tui/vaults-panel.js +45 -6
- package/dist/src/tui/vaults-panel.js.map +1 -1
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +21 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +3 -3
- package/src/commands/decrypt.ts +56 -23
- package/src/commands/download.ts +82 -11
- package/src/commands/export.ts +35 -19
- package/src/commands/files.ts +93 -9
- package/src/commands/folder.ts +169 -0
- package/src/commands/rehydrate.ts +15 -8
- package/src/commands/trash.ts +121 -0
- package/src/commands/upload.ts +126 -3
- package/src/commands/wallet.ts +183 -0
- package/src/commands/webhook.ts +193 -0
- package/src/crypto.ts +35 -0
- package/src/index.ts +17 -4
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools/files.ts +43 -5
- package/src/seal.ts +34 -1
- package/src/tui/files-panel.ts +139 -11
- package/src/tui/index.ts +289 -33
- package/src/tui/overview.ts +22 -8
- package/src/tui/trash-screen.ts +203 -0
- package/src/tui/vaults-panel.ts +55 -6
- package/src/version.ts +21 -0
- package/vitest.config.ts +1 -0
- package/dist/src/client.d.ts +0 -120
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js +0 -152
- package/dist/src/client.js.map +0 -1
package/src/crypto.ts
CHANGED
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
pbkdf2Sync,
|
|
8
8
|
} from 'crypto';
|
|
9
9
|
|
|
10
|
+
export interface FileMeta {
|
|
11
|
+
/** Original filename */
|
|
12
|
+
n: string;
|
|
13
|
+
/** Original MIME type */
|
|
14
|
+
m: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
const PBKDF2_ITERATIONS = 600_000;
|
|
11
18
|
const AES_ALGO = 'aes-256-gcm' as const;
|
|
12
19
|
const VERIFIER_CONTEXT = 'tuskydp-key-verifier-v1';
|
|
@@ -92,6 +99,34 @@ export function encryptBuffer(plaintext: Buffer, masterKey: Buffer): {
|
|
|
92
99
|
};
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Encrypt filename + MIME type together using master key (AES-256-GCM).
|
|
104
|
+
* Format: base64([iv(12) | ciphertext | authTag(16)])
|
|
105
|
+
*/
|
|
106
|
+
export function encryptMetadata(name: string, mimeType: string, masterKey: Buffer): string {
|
|
107
|
+
const plaintext = Buffer.from(JSON.stringify({ n: name, m: mimeType }), 'utf8');
|
|
108
|
+
const iv = randomBytes(12);
|
|
109
|
+
const cipher = createCipheriv(AES_ALGO, masterKey, iv);
|
|
110
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
111
|
+
const tag = cipher.getAuthTag();
|
|
112
|
+
return Buffer.concat([iv, encrypted, tag]).toString('base64');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Decrypt an encryptedName blob produced by encryptMetadata.
|
|
117
|
+
* Returns the original { name, mimeType }.
|
|
118
|
+
*/
|
|
119
|
+
export function decryptMetadata(encryptedBase64: string, masterKey: Buffer): FileMeta {
|
|
120
|
+
const buf = Buffer.from(encryptedBase64, 'base64');
|
|
121
|
+
const iv = buf.subarray(0, 12);
|
|
122
|
+
const tag = buf.subarray(buf.length - 16);
|
|
123
|
+
const ciphertext = buf.subarray(12, buf.length - 16);
|
|
124
|
+
const decipher = createDecipheriv(AES_ALGO, masterKey, iv);
|
|
125
|
+
decipher.setAuthTag(tag);
|
|
126
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
127
|
+
return JSON.parse(plaintext.toString('utf8')) as FileMeta;
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
export function decryptBuffer(
|
|
96
131
|
ciphertext: Buffer,
|
|
97
132
|
wrappedKeyBase64: string,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { registerEncryptionCommands } from './commands/encryption.js';
|
|
|
5
5
|
import { registerVaultCommands } from './commands/vault.js';
|
|
6
6
|
import { registerFileCommands } from './commands/files.js';
|
|
7
7
|
import { registerAccountCommands } from './commands/account.js';
|
|
8
|
+
import { registerFolderCommands } from './commands/folder.js';
|
|
9
|
+
import { registerTrashCommands } from './commands/trash.js';
|
|
10
|
+
import { registerWebhookCommands } from './commands/webhook.js';
|
|
11
|
+
import { registerWalletCommands } from './commands/wallet.js';
|
|
8
12
|
import { uploadCommand } from './commands/upload.js';
|
|
9
13
|
import { downloadCommand } from './commands/download.js';
|
|
10
14
|
import { rehydrateCommand } from './commands/rehydrate.js';
|
|
@@ -12,15 +16,14 @@ import { registerTuiCommand } from './commands/tui.js';
|
|
|
12
16
|
import { registerMcpCommands } from './commands/mcp.js';
|
|
13
17
|
import { registerExportCommand } from './commands/export.js';
|
|
14
18
|
import { registerDecryptCommand } from './commands/decrypt.js';
|
|
15
|
-
|
|
16
|
-
const VERSION = '0.1.0';
|
|
19
|
+
import { CLI_VERSION } from './version.js';
|
|
17
20
|
|
|
18
21
|
const program = new Command();
|
|
19
22
|
|
|
20
23
|
program
|
|
21
24
|
.name('tusky')
|
|
22
25
|
.description('Tusky — Encrypted decentralized storage for developers, apps, and agents')
|
|
23
|
-
.version(
|
|
26
|
+
.version(CLI_VERSION)
|
|
24
27
|
.option('--api-key <key>', 'Override API key')
|
|
25
28
|
.option('--api-url <url>', 'Override API URL')
|
|
26
29
|
.option('--format <fmt>', 'Output format: table, json, plain', 'table')
|
|
@@ -39,6 +42,10 @@ registerAuthCommands(program);
|
|
|
39
42
|
registerEncryptionCommands(program);
|
|
40
43
|
registerVaultCommands(program);
|
|
41
44
|
registerFileCommands(program);
|
|
45
|
+
registerFolderCommands(program);
|
|
46
|
+
registerTrashCommands(program);
|
|
47
|
+
registerWebhookCommands(program);
|
|
48
|
+
registerWalletCommands(program);
|
|
42
49
|
registerAccountCommands(program);
|
|
43
50
|
registerTuiCommand(program);
|
|
44
51
|
registerMcpCommands(program);
|
|
@@ -46,10 +53,14 @@ registerExportCommand(program);
|
|
|
46
53
|
registerDecryptCommand(program);
|
|
47
54
|
|
|
48
55
|
// Direct shortcuts for common operations
|
|
49
|
-
program.command('upload
|
|
56
|
+
program.command('upload [paths...]')
|
|
50
57
|
.description('Upload files')
|
|
51
58
|
.option('--vault <vault>', 'Target vault')
|
|
59
|
+
.option('--folder <folder-id>', 'Target folder ID within the vault')
|
|
52
60
|
.option('--recursive', 'Upload directory contents')
|
|
61
|
+
.option('--content <text>', 'Upload inline text content instead of a file path')
|
|
62
|
+
.option('--stdin', 'Read file content from stdin')
|
|
63
|
+
.option('--name <filename>', 'File name to use (required with --content or --stdin)')
|
|
53
64
|
.action(async (paths: string[], options) => {
|
|
54
65
|
await uploadCommand(paths, options, program);
|
|
55
66
|
});
|
|
@@ -57,6 +68,7 @@ program.command('upload <paths...>')
|
|
|
57
68
|
program.command('download <file-id>')
|
|
58
69
|
.description('Download a file')
|
|
59
70
|
.option('--output <path>', 'Output path')
|
|
71
|
+
.option('--stdout', 'Write file content to stdout (for sandboxed agents without filesystem access)')
|
|
60
72
|
.action(async (fileId: string, options) => {
|
|
61
73
|
await downloadCommand(fileId, options, program);
|
|
62
74
|
});
|
|
@@ -64,6 +76,7 @@ program.command('download <file-id>')
|
|
|
64
76
|
program.command('rehydrate <blob-id>')
|
|
65
77
|
.description('Download a file directly from Walrus by blob ID')
|
|
66
78
|
.option('--output <path>', 'Output file path')
|
|
79
|
+
.option('--stdout', 'Write blob content to stdout (for sandboxed agents without filesystem access)')
|
|
67
80
|
.action(async (blobId: string, options) => {
|
|
68
81
|
await rehydrateCommand(blobId, options, program);
|
|
69
82
|
});
|
package/src/mcp/server.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { registerFolderTools } from './tools/folders.js';
|
|
|
25
25
|
import { registerFileTools } from './tools/files.js';
|
|
26
26
|
import { registerTrashTools } from './tools/trash.js';
|
|
27
27
|
import { registerSharedVaultTools } from './tools/sharedVaults.js';
|
|
28
|
+
import { CLI_VERSION } from '../version.js';
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Encryption bootstrap
|
|
@@ -135,7 +136,7 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
|
|
|
135
136
|
// Create MCP server
|
|
136
137
|
const server = new McpServer({
|
|
137
138
|
name: 'tusky',
|
|
138
|
-
version:
|
|
139
|
+
version: CLI_VERSION,
|
|
139
140
|
});
|
|
140
141
|
|
|
141
142
|
// Register all tools
|
package/src/mcp/tools/files.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { basename, resolve, join } from 'path';
|
|
|
13
13
|
import { lookup } from 'mime-types';
|
|
14
14
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
15
|
import type { McpContext } from '../context.js';
|
|
16
|
-
import { encryptBuffer, decryptBuffer } from '../../crypto.js';
|
|
17
|
-
import { isSealConfigured, sealEncrypt, sealDecrypt } from '../../seal.js';
|
|
16
|
+
import { encryptBuffer, decryptBuffer, encryptMetadata, decryptMetadata } from '../../crypto.js';
|
|
17
|
+
import { isSealConfigured, sealEncrypt, sealDecrypt, sealEncryptMetadata, sealDecryptMetadata } from '../../seal.js';
|
|
18
18
|
import { wrapToolError } from './helpers.js';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -117,6 +117,38 @@ async function fetchAndDecryptFile(fileId: string, ctx: McpContext) {
|
|
|
117
117
|
return { fileBuffer, encryption };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the real filename from a FileStatusResponse.
|
|
122
|
+
* Decrypts encryptedName (private vaults) or encryptedMetadata (shared vaults) when present.
|
|
123
|
+
* Falls back to the stored placeholder name on failure.
|
|
124
|
+
*/
|
|
125
|
+
async function resolveFilename(
|
|
126
|
+
fileInfo: { name: string; nameEncrypted?: boolean; encryptedName?: string | null; encryptedMetadata?: string | null; fileId?: string },
|
|
127
|
+
ctx: McpContext,
|
|
128
|
+
): Promise<string> {
|
|
129
|
+
if (!fileInfo.nameEncrypted) return fileInfo.name;
|
|
130
|
+
try {
|
|
131
|
+
if (fileInfo.encryptedName) {
|
|
132
|
+
const masterKey = ctx.getMasterKey();
|
|
133
|
+
if (masterKey) {
|
|
134
|
+
const meta = decryptMetadata(fileInfo.encryptedName, masterKey);
|
|
135
|
+
return meta.n;
|
|
136
|
+
}
|
|
137
|
+
} else if (fileInfo.encryptedMetadata && fileInfo.fileId) {
|
|
138
|
+
const keypair = ctx.getSuiKeypair();
|
|
139
|
+
if (keypair) {
|
|
140
|
+
const fileObj = await ctx.sdk.files.get(fileInfo.fileId);
|
|
141
|
+
const vault = await ctx.sdk.vaults.get(fileObj.vaultId);
|
|
142
|
+
const meta = await sealDecryptMetadata(fileInfo.encryptedMetadata, vault, keypair);
|
|
143
|
+
return meta.n;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Fall through to placeholder
|
|
148
|
+
}
|
|
149
|
+
return fileInfo.name;
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
/**
|
|
121
153
|
* Encrypt (if private vault) and upload a buffer to Tusky.
|
|
122
154
|
* Shared by tusky_file_upload (from disk) and tusky_file_create (from content).
|
|
@@ -141,6 +173,8 @@ async function encryptAndUpload(
|
|
|
141
173
|
plaintextChecksumSha256?: string;
|
|
142
174
|
sealIdentity?: string;
|
|
143
175
|
sealEncryptedObject?: string;
|
|
176
|
+
encryptedName?: string;
|
|
177
|
+
encryptedMetadata?: string;
|
|
144
178
|
} = {};
|
|
145
179
|
|
|
146
180
|
if (isPrivate) {
|
|
@@ -159,6 +193,7 @@ async function encryptAndUpload(
|
|
|
159
193
|
encryptionIv: iv,
|
|
160
194
|
plaintextSizeBytes: fileBuffer.length,
|
|
161
195
|
plaintextChecksumSha256: plaintextChecksum,
|
|
196
|
+
encryptedName: encryptMetadata(fileName, mimeType, masterKey),
|
|
162
197
|
};
|
|
163
198
|
} else if (isShared) {
|
|
164
199
|
const keypair = ctx.getSuiKeypair();
|
|
@@ -176,6 +211,7 @@ async function encryptAndUpload(
|
|
|
176
211
|
sealIdentity: sealResult.sealIdentity,
|
|
177
212
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
178
213
|
plaintextSizeBytes: fileBuffer.length,
|
|
214
|
+
encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
|
|
179
215
|
};
|
|
180
216
|
} else {
|
|
181
217
|
uploadBody = fileBuffer;
|
|
@@ -313,13 +349,14 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
313
349
|
const { fileBuffer, encryption } = await fetchAndDecryptFile(fileId, ctx);
|
|
314
350
|
|
|
315
351
|
const fileInfo = await ctx.sdk.files.getStatus(fileId);
|
|
352
|
+
const realName = await resolveFilename({ ...fileInfo, fileId }, ctx);
|
|
316
353
|
const finalPath = resolve(outputPath);
|
|
317
354
|
|
|
318
355
|
writeFileSync(finalPath, fileBuffer);
|
|
319
356
|
|
|
320
357
|
const result = {
|
|
321
358
|
fileId,
|
|
322
|
-
name:
|
|
359
|
+
name: realName,
|
|
323
360
|
savedTo: finalPath,
|
|
324
361
|
sizeBytes: fileBuffer.length,
|
|
325
362
|
mimeType: fileInfo.mimeType,
|
|
@@ -346,6 +383,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
346
383
|
try {
|
|
347
384
|
const { fileBuffer, encryption } = await fetchAndDecryptFile(fileId, ctx);
|
|
348
385
|
const fileInfo = await ctx.sdk.files.getStatus(fileId);
|
|
386
|
+
const realName = await resolveFilename({ ...fileInfo, fileId }, ctx);
|
|
349
387
|
|
|
350
388
|
const isText = isTextMimeType(fileInfo.mimeType);
|
|
351
389
|
|
|
@@ -354,7 +392,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
354
392
|
content: [
|
|
355
393
|
{
|
|
356
394
|
type: 'text' as const,
|
|
357
|
-
text: `--- ${
|
|
395
|
+
text: `--- ${realName} (${fileInfo.mimeType}, ${fileBuffer.length} bytes${encryption?.encrypted ? ', decrypted' : ''}) ---\n\n${fileBuffer.toString('utf-8')}`,
|
|
358
396
|
},
|
|
359
397
|
],
|
|
360
398
|
};
|
|
@@ -367,7 +405,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
367
405
|
type: 'text' as const,
|
|
368
406
|
text: JSON.stringify({
|
|
369
407
|
fileId,
|
|
370
|
-
name:
|
|
408
|
+
name: realName,
|
|
371
409
|
mimeType: fileInfo.mimeType,
|
|
372
410
|
sizeBytes: fileBuffer.length,
|
|
373
411
|
wasEncrypted: encryption?.encrypted ?? false,
|
package/src/seal.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
|
18
18
|
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
19
19
|
import { Transaction } from '@mysten/sui/transactions';
|
|
20
20
|
import { fromHex } from '@mysten/sui/utils';
|
|
21
|
-
import { SEAL_PACKAGE_ID, SEAL_DEFAULT_KEY_SERVER_CONFIGS } from '@tuskydp/shared/constants.js';
|
|
21
|
+
import { SEAL_PACKAGE_ID, SEAL_DEFAULT_KEY_SERVER_CONFIGS, METADATA_NONCE_PREFIX } from '@tuskydp/shared/constants.js';
|
|
22
22
|
import type { VaultResponse } from '@tuskydp/sdk';
|
|
23
23
|
import { getSuiPrivateKey } from './config.js';
|
|
24
24
|
|
|
@@ -193,6 +193,39 @@ export async function sealEncrypt(
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Encrypt filename + MIME type using SEAL for a shared vault.
|
|
198
|
+
* Uses a distinct nonce (METADATA_NONCE_PREFIX + fileNonce) so the metadata
|
|
199
|
+
* identity differs from the file content identity.
|
|
200
|
+
*
|
|
201
|
+
* Returns base64-encoded full SEAL EncryptedObject (stored in encrypted_metadata column).
|
|
202
|
+
*/
|
|
203
|
+
export async function sealEncryptMetadata(
|
|
204
|
+
name: string,
|
|
205
|
+
mimeType: string,
|
|
206
|
+
vault: VaultResponse,
|
|
207
|
+
fileNonce: string,
|
|
208
|
+
): Promise<string> {
|
|
209
|
+
const plaintext = new TextEncoder().encode(JSON.stringify({ n: name, m: mimeType }));
|
|
210
|
+
const metaNonce = METADATA_NONCE_PREFIX + fileNonce;
|
|
211
|
+
const result = await sealEncrypt(plaintext, vault, metaNonce);
|
|
212
|
+
return Buffer.from(result.encryptedData).toString('base64');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Decrypt an encryptedMetadata blob produced by sealEncryptMetadata.
|
|
217
|
+
* Returns the original { n: name, m: mimeType }.
|
|
218
|
+
*/
|
|
219
|
+
export async function sealDecryptMetadata(
|
|
220
|
+
encryptedBase64: string,
|
|
221
|
+
vault: VaultResponse,
|
|
222
|
+
keypair: import('@mysten/sui/keypairs/ed25519').Ed25519Keypair,
|
|
223
|
+
): Promise<{ n: string; m: string }> {
|
|
224
|
+
const encryptedData = new Uint8Array(Buffer.from(encryptedBase64, 'base64'));
|
|
225
|
+
const plaintext = await sealDecrypt(encryptedData, vault, keypair);
|
|
226
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
227
|
+
}
|
|
228
|
+
|
|
196
229
|
// ---------------------------------------------------------------------------
|
|
197
230
|
// Decrypt
|
|
198
231
|
// ---------------------------------------------------------------------------
|
package/src/tui/files-panel.ts
CHANGED
|
@@ -2,6 +2,12 @@ import blessed from 'blessed';
|
|
|
2
2
|
import type { TuskyClient } from '@tuskydp/sdk';
|
|
3
3
|
import { formatBytes, formatDate, formatRow, statusColor, statusColorClose, getCurrentTheme } from './helpers.js';
|
|
4
4
|
|
|
5
|
+
export interface FolderItem {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
parentId: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export interface FileItem {
|
|
6
12
|
id: string;
|
|
7
13
|
name: string;
|
|
@@ -13,6 +19,9 @@ export interface FileItem {
|
|
|
13
19
|
walrusBlobId: string | null;
|
|
14
20
|
walrusBlobObjectId: string | null;
|
|
15
21
|
encrypted: boolean;
|
|
22
|
+
folderId: string | null;
|
|
23
|
+
autoRenew: boolean;
|
|
24
|
+
ppuEpochEnd: string | null;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export class FilesPanel {
|
|
@@ -20,9 +29,15 @@ export class FilesPanel {
|
|
|
20
29
|
private screen: blessed.Widgets.Screen;
|
|
21
30
|
private sdk: TuskyClient;
|
|
22
31
|
private files: FileItem[] = [];
|
|
32
|
+
private folders: FolderItem[] = [];
|
|
33
|
+
/** Combined display items: folders first, then files */
|
|
34
|
+
private displayItems: Array<{ type: 'folder'; folder: FolderItem } | { type: 'file'; file: FileItem } | { type: 'parent' }> = [];
|
|
23
35
|
private loading = false;
|
|
24
36
|
private currentVaultId: string | null = null;
|
|
25
37
|
private currentVaultName = '';
|
|
38
|
+
private currentFolderId: string | null = null;
|
|
39
|
+
private currentFolderName: string | null = null;
|
|
40
|
+
private folderStack: Array<{ id: string | null; name: string | null }> = [];
|
|
26
41
|
|
|
27
42
|
constructor(
|
|
28
43
|
screen: blessed.Widgets.Screen,
|
|
@@ -63,18 +78,43 @@ export class FilesPanel {
|
|
|
63
78
|
});
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
async loadForVault(vaultId: string, vaultName: string) {
|
|
81
|
+
async loadForVault(vaultId: string, vaultName: string, folderId?: string | null) {
|
|
67
82
|
if (this.loading) return;
|
|
68
83
|
this.loading = true;
|
|
69
84
|
this.currentVaultId = vaultId;
|
|
70
85
|
this.currentVaultName = vaultName;
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
|
|
87
|
+
if (folderId !== undefined) {
|
|
88
|
+
this.currentFolderId = folderId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const folderPath = this.currentFolderName
|
|
92
|
+
? `${vaultName} / ${this.currentFolderName}`
|
|
93
|
+
: vaultName;
|
|
94
|
+
this.list.setLabel(` Files — ${folderPath} `);
|
|
95
|
+
this.list.setItems(['Loading...'] as any);
|
|
73
96
|
this.screen.render();
|
|
74
97
|
|
|
75
98
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
// Load folders and files for current context
|
|
100
|
+
const [foldersResult, filesResult] = await Promise.all([
|
|
101
|
+
this.sdk.folders.list({ vaultId, parentId: this.currentFolderId || undefined }).catch(() => []),
|
|
102
|
+
this.sdk.files.list({
|
|
103
|
+
vaultId,
|
|
104
|
+
folderId: this.currentFolderId || undefined,
|
|
105
|
+
limit: 100,
|
|
106
|
+
sortBy: 'createdAt',
|
|
107
|
+
order: 'desc',
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
this.folders = (foldersResult as any[]).map((f) => ({
|
|
112
|
+
id: f.id,
|
|
113
|
+
name: f.name,
|
|
114
|
+
parentId: f.parentId || null,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
this.files = filesResult.files.map((f) => ({
|
|
78
118
|
id: f.id,
|
|
79
119
|
name: f.name,
|
|
80
120
|
sizeBytes: f.sizeBytes || 0,
|
|
@@ -85,10 +125,31 @@ export class FilesPanel {
|
|
|
85
125
|
walrusBlobId: f.walrusBlobId || null,
|
|
86
126
|
walrusBlobObjectId: f.walrusBlobObjectId || null,
|
|
87
127
|
encrypted: f.encrypted || false,
|
|
128
|
+
folderId: f.folderId || null,
|
|
129
|
+
autoRenew: f.autoRenew || false,
|
|
130
|
+
ppuEpochEnd: f.ppuEpochEnd || null,
|
|
88
131
|
}));
|
|
89
132
|
|
|
90
|
-
|
|
91
|
-
|
|
133
|
+
// Build combined display list
|
|
134
|
+
this.displayItems = [];
|
|
135
|
+
|
|
136
|
+
// Show ".." to go up if we're in a subfolder
|
|
137
|
+
if (this.currentFolderId) {
|
|
138
|
+
this.displayItems.push({ type: 'parent' });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Folders first
|
|
142
|
+
for (const folder of this.folders) {
|
|
143
|
+
this.displayItems.push({ type: 'folder', folder });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Then files
|
|
147
|
+
for (const file of this.files) {
|
|
148
|
+
this.displayItems.push({ type: 'file', file });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.displayItems.length === 0) {
|
|
152
|
+
this.list.setItems(['(empty — press u to upload, f to create folder)'] as any);
|
|
92
153
|
} else {
|
|
93
154
|
const totalW = (this.list.width as number) - 4;
|
|
94
155
|
const sizeW = 9;
|
|
@@ -97,13 +158,37 @@ export class FilesPanel {
|
|
|
97
158
|
const encW = 3;
|
|
98
159
|
const nameW = Math.max(10, totalW - sizeW - statusW - dateW - encW - 4);
|
|
99
160
|
|
|
100
|
-
const items = this.
|
|
161
|
+
const items = this.displayItems.map((item) => {
|
|
162
|
+
if (item.type === 'parent') {
|
|
163
|
+
return formatRow([
|
|
164
|
+
{ text: '../', tagged: '{yellow-fg}../{/yellow-fg}', width: nameW },
|
|
165
|
+
{ text: '', width: sizeW, align: 'right' },
|
|
166
|
+
{ text: '', width: statusW },
|
|
167
|
+
{ text: '', width: encW },
|
|
168
|
+
{ text: '', width: dateW, align: 'right' },
|
|
169
|
+
], totalW);
|
|
170
|
+
}
|
|
171
|
+
if (item.type === 'folder') {
|
|
172
|
+
const name = item.folder.name;
|
|
173
|
+
const display = name.length > nameW - 2
|
|
174
|
+
? name.slice(0, nameW - 3) + '...'
|
|
175
|
+
: name + '/';
|
|
176
|
+
return formatRow([
|
|
177
|
+
{ text: display, tagged: `{cyan-fg}${display}{/cyan-fg}`, width: nameW },
|
|
178
|
+
{ text: '', width: sizeW, align: 'right' },
|
|
179
|
+
{ text: 'folder', tagged: '{cyan-fg}folder{/cyan-fg}', width: statusW },
|
|
180
|
+
{ text: '', width: encW },
|
|
181
|
+
{ text: '', width: dateW, align: 'right' },
|
|
182
|
+
], totalW);
|
|
183
|
+
}
|
|
184
|
+
// file
|
|
185
|
+
const f = item.file;
|
|
101
186
|
const size = formatBytes(f.plaintextSizeBytes ?? f.sizeBytes);
|
|
102
187
|
const status = f.status;
|
|
103
188
|
const statusTagged = `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`;
|
|
104
189
|
const date = f.createdAt ? formatDate(f.createdAt) : '';
|
|
105
190
|
const enc = f.encrypted ? '🔒' : '';
|
|
106
|
-
const name = f.name.length > nameW ? f.name.slice(0, nameW - 1) + '
|
|
191
|
+
const name = f.name.length > nameW ? f.name.slice(0, nameW - 1) + '...' : f.name;
|
|
107
192
|
return formatRow([
|
|
108
193
|
{ text: name, width: nameW },
|
|
109
194
|
{ text: size, width: sizeW, align: 'right' },
|
|
@@ -122,18 +207,57 @@ export class FilesPanel {
|
|
|
122
207
|
this.screen.render();
|
|
123
208
|
}
|
|
124
209
|
|
|
210
|
+
/** Navigate into a folder or back to parent */
|
|
211
|
+
async navigateToFolder(folderId: string | null, folderName: string | null) {
|
|
212
|
+
if (!this.currentVaultId) return;
|
|
213
|
+
|
|
214
|
+
// Push current location onto stack before navigating
|
|
215
|
+
this.folderStack.push({ id: this.currentFolderId, name: this.currentFolderName });
|
|
216
|
+
this.currentFolderId = folderId;
|
|
217
|
+
this.currentFolderName = folderName;
|
|
218
|
+
await this.loadForVault(this.currentVaultId, this.currentVaultName);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Navigate back to parent folder */
|
|
222
|
+
async navigateUp() {
|
|
223
|
+
if (!this.currentVaultId || !this.currentFolderId) return;
|
|
224
|
+
|
|
225
|
+
const prev = this.folderStack.pop();
|
|
226
|
+
this.currentFolderId = prev?.id || null;
|
|
227
|
+
this.currentFolderName = prev?.name || null;
|
|
228
|
+
await this.loadForVault(this.currentVaultId, this.currentVaultName);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Reset folder navigation when switching vaults */
|
|
232
|
+
resetFolderNav() {
|
|
233
|
+
this.currentFolderId = null;
|
|
234
|
+
this.currentFolderName = null;
|
|
235
|
+
this.folderStack = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getSelectedItem(): { type: 'parent' } | { type: 'folder'; folder: FolderItem } | { type: 'file'; file: FileItem } | null {
|
|
239
|
+
const idx = (this.list as any).selected as number;
|
|
240
|
+
return this.displayItems[idx] || null;
|
|
241
|
+
}
|
|
242
|
+
|
|
125
243
|
clear() {
|
|
126
244
|
this.files = [];
|
|
245
|
+
this.folders = [];
|
|
246
|
+
this.displayItems = [];
|
|
127
247
|
this.currentVaultId = null;
|
|
128
248
|
this.currentVaultName = '';
|
|
249
|
+
this.currentFolderId = null;
|
|
250
|
+
this.currentFolderName = null;
|
|
251
|
+
this.folderStack = [];
|
|
129
252
|
this.list.setLabel(' Files ');
|
|
130
253
|
this.list.setItems(['Select a vault'] as any);
|
|
131
254
|
this.screen.render();
|
|
132
255
|
}
|
|
133
256
|
|
|
134
257
|
getSelected(): FileItem | null {
|
|
135
|
-
const
|
|
136
|
-
|
|
258
|
+
const item = this.getSelectedItem();
|
|
259
|
+
if (item && item.type === 'file') return item.file;
|
|
260
|
+
return null;
|
|
137
261
|
}
|
|
138
262
|
|
|
139
263
|
getFiles(): FileItem[] {
|
|
@@ -148,6 +272,10 @@ export class FilesPanel {
|
|
|
148
272
|
return this.currentVaultName;
|
|
149
273
|
}
|
|
150
274
|
|
|
275
|
+
getCurrentFolderId(): string | null {
|
|
276
|
+
return this.currentFolderId;
|
|
277
|
+
}
|
|
278
|
+
|
|
151
279
|
focus() {
|
|
152
280
|
this.list.focus();
|
|
153
281
|
this.list.style.border = { fg: getCurrentTheme().borderFocus } as any;
|