@lifestreamdynamics/vault-cli 1.1.0 → 1.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/README.md +140 -30
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -11
- package/dist/commands/admin.js +5 -5
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +124 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- package/dist/commands/auth.js +10 -105
- package/dist/commands/booking.d.ts +2 -0
- package/dist/commands/booking.js +739 -0
- package/dist/commands/calendar.js +778 -6
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.js +60 -0
- package/dist/commands/config.js +17 -16
- package/dist/commands/connectors.js +12 -1
- package/dist/commands/custom-domains.d.ts +2 -0
- package/dist/commands/custom-domains.js +154 -0
- package/dist/commands/docs.js +152 -5
- package/dist/commands/hooks.js +6 -1
- package/dist/commands/links.js +9 -2
- package/dist/commands/mfa.js +1 -70
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +172 -0
- package/dist/commands/publish-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +63 -2
- package/dist/commands/saml.d.ts +2 -0
- package/dist/commands/saml.js +220 -0
- package/dist/commands/scim.d.ts +2 -0
- package/dist/commands/scim.js +238 -0
- package/dist/commands/shares.js +25 -3
- package/dist/commands/subscription.js +9 -2
- package/dist/commands/sync.js +3 -0
- package/dist/commands/teams.js +233 -4
- package/dist/commands/user.js +444 -0
- package/dist/commands/vaults.js +240 -8
- package/dist/commands/webhooks.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -3
- package/dist/index.js +28 -1
- package/dist/lib/credential-manager.js +32 -7
- package/dist/lib/migration.js +2 -2
- package/dist/lib/profiles.js +4 -4
- package/dist/sync/config.js +2 -2
- package/dist/sync/daemon-worker.js +13 -6
- package/dist/sync/daemon.js +2 -1
- package/dist/sync/remote-poller.js +7 -3
- package/dist/sync/state.js +2 -2
- package/dist/utils/confirm.d.ts +11 -0
- package/dist/utils/confirm.js +23 -0
- package/dist/utils/format.js +1 -1
- package/dist/utils/output.js +4 -1
- package/dist/utils/prompt.d.ts +29 -0
- package/dist/utils/prompt.js +146 -0
- package/package.json +2 -2
package/dist/commands/vaults.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getClientAsync } from '../client.js';
|
|
|
3
3
|
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
4
|
import { createOutput, handleError } from '../utils/output.js';
|
|
5
5
|
import { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
|
|
6
|
-
import {
|
|
6
|
+
import { getCredentialManager } from '../config.js';
|
|
7
7
|
export function registerVaultCommands(program) {
|
|
8
8
|
const vaults = program.command('vaults').description('Create, list, and inspect document vaults');
|
|
9
9
|
addGlobalFlags(vaults.command('list')
|
|
@@ -84,7 +84,7 @@ EXAMPLES
|
|
|
84
84
|
});
|
|
85
85
|
if (isEncrypted) {
|
|
86
86
|
const key = generateVaultKey();
|
|
87
|
-
const credManager =
|
|
87
|
+
const credManager = getCredentialManager();
|
|
88
88
|
await credManager.saveVaultKey(vault.id, key);
|
|
89
89
|
out.success(`Encrypted vault created: ${chalk.cyan(vault.name)} (${vault.slug})`, {
|
|
90
90
|
id: vault.id,
|
|
@@ -93,10 +93,13 @@ EXAMPLES
|
|
|
93
93
|
encrypted: true,
|
|
94
94
|
vaultKey: key,
|
|
95
95
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
// Always write key warning to stderr — even in JSON mode the caller must not miss this.
|
|
97
|
+
process.stderr.write(chalk.yellow('WARNING: Save this encryption key securely. If lost, your data cannot be recovered.\n'));
|
|
98
|
+
if (flags.output !== 'json') {
|
|
99
|
+
out.status(`Vault Key: ${chalk.green(key)}`);
|
|
100
|
+
out.status(chalk.dim('The key has been saved to your credential store.'));
|
|
101
|
+
out.status(chalk.dim('You can export it later with: lsvault vaults export-key ' + vault.id));
|
|
102
|
+
}
|
|
100
103
|
out.warn('Encrypted vaults disable: full-text search, AI features, hooks, and webhooks.');
|
|
101
104
|
}
|
|
102
105
|
else {
|
|
@@ -118,7 +121,7 @@ EXAMPLES
|
|
|
118
121
|
const flags = resolveFlags(_opts);
|
|
119
122
|
const out = createOutput(flags);
|
|
120
123
|
try {
|
|
121
|
-
const credManager =
|
|
124
|
+
const credManager = getCredentialManager();
|
|
122
125
|
const key = await credManager.getVaultKey(vaultId);
|
|
123
126
|
if (!key) {
|
|
124
127
|
out.error('No encryption key found for vault ' + vaultId);
|
|
@@ -146,7 +149,7 @@ EXAMPLES
|
|
|
146
149
|
process.exitCode = 1;
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
|
-
const credManager =
|
|
152
|
+
const credManager = getCredentialManager();
|
|
150
153
|
await credManager.saveVaultKey(vaultId, keyValue);
|
|
151
154
|
out.success('Vault encryption key saved successfully.', { vaultId });
|
|
152
155
|
}
|
|
@@ -154,4 +157,233 @@ EXAMPLES
|
|
|
154
157
|
handleError(out, err, 'Failed to import vault key');
|
|
155
158
|
}
|
|
156
159
|
});
|
|
160
|
+
// vault tree
|
|
161
|
+
addGlobalFlags(vaults.command('tree')
|
|
162
|
+
.description('Show vault file tree')
|
|
163
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
164
|
+
.action(async (vaultId, _opts) => {
|
|
165
|
+
const flags = resolveFlags(_opts);
|
|
166
|
+
const out = createOutput(flags);
|
|
167
|
+
out.startSpinner('Fetching vault tree...');
|
|
168
|
+
try {
|
|
169
|
+
const client = await getClientAsync();
|
|
170
|
+
const tree = await client.vaults.getTree(vaultId);
|
|
171
|
+
out.stopSpinner();
|
|
172
|
+
if (flags.output === 'json') {
|
|
173
|
+
out.raw(JSON.stringify(tree, null, 2) + '\n');
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
function printNode(node, depth) {
|
|
177
|
+
const indent = ' '.repeat(depth);
|
|
178
|
+
const icon = node.type === 'directory' ? chalk.yellow('📁') : chalk.cyan('📄');
|
|
179
|
+
process.stdout.write(`${indent}${icon} ${node.name}\n`);
|
|
180
|
+
if (node.children) {
|
|
181
|
+
for (const child of node.children)
|
|
182
|
+
printNode(child, depth + 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const node of tree)
|
|
186
|
+
printNode(node, 0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
handleError(out, err, 'Failed to fetch vault tree');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// vault archive
|
|
194
|
+
addGlobalFlags(vaults.command('archive')
|
|
195
|
+
.description('Archive a vault')
|
|
196
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
197
|
+
.action(async (vaultId, _opts) => {
|
|
198
|
+
const flags = resolveFlags(_opts);
|
|
199
|
+
const out = createOutput(flags);
|
|
200
|
+
out.startSpinner('Archiving vault...');
|
|
201
|
+
try {
|
|
202
|
+
const client = await getClientAsync();
|
|
203
|
+
const vault = await client.vaults.archive(vaultId);
|
|
204
|
+
out.success(`Vault archived: ${vault.name}`, { id: vault.id, name: vault.name, isArchived: vault.isArchived });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
handleError(out, err, 'Failed to archive vault');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// vault unarchive
|
|
211
|
+
addGlobalFlags(vaults.command('unarchive')
|
|
212
|
+
.description('Unarchive a vault')
|
|
213
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
214
|
+
.action(async (vaultId, _opts) => {
|
|
215
|
+
const flags = resolveFlags(_opts);
|
|
216
|
+
const out = createOutput(flags);
|
|
217
|
+
out.startSpinner('Unarchiving vault...');
|
|
218
|
+
try {
|
|
219
|
+
const client = await getClientAsync();
|
|
220
|
+
const vault = await client.vaults.unarchive(vaultId);
|
|
221
|
+
out.success(`Vault unarchived: ${vault.name}`, { id: vault.id, name: vault.name, isArchived: vault.isArchived });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
handleError(out, err, 'Failed to unarchive vault');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// vault transfer
|
|
228
|
+
addGlobalFlags(vaults.command('transfer')
|
|
229
|
+
.description('Transfer vault ownership to another user')
|
|
230
|
+
.argument('<vaultId>', 'Vault ID')
|
|
231
|
+
.argument('<targetEmail>', 'Email of the user to transfer to'))
|
|
232
|
+
.action(async (vaultId, targetEmail, _opts) => {
|
|
233
|
+
const flags = resolveFlags(_opts);
|
|
234
|
+
const out = createOutput(flags);
|
|
235
|
+
out.startSpinner('Transferring vault...');
|
|
236
|
+
try {
|
|
237
|
+
const client = await getClientAsync();
|
|
238
|
+
const vault = await client.vaults.transfer(vaultId, targetEmail);
|
|
239
|
+
out.success(`Vault transferred to ${targetEmail}`, { id: vault.id, name: vault.name });
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
handleError(out, err, 'Failed to transfer vault');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// vault export-vault subgroup
|
|
246
|
+
const exportVault = vaults.command('export-vault').description('Vault export operations');
|
|
247
|
+
addGlobalFlags(exportVault.command('create')
|
|
248
|
+
.description('Create a vault export')
|
|
249
|
+
.argument('<vaultId>', 'Vault ID')
|
|
250
|
+
.option('--metadata', 'Include metadata in export')
|
|
251
|
+
.option('--format <fmt>', 'Export format', 'zip'))
|
|
252
|
+
.action(async (vaultId, _opts) => {
|
|
253
|
+
const flags = resolveFlags(_opts);
|
|
254
|
+
const out = createOutput(flags);
|
|
255
|
+
out.startSpinner('Creating export...');
|
|
256
|
+
try {
|
|
257
|
+
const client = await getClientAsync();
|
|
258
|
+
const exp = await client.vaults.createExport(vaultId, {
|
|
259
|
+
includeMetadata: _opts.metadata === true,
|
|
260
|
+
format: _opts.format || 'zip',
|
|
261
|
+
});
|
|
262
|
+
out.success('Export created', { id: exp.id, status: exp.status, format: exp.format });
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
handleError(out, err, 'Failed to create export');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
addGlobalFlags(exportVault.command('list')
|
|
269
|
+
.description('List vault exports')
|
|
270
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
271
|
+
.action(async (vaultId, _opts) => {
|
|
272
|
+
const flags = resolveFlags(_opts);
|
|
273
|
+
const out = createOutput(flags);
|
|
274
|
+
out.startSpinner('Fetching exports...');
|
|
275
|
+
try {
|
|
276
|
+
const client = await getClientAsync();
|
|
277
|
+
const exports = await client.vaults.listExports(vaultId);
|
|
278
|
+
out.stopSpinner();
|
|
279
|
+
out.list(exports.map(e => ({ id: e.id, status: e.status, format: e.format, createdAt: e.createdAt, completedAt: e.completedAt || '' })), {
|
|
280
|
+
emptyMessage: 'No exports found.',
|
|
281
|
+
columns: [
|
|
282
|
+
{ key: 'id', header: 'ID' },
|
|
283
|
+
{ key: 'status', header: 'Status' },
|
|
284
|
+
{ key: 'format', header: 'Format' },
|
|
285
|
+
{ key: 'createdAt', header: 'Created' },
|
|
286
|
+
{ key: 'completedAt', header: 'Completed' },
|
|
287
|
+
],
|
|
288
|
+
textFn: (e) => `${chalk.cyan(String(e.id))} [${String(e.status)}] ${String(e.format)} created: ${String(e.createdAt)}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
handleError(out, err, 'Failed to list exports');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
addGlobalFlags(exportVault.command('download')
|
|
296
|
+
.description('Download a vault export')
|
|
297
|
+
.argument('<vaultId>', 'Vault ID')
|
|
298
|
+
.argument('<exportId>', 'Export ID')
|
|
299
|
+
.requiredOption('--file <path>', 'Output file path'))
|
|
300
|
+
.action(async (vaultId, exportId, _opts) => {
|
|
301
|
+
const flags = resolveFlags(_opts);
|
|
302
|
+
const out = createOutput(flags);
|
|
303
|
+
out.startSpinner('Downloading export...');
|
|
304
|
+
try {
|
|
305
|
+
const { writeFile } = await import('node:fs/promises');
|
|
306
|
+
const client = await getClientAsync();
|
|
307
|
+
const blob = await client.vaults.downloadExport(vaultId, exportId);
|
|
308
|
+
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
309
|
+
await writeFile(_opts.file, buffer);
|
|
310
|
+
out.success(`Export downloaded to ${String(_opts.file)}`, { path: _opts.file, size: buffer.length });
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
handleError(out, err, 'Failed to download export');
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
// vault mfa subgroup
|
|
317
|
+
const mfa = vaults.command('mfa').description('Vault MFA configuration');
|
|
318
|
+
addGlobalFlags(mfa.command('get')
|
|
319
|
+
.description('Get vault MFA configuration')
|
|
320
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
321
|
+
.action(async (vaultId, _opts) => {
|
|
322
|
+
const flags = resolveFlags(_opts);
|
|
323
|
+
const out = createOutput(flags);
|
|
324
|
+
out.startSpinner('Fetching MFA config...');
|
|
325
|
+
try {
|
|
326
|
+
const client = await getClientAsync();
|
|
327
|
+
const config = await client.vaults.getMfaConfig(vaultId);
|
|
328
|
+
out.stopSpinner();
|
|
329
|
+
out.record({
|
|
330
|
+
mfaRequired: config.mfaRequired,
|
|
331
|
+
sessionWindowMinutes: config.sessionWindowMinutes,
|
|
332
|
+
userVerified: config.userVerified,
|
|
333
|
+
verificationExpiresAt: config.verificationExpiresAt,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
handleError(out, err, 'Failed to fetch MFA config');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
addGlobalFlags(mfa.command('set')
|
|
341
|
+
.description('Set vault MFA configuration')
|
|
342
|
+
.argument('<vaultId>', 'Vault ID')
|
|
343
|
+
.option('--require', 'Require MFA for vault access')
|
|
344
|
+
.option('--no-require', 'Disable MFA requirement')
|
|
345
|
+
.option('--window <minutes>', 'Session window in minutes', '60'))
|
|
346
|
+
.action(async (vaultId, _opts) => {
|
|
347
|
+
const flags = resolveFlags(_opts);
|
|
348
|
+
const out = createOutput(flags);
|
|
349
|
+
// Require an explicit --require or --no-require flag to avoid silent side-effects.
|
|
350
|
+
if (_opts.require === undefined) {
|
|
351
|
+
out.error('You must pass --require or --no-require to set the MFA policy.');
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
out.startSpinner('Updating MFA config...');
|
|
356
|
+
try {
|
|
357
|
+
const client = await getClientAsync();
|
|
358
|
+
const config = await client.vaults.setMfaConfig(vaultId, {
|
|
359
|
+
mfaRequired: _opts.require !== false,
|
|
360
|
+
sessionWindowMinutes: parseInt(String(_opts.window || '60'), 10),
|
|
361
|
+
});
|
|
362
|
+
out.success('MFA config updated', { mfaRequired: config.mfaRequired, sessionWindowMinutes: config.sessionWindowMinutes });
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
handleError(out, err, 'Failed to update MFA config');
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
addGlobalFlags(mfa.command('verify')
|
|
369
|
+
.description('Verify MFA for vault access')
|
|
370
|
+
.argument('<vaultId>', 'Vault ID')
|
|
371
|
+
.requiredOption('--method <totp|backup_code>', 'MFA method')
|
|
372
|
+
.requiredOption('--code <code>', 'MFA code'))
|
|
373
|
+
.action(async (vaultId, _opts) => {
|
|
374
|
+
const flags = resolveFlags(_opts);
|
|
375
|
+
const out = createOutput(flags);
|
|
376
|
+
out.startSpinner('Verifying MFA...');
|
|
377
|
+
try {
|
|
378
|
+
const client = await getClientAsync();
|
|
379
|
+
const result = await client.vaults.verifyMfa(vaultId, {
|
|
380
|
+
method: _opts.method,
|
|
381
|
+
code: _opts.code,
|
|
382
|
+
});
|
|
383
|
+
out.success('MFA verified', { verified: result.verified, expiresAt: result.expiresAt });
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
handleError(out, err, 'Failed to verify MFA');
|
|
387
|
+
}
|
|
388
|
+
});
|
|
157
389
|
}
|
|
@@ -119,10 +119,15 @@ export function registerWebhookCommands(program) {
|
|
|
119
119
|
addGlobalFlags(webhooks.command('delete')
|
|
120
120
|
.description('Delete a webhook')
|
|
121
121
|
.argument('<vaultId>', 'Vault ID')
|
|
122
|
-
.argument('<webhookId>', 'Webhook ID')
|
|
122
|
+
.argument('<webhookId>', 'Webhook ID')
|
|
123
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
123
124
|
.action(async (vaultId, webhookId, _opts) => {
|
|
124
125
|
const flags = resolveFlags(_opts);
|
|
125
126
|
const out = createOutput(flags);
|
|
127
|
+
if (!_opts.yes) {
|
|
128
|
+
out.status(chalk.yellow(`Pass --yes to delete webhook ${webhookId}.`));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
126
131
|
out.startSpinner('Deleting webhook...');
|
|
127
132
|
try {
|
|
128
133
|
const client = await getClientAsync();
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface CliConfig {
|
|
|
5
5
|
apiKey?: string;
|
|
6
6
|
accessToken?: string;
|
|
7
7
|
refreshToken?: string;
|
|
8
|
+
/** Per-vault client-side encryption keys, keyed by vaultId */
|
|
9
|
+
vaultKeys?: Record<string, string>;
|
|
8
10
|
}
|
|
9
11
|
export declare function getCredentialManager(): CredentialManager;
|
|
10
12
|
/**
|
package/dist/config.js
CHANGED
|
@@ -71,19 +71,23 @@ export async function loadConfigAsync() {
|
|
|
71
71
|
const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
72
72
|
if (fileConfig.apiUrl && !config.apiUrl)
|
|
73
73
|
config.apiUrl = fileConfig.apiUrl;
|
|
74
|
-
if (fileConfig.apiKey && !config.apiKey)
|
|
74
|
+
if (fileConfig.apiKey && !config.apiKey) {
|
|
75
75
|
config.apiKey = fileConfig.apiKey;
|
|
76
|
+
process.stderr.write('\x1b[33mWarning: API key loaded from plaintext config (~/.lsvault/config.json).\n' +
|
|
77
|
+
'Run `lsvault auth migrate` to migrate to secure storage.\n' +
|
|
78
|
+
'See: https://docs.lifestreamdynamics.com/migration-to-secure-storage\x1b[0m\n');
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
return config;
|
|
78
82
|
}
|
|
79
83
|
export function saveConfig(config) {
|
|
80
84
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
81
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
85
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
82
86
|
}
|
|
83
87
|
let existing = {};
|
|
84
88
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
85
89
|
existing = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
86
90
|
}
|
|
87
91
|
const merged = { ...existing, ...config };
|
|
88
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
|
|
92
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
89
93
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
// Apply --no-color / NO_COLOR at startup so it takes effect for all output,
|
|
8
|
+
// including messages printed before any action handler runs.
|
|
9
|
+
if (process.argv.includes('--no-color') || process.env.NO_COLOR !== undefined) {
|
|
10
|
+
chalk.level = 0;
|
|
11
|
+
}
|
|
3
12
|
import { registerAuthCommands } from './commands/auth.js';
|
|
4
13
|
import { registerMfaCommands } from './commands/mfa.js';
|
|
5
14
|
import { registerVaultCommands } from './commands/vaults.js';
|
|
@@ -21,11 +30,20 @@ import { registerSyncCommands } from './commands/sync.js';
|
|
|
21
30
|
import { registerVersionCommands } from './commands/versions.js';
|
|
22
31
|
import { registerLinkCommands } from './commands/links.js';
|
|
23
32
|
import { registerCalendarCommands } from './commands/calendar.js';
|
|
33
|
+
import { registerBookingCommands } from './commands/booking.js';
|
|
34
|
+
import { registerAiCommands } from './commands/ai.js';
|
|
35
|
+
import { registerAnalyticsCommands } from './commands/analytics.js';
|
|
36
|
+
import { registerCustomDomainCommands } from './commands/custom-domains.js';
|
|
37
|
+
import { registerPublishVaultCommands } from './commands/publish-vault.js';
|
|
38
|
+
import { registerSamlCommands } from './commands/saml.js';
|
|
39
|
+
import { registerScimCommands } from './commands/scim.js';
|
|
40
|
+
import { registerPluginCommands } from './commands/plugins.js';
|
|
41
|
+
import { registerCompletionCommands } from './commands/completion.js';
|
|
24
42
|
const program = new Command();
|
|
25
43
|
program
|
|
26
44
|
.name('lsvault')
|
|
27
45
|
.description('Lifestream Vault CLI - manage vaults, documents, and settings')
|
|
28
|
-
.version(
|
|
46
|
+
.version(pkg.version)
|
|
29
47
|
.addHelpText('after', `
|
|
30
48
|
GETTING STARTED
|
|
31
49
|
lsvault auth login --email <email> Log in with email/password
|
|
@@ -66,4 +84,13 @@ registerSyncCommands(program);
|
|
|
66
84
|
registerVersionCommands(program);
|
|
67
85
|
registerLinkCommands(program);
|
|
68
86
|
registerCalendarCommands(program);
|
|
87
|
+
registerBookingCommands(program);
|
|
88
|
+
registerAiCommands(program);
|
|
89
|
+
registerAnalyticsCommands(program);
|
|
90
|
+
registerCustomDomainCommands(program);
|
|
91
|
+
registerPublishVaultCommands(program);
|
|
92
|
+
registerSamlCommands(program);
|
|
93
|
+
registerScimCommands(program);
|
|
94
|
+
registerPluginCommands(program);
|
|
95
|
+
registerCompletionCommands(program);
|
|
69
96
|
program.parse();
|
|
@@ -1,13 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import os from 'node:os';
|
|
1
5
|
import { createKeychainBackend } from './keychain.js';
|
|
2
6
|
import { createEncryptedConfigBackend } from './encrypted-config.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Returns the machine-specific passphrase used to encrypt the local credential
|
|
9
|
+
* store. On first call it generates a cryptographically random 32-byte hex
|
|
10
|
+
* string, writes it to `~/.lsvault/.passphrase` (mode 0o600, directory 0o700),
|
|
11
|
+
* and returns it. Subsequent calls read the stored value.
|
|
12
|
+
*
|
|
13
|
+
* This replaces the old hardcoded `DEFAULT_PASSPHRASE` constant so that the
|
|
14
|
+
* passphrase is never present in source code or version control.
|
|
15
|
+
*/
|
|
16
|
+
function getOrCreatePassphrase() {
|
|
17
|
+
const configDir = path.join(os.homedir(), '.lsvault');
|
|
18
|
+
const passphrasePath = path.join(configDir, '.passphrase');
|
|
19
|
+
try {
|
|
20
|
+
return fs.readFileSync(passphrasePath, 'utf-8').trim();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// File does not exist yet — generate a new one.
|
|
24
|
+
const passphrase = crypto.randomBytes(32).toString('hex');
|
|
25
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
26
|
+
fs.writeFileSync(passphrasePath, passphrase, { mode: 0o600 });
|
|
27
|
+
return passphrase;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
export function createCredentialManager(options = {}) {
|
|
8
31
|
const keychain = options.keychain ?? createKeychainBackend();
|
|
9
32
|
const encryptedConfig = options.encryptedConfig ?? createEncryptedConfigBackend();
|
|
10
|
-
|
|
33
|
+
// Use the caller-supplied passphrase (tests pass one explicitly) or lazily
|
|
34
|
+
// generate / load the per-machine passphrase from ~/.lsvault/.passphrase.
|
|
35
|
+
const passphrase = options.passphrase ?? getOrCreatePassphrase();
|
|
11
36
|
return {
|
|
12
37
|
async getCredentials() {
|
|
13
38
|
const result = {};
|
|
@@ -87,13 +112,13 @@ export function createCredentialManager(options = {}) {
|
|
|
87
112
|
async saveVaultKey(vaultId, keyHex) {
|
|
88
113
|
// Read existing config, merge vault key, save
|
|
89
114
|
const existing = encryptedConfig.getCredentials(passphrase) ?? {};
|
|
90
|
-
const vaultKeys = existing.vaultKeys ?? {};
|
|
115
|
+
const vaultKeys = { ...(existing.vaultKeys ?? {}) };
|
|
91
116
|
vaultKeys[vaultId] = keyHex;
|
|
92
117
|
encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
|
|
93
118
|
},
|
|
94
119
|
async deleteVaultKey(vaultId) {
|
|
95
120
|
const existing = encryptedConfig.getCredentials(passphrase) ?? {};
|
|
96
|
-
const vaultKeys = existing.vaultKeys ?? {};
|
|
121
|
+
const vaultKeys = { ...(existing.vaultKeys ?? {}) };
|
|
97
122
|
delete vaultKeys[vaultId];
|
|
98
123
|
encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
|
|
99
124
|
},
|
package/dist/lib/migration.js
CHANGED
|
@@ -85,8 +85,8 @@ export async function migrateCredentials(credentialManager, promptFn) {
|
|
|
85
85
|
export async function checkAndPromptMigration(credentialManager) {
|
|
86
86
|
if (!hasPlaintextCredentials())
|
|
87
87
|
return false;
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
process.stderr.write(chalk.yellow('\nWarning: API key found in plaintext config (~/.lsvault/config.json)') + '\n');
|
|
89
|
+
process.stderr.write(chalk.yellow('Run `lsvault auth migrate` to migrate to secure storage.\n') + '\n');
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
export { CONFIG_FILE, CONFIG_DIR };
|
package/dist/lib/profiles.js
CHANGED
|
@@ -29,9 +29,9 @@ export function getActiveProfile() {
|
|
|
29
29
|
*/
|
|
30
30
|
export function setActiveProfile(name) {
|
|
31
31
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
32
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
33
33
|
}
|
|
34
|
-
fs.writeFileSync(ACTIVE_PROFILE_FILE, name + '\n');
|
|
34
|
+
fs.writeFileSync(ACTIVE_PROFILE_FILE, name + '\n', { mode: 0o600 });
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Loads a profile's configuration. Returns an empty object if the profile
|
|
@@ -54,11 +54,11 @@ export function loadProfile(name) {
|
|
|
54
54
|
*/
|
|
55
55
|
export function setProfileValue(name, key, value) {
|
|
56
56
|
if (!fs.existsSync(PROFILES_DIR)) {
|
|
57
|
-
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
57
|
+
fs.mkdirSync(PROFILES_DIR, { recursive: true, mode: 0o700 });
|
|
58
58
|
}
|
|
59
59
|
const config = loadProfile(name);
|
|
60
60
|
config[key] = value;
|
|
61
|
-
fs.writeFileSync(getProfilePath(name), JSON.stringify(config, null, 2) + '\n');
|
|
61
|
+
fs.writeFileSync(getProfilePath(name), JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Gets a single value from a profile.
|
package/dist/sync/config.js
CHANGED
|
@@ -31,9 +31,9 @@ export function loadSyncConfigs() {
|
|
|
31
31
|
*/
|
|
32
32
|
export function saveSyncConfigs(configs) {
|
|
33
33
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
35
35
|
}
|
|
36
|
-
fs.writeFileSync(SYNCS_FILE, JSON.stringify(configs, null, 2) + '\n');
|
|
36
|
+
fs.writeFileSync(SYNCS_FILE, JSON.stringify(configs, null, 2) + '\n', { mode: 0o600 });
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Find a sync config by its ID.
|
|
@@ -8,7 +8,7 @@ import { resolveIgnorePatterns } from './ignore.js';
|
|
|
8
8
|
import { createWatcher } from './watcher.js';
|
|
9
9
|
import { createRemotePoller } from './remote-poller.js';
|
|
10
10
|
import { removePid } from './daemon.js';
|
|
11
|
-
import {
|
|
11
|
+
import { loadConfigAsync } from '../config.js';
|
|
12
12
|
import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';
|
|
13
13
|
import { scanLocalFiles, scanRemoteFiles, computePushDiff, computePullDiff, executePush, executePull } from './engine.js';
|
|
14
14
|
import { loadSyncState } from './state.js';
|
|
@@ -17,10 +17,17 @@ function log(msg) {
|
|
|
17
17
|
const ts = new Date().toISOString();
|
|
18
18
|
process.stdout.write(`[${ts}] ${msg}\n`);
|
|
19
19
|
}
|
|
20
|
-
function createClient() {
|
|
21
|
-
const config =
|
|
22
|
-
if (!config.apiKey) {
|
|
23
|
-
throw new Error('No
|
|
20
|
+
async function createClient() {
|
|
21
|
+
const config = await loadConfigAsync();
|
|
22
|
+
if (!config.apiKey && !config.accessToken) {
|
|
23
|
+
throw new Error('No credentials configured. Run `lsvault auth login` first.');
|
|
24
|
+
}
|
|
25
|
+
if (config.accessToken) {
|
|
26
|
+
return new LifestreamVaultClient({
|
|
27
|
+
baseUrl: config.apiUrl,
|
|
28
|
+
accessToken: config.accessToken,
|
|
29
|
+
refreshToken: config.refreshToken,
|
|
30
|
+
});
|
|
24
31
|
}
|
|
25
32
|
return new LifestreamVaultClient({
|
|
26
33
|
baseUrl: config.apiUrl,
|
|
@@ -36,7 +43,7 @@ async function start() {
|
|
|
36
43
|
process.exit(0);
|
|
37
44
|
}
|
|
38
45
|
log(`Found ${configs.length} auto-sync configuration(s)`);
|
|
39
|
-
const client = createClient();
|
|
46
|
+
const client = await createClient();
|
|
40
47
|
// Startup reconciliation: catch changes made while daemon was stopped
|
|
41
48
|
for (const config of configs) {
|
|
42
49
|
try {
|
package/dist/sync/daemon.js
CHANGED
|
@@ -137,7 +137,8 @@ export function startDaemon(logFile) {
|
|
|
137
137
|
rotateLogIfNeeded(targetLog);
|
|
138
138
|
const logFd = fs.openSync(targetLog, 'a');
|
|
139
139
|
// Spawn the daemon worker as a detached process
|
|
140
|
-
|
|
140
|
+
// Use URL constructor for Node 20.0-20.10 compatibility (import.meta.dirname was added in 20.11).
|
|
141
|
+
const workerPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'daemon-worker.js');
|
|
141
142
|
const child = spawn(process.execPath, [workerPath], {
|
|
142
143
|
detached: true,
|
|
143
144
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -63,7 +63,9 @@ export function createRemotePoller(client, config, options) {
|
|
|
63
63
|
if (resolution === 'remote') {
|
|
64
64
|
conflictFile = createConflictFile(config.localPath, doc.path, localContent, 'local');
|
|
65
65
|
onLocalWrite?.(doc.path);
|
|
66
|
-
|
|
66
|
+
const tmpConflict = localFile + '.tmp';
|
|
67
|
+
fs.writeFileSync(tmpConflict, content, 'utf-8');
|
|
68
|
+
fs.renameSync(tmpConflict, localFile);
|
|
67
69
|
log(`Conflict: ${doc.path} — used remote, saved local as ${conflictFile}`);
|
|
68
70
|
}
|
|
69
71
|
else {
|
|
@@ -80,13 +82,15 @@ export function createRemotePoller(client, config, options) {
|
|
|
80
82
|
continue;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
|
-
// No conflict — download the file
|
|
85
|
+
// No conflict — download the file atomically
|
|
84
86
|
const dir = path.dirname(localFile);
|
|
85
87
|
if (!fs.existsSync(dir)) {
|
|
86
88
|
fs.mkdirSync(dir, { recursive: true });
|
|
87
89
|
}
|
|
88
90
|
onLocalWrite?.(doc.path);
|
|
89
|
-
|
|
91
|
+
const tmpFile = localFile + '.tmp';
|
|
92
|
+
fs.writeFileSync(tmpFile, content, 'utf-8');
|
|
93
|
+
fs.renameSync(tmpFile, localFile);
|
|
90
94
|
log(`Pulled: ${doc.path}`);
|
|
91
95
|
changes++;
|
|
92
96
|
state.local[doc.path] = {
|
package/dist/sync/state.js
CHANGED
|
@@ -43,10 +43,10 @@ export function loadSyncState(syncId) {
|
|
|
43
43
|
*/
|
|
44
44
|
export function saveSyncState(state) {
|
|
45
45
|
if (!fs.existsSync(STATE_DIR)) {
|
|
46
|
-
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
46
|
+
fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
47
47
|
}
|
|
48
48
|
state.updatedAt = new Date().toISOString();
|
|
49
|
-
fs.writeFileSync(stateFilePath(state.syncId), JSON.stringify(state, null, 2) + '\n');
|
|
49
|
+
fs.writeFileSync(stateFilePath(state.syncId), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Delete sync state for a given sync configuration.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt the user to confirm a destructive action.
|
|
3
|
+
*
|
|
4
|
+
* @param message - The confirmation message to display (without " [y/N] " suffix)
|
|
5
|
+
* @param options.yes - If true, skip the prompt and return true immediately (e.g. --yes flag)
|
|
6
|
+
* @returns Promise resolving to true if confirmed, false if declined
|
|
7
|
+
* @throws Error if stdin is not a TTY and --yes was not provided
|
|
8
|
+
*/
|
|
9
|
+
export declare function confirmAction(message: string, options?: {
|
|
10
|
+
yes?: boolean;
|
|
11
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
/**
|
|
3
|
+
* Prompt the user to confirm a destructive action.
|
|
4
|
+
*
|
|
5
|
+
* @param message - The confirmation message to display (without " [y/N] " suffix)
|
|
6
|
+
* @param options.yes - If true, skip the prompt and return true immediately (e.g. --yes flag)
|
|
7
|
+
* @returns Promise resolving to true if confirmed, false if declined
|
|
8
|
+
* @throws Error if stdin is not a TTY and --yes was not provided
|
|
9
|
+
*/
|
|
10
|
+
export async function confirmAction(message, options) {
|
|
11
|
+
if (options?.yes)
|
|
12
|
+
return true;
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
throw new Error('Cannot prompt for confirmation in non-interactive mode. Use --yes to skip confirmation.');
|
|
15
|
+
}
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/utils/format.js
CHANGED