squidcloudctl 3.0.5 → 3.0.7

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.
@@ -310,20 +310,37 @@ export async function downloadFile(target, destPath, onProgress) {
310
310
  const { detectChunkVersion, decryptAny, decryptV3, deriveRawKey } = await import('./crypto/res54.js');
311
311
  const master = deriveRawKey(defaultKey || '');
312
312
  const fileKeyRaw = noble.hkdf(sha2.sha256, master, new Uint8Array(0), enc.encode('v3-file-master' + target.id), 32);
313
+ let activeMaster = master;
313
314
  for (let i = 0; i < chunks.length; i++) {
314
315
  const blob = await clusterDownloadChunk(chunks[i], target.userId || '');
315
316
  const version = detectChunkVersion(blob);
316
317
  let plain;
317
- if (version === 3) {
318
- plain = decryptV3(blob, master, target.userId || '', target.id, i, chunks.length);
319
- }
320
- else if (version === 1 && !encrypted) {
318
+ if (version === 1 && !encrypted && !defaultKey) {
321
319
  plain = blob;
322
320
  }
323
321
  else {
324
- plain = decryptAny(blob, defaultKey || '', {
325
- userId: target.userId, fileId: target.id, chunkIndex: i, totalChunks: chunks.length,
326
- }).plain;
322
+ if (!defaultKey && byokCache === undefined) {
323
+ byokCache = (await askByokKey(target.name)) ?? '';
324
+ }
325
+ const attemptKey = defaultKey || byokCache || '';
326
+ try {
327
+ if (version === 3) {
328
+ const m2 = attemptKey ? deriveRawKey(attemptKey) : master;
329
+ plain = decryptV3(blob, m2, target.userId || '', target.id, i, chunks.length);
330
+ activeMaster = m2;
331
+ }
332
+ else {
333
+ plain = decryptAny(blob, attemptKey, {
334
+ userId: target.userId, fileId: target.id, chunkIndex: i, totalChunks: chunks.length,
335
+ }).plain;
336
+ }
337
+ }
338
+ catch (err) {
339
+ if (!attemptKey)
340
+ throw new Error('BYOK_KEY_REQUIRED: pass --key or run interactively');
341
+ throw err;
342
+ }
343
+ void activeMaster;
327
344
  }
328
345
  void fileKeyRaw;
329
346
  progress.update(i + 1, `chunk ${i + 1}/${chunks.length}`);
@@ -338,17 +355,52 @@ export async function downloadFile(target, destPath, onProgress) {
338
355
  return finalPath2;
339
356
  }
340
357
  catch (e) {
341
- errors.push(`cluster: ${e instanceof Error ? e.message : String(e)}`);
358
+ const msg = e instanceof Error ? e.message : String(e);
359
+ if (msg.startsWith('VAULT_KEY_DRIFT'))
360
+ throw e;
361
+ if (msg.startsWith('WEB_SESSION_REQUIRED')) {
362
+ // No fresh web JWT — fall back to signed-mode + vault key (no JWT needed).
363
+ try {
364
+ const key0 = defaultKey || tagsKey || '';
365
+ const usable = key0 && !/^(managed_key|sha256:.*|byok_encrypted|byok_protected)$/.test(key0);
366
+ const vaultKey = usable ? key0 : await fetchVaultKey(target.id);
367
+ if (!vaultKey && !usable && encrypted)
368
+ throw new Error(msg);
369
+ const sres = await apiRaw(`/download/${target.id}?signed=true`);
370
+ if (!sres.ok)
371
+ throw new Error(`signed HTTP ${sres.status}`);
372
+ const smeta = (await sres.json());
373
+ if (!smeta?.success || !smeta.signed_urls?.length)
374
+ throw new Error('signed: empty');
375
+ return await fetchDecryptWrite(smeta.signed_urls, target, destPath, onProgress, vaultKey ?? undefined, chunks.length);
376
+ }
377
+ catch (e2) {
378
+ errors.push(`signed: ${e2 instanceof Error ? e2.message : String(e2)}`);
379
+ }
380
+ }
381
+ errors.push(`cluster: ${msg}`);
342
382
  }
343
383
  }
344
384
  throw new Error(errors.join(' · ') || 'No downloadable strategy succeeded');
345
385
  }
386
+ let byokCache;
387
+ async function askByokKey(fileName) {
388
+ if (byokCache !== undefined)
389
+ return byokCache;
390
+ if (!process.stdin.isTTY || process.env.SQUIDCLOUD_NONINTERACTIVE === '1')
391
+ return null;
392
+ const readline = await import('node:readline');
393
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
394
+ const answer = await new Promise((res) => rl.question(`\u25cf decryption key for ${fileName}: `, (v) => { rl.close(); res(v); }));
395
+ byokCache = answer.trim() || null;
396
+ return byokCache;
397
+ }
346
398
  async function resolveKey(fileId, tagsKey) {
347
399
  if (tagsKey && !/^(managed_key|sha256:\S+|byok_encrypted|byok_protected)$/.test(tagsKey))
348
400
  return tagsKey;
349
401
  return fetchVaultKey(fileId);
350
402
  }
351
- async function fetchDecryptWrite(urls, target, destPath, onProgress) {
403
+ async function fetchDecryptWrite(urls, target, destPath, onProgress, preKey, declaredChunks) {
352
404
  const totalChunks = urls.length;
353
405
  const finalPath = destPath || target.name;
354
406
  const out = fs.createWriteStream(finalPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidcloudctl",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "SquidCloud CLI (squidcloudctl) — encrypted storage, shares and secrets from your terminal",
5
5
  "main": "dist/bin/squidcloud.js",
6
6
  "bin": {