squidcloudctl 3.0.3 → 3.0.5
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/dist/bin/upload-v3.txt +1 -0
- package/dist/lib/transfer.js +98 -37
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
upload test v3
|
package/dist/lib/transfer.js
CHANGED
|
@@ -138,10 +138,18 @@ async function fetchVaultKey(fileId) {
|
|
|
138
138
|
},
|
|
139
139
|
body: JSON.stringify({ fileId }),
|
|
140
140
|
});
|
|
141
|
+
if (r.status === 500) {
|
|
142
|
+
const t = await r.text().catch(() => '');
|
|
143
|
+
if (/decrypt file key/i.test(t)) {
|
|
144
|
+
throw new Error('VAULT_KEY_DRIFT: the SQUIDVEIL_MASTER_KEY secret has drifted since this file was uploaded - restore it in Supabase > Edge Functions > Secrets to decrypt managed files.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
141
147
|
const d = (await r.json());
|
|
142
148
|
return d?.success && d.key ? d.key : null;
|
|
143
149
|
}
|
|
144
|
-
catch {
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err instanceof Error && err.message.startsWith('VAULT_KEY_DRIFT'))
|
|
152
|
+
throw err;
|
|
145
153
|
return null;
|
|
146
154
|
}
|
|
147
155
|
}
|
|
@@ -222,9 +230,56 @@ async function bridgeResolve(apiKey, chunks) {
|
|
|
222
230
|
return u;
|
|
223
231
|
});
|
|
224
232
|
}
|
|
233
|
+
/** Frontend-parity chunk transport: squidcloud-cluster issues the signed
|
|
234
|
+
object URL for each chunk. Requires a real Supabase JWT (sb_token from
|
|
235
|
+
browser login). */
|
|
236
|
+
async function getSupabaseJwt() {
|
|
237
|
+
const { sbToken, sbTokenExpiresAt } = await import('./auth/oauth-session.js');
|
|
238
|
+
if (sbToken && (!sbTokenExpiresAt || Date.now() < sbTokenExpiresAt))
|
|
239
|
+
return sbToken;
|
|
240
|
+
throw new Error('WEB_SESSION_REQUIRED: this download needs a short-lived web session token. Run `squidcloudctl login` again, then retry.');
|
|
241
|
+
}
|
|
242
|
+
async function clusterDownloadChunk(chunk, userId) {
|
|
243
|
+
const { SUPABASE_URL } = await import('./auth/constants.js');
|
|
244
|
+
const jwt = await getSupabaseJwt();
|
|
245
|
+
const r = await fetch(`${SUPABASE_URL}/functions/v1/squidcloud-cluster`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}` },
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
action: 'download',
|
|
250
|
+
path: chunk.path,
|
|
251
|
+
repo: chunk.repo,
|
|
252
|
+
clusterId: chunk.clusterId,
|
|
253
|
+
bucket: chunk.bucket,
|
|
254
|
+
userId,
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
const text = await r.text();
|
|
258
|
+
let data;
|
|
259
|
+
try {
|
|
260
|
+
data = JSON.parse(text);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
throw new Error(`cluster HTTP ${r.status}: ${text.slice(0, 120)}`);
|
|
264
|
+
}
|
|
265
|
+
if (!r.ok)
|
|
266
|
+
throw new Error(data?.error || `cluster HTTP ${r.status}`);
|
|
267
|
+
if (data.legacy) {
|
|
268
|
+
const decoded = Buffer.from(String(data.content).replace(/\s/g, ''), 'base64').toString('utf8');
|
|
269
|
+
const parsed = JSON.parse(decoded);
|
|
270
|
+
const b64 = parsed.v === '2.3' ? parsed.d : parsed.chunkData;
|
|
271
|
+
return new Uint8Array(Buffer.from(String(b64), 'base64'));
|
|
272
|
+
}
|
|
273
|
+
if (!data.downloadUrl)
|
|
274
|
+
throw new Error('No download URL received');
|
|
275
|
+
const obj = await fetch(data.downloadUrl);
|
|
276
|
+
if (!obj.ok)
|
|
277
|
+
throw new Error(`CDN HTTP ${obj.status}`);
|
|
278
|
+
return new Uint8Array(await obj.arrayBuffer());
|
|
279
|
+
}
|
|
225
280
|
export async function downloadFile(target, destPath, onProgress) {
|
|
226
281
|
const errors = [];
|
|
227
|
-
//
|
|
282
|
+
// Parse metadata exactly like the web app.
|
|
228
283
|
let chunks = [];
|
|
229
284
|
let tagsKey;
|
|
230
285
|
let encrypted = false;
|
|
@@ -238,46 +293,52 @@ export async function downloadFile(target, destPath, onProgress) {
|
|
|
238
293
|
if (!target.userId && typeof row.user_id === 'string')
|
|
239
294
|
target.userId = row.user_id;
|
|
240
295
|
}
|
|
241
|
-
catch { /*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
catch { /* optional */ }
|
|
297
|
+
// Frontend key-resolution order.
|
|
298
|
+
let defaultKey = tagsKey;
|
|
299
|
+
if (defaultKey === 'managed_key')
|
|
300
|
+
defaultKey = (await fetchVaultKey(target.id)) ?? undefined;
|
|
301
|
+
// ── Strategy A: exact frontend transport (squidcloud-cluster) ──
|
|
302
|
+
if (chunks.length > 0) {
|
|
246
303
|
try {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
const finalPath2 = destPath || target.name;
|
|
305
|
+
const out = fs.createWriteStream(finalPath2);
|
|
306
|
+
const progress = new TransferProgress('download', target.size ?? 0, process.stdout.isTTY);
|
|
307
|
+
const noble = await import('@noble/hashes/hkdf.js');
|
|
308
|
+
const sha2 = await import('@noble/hashes/sha2.js');
|
|
309
|
+
const enc = new TextEncoder();
|
|
310
|
+
const { detectChunkVersion, decryptAny, decryptV3, deriveRawKey } = await import('./crypto/res54.js');
|
|
311
|
+
const master = deriveRawKey(defaultKey || '');
|
|
312
|
+
const fileKeyRaw = noble.hkdf(sha2.sha256, master, new Uint8Array(0), enc.encode('v3-file-master' + target.id), 32);
|
|
313
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
314
|
+
const blob = await clusterDownloadChunk(chunks[i], target.userId || '');
|
|
315
|
+
const version = detectChunkVersion(blob);
|
|
316
|
+
let plain;
|
|
317
|
+
if (version === 3) {
|
|
318
|
+
plain = decryptV3(blob, master, target.userId || '', target.id, i, chunks.length);
|
|
252
319
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
else if (version === 1 && !encrypted) {
|
|
321
|
+
plain = blob;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
plain = decryptAny(blob, defaultKey || '', {
|
|
325
|
+
userId: target.userId, fileId: target.id, chunkIndex: i, totalChunks: chunks.length,
|
|
326
|
+
}).plain;
|
|
327
|
+
}
|
|
328
|
+
void fileKeyRaw;
|
|
329
|
+
progress.update(i + 1, `chunk ${i + 1}/${chunks.length}`);
|
|
330
|
+
void onProgress;
|
|
331
|
+
const buf = Buffer.from(plain);
|
|
332
|
+
if (!out.write(buf))
|
|
333
|
+
await new Promise((r2) => out.once('drain', () => r2()));
|
|
257
334
|
}
|
|
335
|
+
out.end();
|
|
336
|
+
await new Promise((resolve, reject) => { out.on('finish', () => resolve()); out.on('error', reject); });
|
|
337
|
+
progress.finish();
|
|
338
|
+
return finalPath2;
|
|
258
339
|
}
|
|
259
340
|
catch (e) {
|
|
260
|
-
errors.push(`
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// 2) Bridge mode (SquidFS-origin files).
|
|
264
|
-
if (hasBridgeOnly) {
|
|
265
|
-
try {
|
|
266
|
-
const apiKey = process.env.SQUIDCLOUD_API_KEY ?? null;
|
|
267
|
-
const urls = await bridgeResolve(apiKey, chunks.map((c) => ({ path: c.path, index: c.index, bucket: c.bucket })));
|
|
268
|
-
return await fetchDecryptWrite(urls, target, destPath, onProgress);
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
errors.push(`bridge: ${e instanceof Error ? e.message : String(e)}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
// 3) Plain server-decrypted stream (non-distributed storage).
|
|
275
|
-
if (!encrypted && chunks.length === 0) {
|
|
276
|
-
try {
|
|
277
|
-
return await streamPlain(`/download/${target.id}`, target.name, destPath, onProgress);
|
|
278
|
-
}
|
|
279
|
-
catch (e) {
|
|
280
|
-
errors.push(`plain: ${e instanceof Error ? e.message : String(e)}`);
|
|
341
|
+
errors.push(`cluster: ${e instanceof Error ? e.message : String(e)}`);
|
|
281
342
|
}
|
|
282
343
|
}
|
|
283
344
|
throw new Error(errors.join(' · ') || 'No downloadable strategy succeeded');
|